Skip to main content

big_code_analysis/metrics/
nom.rs

1// Per-language metric and AST modules deliberately consume the macro-
2// generated tree-sitter token enums via `use crate::*` and `use Foo::*`
3// inside match expressions — explicit imports would list dozens of
4// variants per arm and obscure the per-language token sets that are the
5// point of these files. Allowed at the module level rather than per
6// function so the per-language impl blocks stay readable.
7#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
8// Metric counts (token, function, branch, argument, etc.) are stored as
9// `usize` and crossed with `f64` averages, ratios, and Halstead scores
10// across the cyclomatic / MI / Halstead computations. The `usize as f64`
11// and `f64 as usize` casts are intentional and snapshot-anchored — every
12// site is bounded by the count it came from. Allowing the lints at the
13// module level keeps the metric arithmetic legible.
14#![allow(
15    clippy::cast_precision_loss,
16    clippy::cast_possible_truncation,
17    clippy::cast_sign_loss
18)]
19
20use std::fmt;
21
22use crate::checker::Checker;
23use crate::macros::implement_metric_trait;
24
25use crate::*;
26
27/// The `Nom` metric suite.
28#[derive(Clone, Debug, PartialEq)]
29#[non_exhaustive]
30pub struct Stats {
31    functions: usize,
32    closures: usize,
33    functions_sum: usize,
34    closures_sum: usize,
35    functions_min: usize,
36    functions_max: usize,
37    closures_min: usize,
38    closures_max: usize,
39    space_count: usize,
40}
41
42impl Default for Stats {
43    fn default() -> Self {
44        Self {
45            functions: 0,
46            closures: 0,
47            functions_sum: 0,
48            closures_sum: 0,
49            functions_min: usize::MAX,
50            functions_max: 0,
51            closures_min: usize::MAX,
52            closures_max: 0,
53            space_count: 1,
54        }
55    }
56}
57
58impl fmt::Display for Stats {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        write!(
61            f,
62            "functions: {}, \
63             closures: {}, \
64             functions_average: {}, \
65             closures_average: {}, \
66             total: {}, \
67             average: {}, \
68             functions_min: {}, \
69             functions_max: {}, \
70             closures_min: {}, \
71             closures_max: {}",
72            self.functions_sum(),
73            self.closures_sum(),
74            self.functions_average(),
75            self.closures_average(),
76            self.total(),
77            self.average(),
78            self.functions_min(),
79            self.functions_max(),
80            self.closures_min(),
81            self.closures_max(),
82        )
83    }
84}
85
86impl Stats {
87    /// Merges a second `Nom` metric suite into the first one
88    pub fn merge(&mut self, other: &Stats) {
89        self.functions_min = self.functions_min.min(other.functions_min);
90        self.functions_max = self.functions_max.max(other.functions_max);
91        self.closures_min = self.closures_min.min(other.closures_min);
92        self.closures_max = self.closures_max.max(other.closures_max);
93        self.functions_sum += other.functions_sum;
94        self.closures_sum += other.closures_sum;
95        self.space_count += other.space_count;
96    }
97
98    /// Counts the number of function definitions in a scope
99    #[inline]
100    #[must_use]
101    pub fn functions(&self) -> u64 {
102        // Only function definitions are considered, not general declarations
103        self.functions as u64
104    }
105
106    /// Counts the number of closures in a scope
107    #[inline]
108    #[must_use]
109    pub fn closures(&self) -> u64 {
110        self.closures as u64
111    }
112
113    /// Return the sum metric for functions
114    #[inline]
115    #[must_use]
116    pub fn functions_sum(&self) -> u64 {
117        // Only function definitions are considered, not general declarations
118        self.functions_sum as u64
119    }
120
121    /// Return the sum metric for closures
122    #[inline]
123    #[must_use]
124    pub fn closures_sum(&self) -> u64 {
125        self.closures_sum as u64
126    }
127
128    /// Returns the average number of function definitions over all spaces.
129    ///
130    /// `nom`'s averages are **per space** (`space_count`). The `.max(1)`
131    /// guard keeps a space-less aggregate from dividing by zero
132    /// (`space_count` defaults to 1 and only grows, so today it is a
133    /// no-op, but the guard removes the reliance on that invariant — cf.
134    /// the divisor guards added for #428).
135    #[inline]
136    #[must_use]
137    pub fn functions_average(&self) -> f64 {
138        crate::metrics::average(self.functions_sum() as f64, self.space_count)
139    }
140
141    /// Returns the average number of closures over all spaces.
142    #[inline]
143    #[must_use]
144    pub fn closures_average(&self) -> f64 {
145        crate::metrics::average(self.closures_sum() as f64, self.space_count)
146    }
147
148    /// Returns the average number of function definitions and closures over all spaces.
149    #[inline]
150    #[must_use]
151    pub fn average(&self) -> f64 {
152        crate::metrics::average(self.total() as f64, self.space_count)
153    }
154
155    /// Counts the number of function definitions in a scope.
156    ///
157    /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
158    /// into `functions_min` to `0.0`, so a never-observed space
159    /// serializes to a meaningful number rather than `1.8446744e19`.
160    #[inline]
161    #[must_use]
162    pub fn functions_min(&self) -> u64 {
163        // Only function definitions are considered, not general declarations
164        if self.functions_min == usize::MAX {
165            0
166        } else {
167            self.functions_min as u64
168        }
169    }
170
171    /// Counts the number of closures in a scope.
172    ///
173    /// Same `usize::MAX` sentinel collapse as `functions_min`.
174    #[inline]
175    #[must_use]
176    pub fn closures_min(&self) -> u64 {
177        if self.closures_min == usize::MAX {
178            0
179        } else {
180            self.closures_min as u64
181        }
182    }
183    /// Counts the number of function definitions in a scope
184    #[inline]
185    #[must_use]
186    pub fn functions_max(&self) -> u64 {
187        // Only function definitions are considered, not general declarations
188        self.functions_max as u64
189    }
190
191    /// Counts the number of closures in a scope
192    #[inline]
193    #[must_use]
194    pub fn closures_max(&self) -> u64 {
195        self.closures_max as u64
196    }
197    /// Returns the total number of function definitions and
198    /// closures in a scope
199    #[inline]
200    #[must_use]
201    pub fn total(&self) -> u64 {
202        self.functions_sum() + self.closures_sum()
203    }
204    #[inline]
205    pub(crate) fn compute_sum(&mut self) {
206        self.functions_sum += self.functions;
207        self.closures_sum += self.closures;
208    }
209    #[inline]
210    pub(crate) fn compute_minmax(&mut self) {
211        self.functions_min = self.functions_min.min(self.functions);
212        self.functions_max = self.functions_max.max(self.functions);
213        self.closures_min = self.closures_min.min(self.closures);
214        self.closures_max = self.closures_max.max(self.closures);
215        self.compute_sum();
216    }
217}
218
219#[doc(hidden)]
220/// Per-language counting of methods (functions + closures).
221pub(crate) trait Nom
222where
223    Self: Checker,
224{
225    /// Walk `node` and update `stats` with this metric for the language
226    /// implementing the trait.
227    ///
228    /// Uses the source-aware [`Checker::is_func_with_code`] rather than the
229    /// byte-less `is_func`. For every grammar that exposes a syntactic
230    /// function-definition node, `is_func_with_code` forwards to `is_func`,
231    /// so the count is unchanged. Elixir is the exception: its `def`/`defp`/
232    /// `defmacro`/`defmacrop` declarations are ordinary `Call` nodes that
233    /// only a source-text lookup can recognise, so its `is_func_with_code`
234    /// override now classifies them as functions instead of leaving
235    /// `functions_sum` permanently at 0 (#696).
236    ///
237    /// `ancestors` is the chain the walker descended through; Elixir's
238    /// `is_func_with_code` needs it to spot a `def` inside a `quote`
239    /// template in `O(1)` per step rather than `O(depth)` (#1084).
240    fn compute<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
241        if Self::is_func_with_code(node, code, ancestors) {
242            stats.functions += 1;
243            return;
244        }
245        if Self::is_closure(node, ancestors) {
246            stats.closures += 1;
247        }
248    }
249}
250
251implement_metric_trait!(
252    [Nom],
253    PythonCode,
254    MozjsCode,
255    JavascriptCode,
256    TypescriptCode,
257    TsxCode,
258    CppCode,
259    MozcppCode,
260    CCode,
261    ObjcCode,
262    RustCode,
263    PreprocCode,
264    CcommentCode,
265    JavaCode,
266    KotlinCode,
267    GoCode,
268    PerlCode,
269    BashCode,
270    LuaCode,
271    TclCode,
272    PhpCode,
273    CsharpCode,
274    ElixirCode,
275    RubyCode,
276    GroovyCode,
277    IrulesCode
278);
279
280#[cfg(test)]
281#[allow(
282    clippy::float_cmp,
283    clippy::cast_precision_loss,
284    clippy::cast_possible_truncation,
285    clippy::cast_sign_loss,
286    clippy::similar_names,
287    clippy::doc_markdown,
288    clippy::needless_raw_string_hashes,
289    clippy::too_many_lines
290)]
291mod tests {
292    use crate::test_support::check_metrics_only_shim;
293
294    use super::*;
295
296    check_metrics_only_shim!(check_metrics, Nom);
297    // The C# indexer / property tests (#464, #472) assert that `nom`'s
298    // function count agrees with `npm`'s method count for the same
299    // member, so they need both families computed.
300    check_metrics_only_shim!(check_nom_and_npm, Nom, Npm);
301
302    /// Regression for #227: a `Stats::default()` that never sees an
303    /// observation must not leak the `usize::MAX` sentinel for
304    /// `functions_min` or `closures_min`. Both getters collapse the
305    /// sentinel to `0.0` so JSON never emits `1.8446744e19`.
306    #[test]
307    fn nom_empty_file_min_is_zero() {
308        let stats = Stats::default();
309        assert_eq!(stats.functions_min(), 0);
310        assert_eq!(stats.closures_min(), 0);
311    }
312
313    #[cfg(feature = "python")]
314    #[test]
315    fn python_nom() {
316        check_metrics::<PythonParser>(
317            "def a():
318                 pass
319             def b():
320                 pass
321             def c():
322                 pass
323             x = lambda a : a + 42",
324            "foo.py",
325            |metric| {
326                // Number of spaces = 4
327                // The `lambda` is detected as a closure via
328                // `PythonCode::is_closure` (widened to accept both
329                // aliased kind_ids in #419); pin the count explicitly
330                // so a regression in the predicate fails loudly.
331                assert_eq!(metric.nom.closures_sum(), 1);
332                insta::assert_json_snapshot!(
333                    metric.nom,
334                    @r#"
335                {
336                  "functions": 3,
337                  "closures": 1,
338                  "functions_average": 0.75,
339                  "closures_average": 0.25,
340                  "total": 4,
341                  "average": 1.0,
342                  "functions_min": 0,
343                  "functions_max": 1,
344                  "closures_min": 0,
345                  "closures_max": 1
346                }
347                "#
348                );
349            },
350        );
351    }
352
353    #[cfg(feature = "rust")]
354    #[test]
355    fn rust_nom() {
356        check_metrics::<RustParser>(
357            "mod A { fn foo() {}}
358             mod B { fn foo() {}}
359             let closure = |i: i32| -> i32 { i + 42 };",
360            "foo.rs",
361            |metric| {
362                // Number of spaces = 4
363                insta::assert_json_snapshot!(
364                    metric.nom,
365                    @r#"
366                {
367                  "functions": 2,
368                  "closures": 1,
369                  "functions_average": 0.5,
370                  "closures_average": 0.25,
371                  "total": 3,
372                  "average": 0.75,
373                  "functions_min": 0,
374                  "functions_max": 1,
375                  "closures_min": 0,
376                  "closures_max": 1
377                }
378                "#
379                );
380            },
381        );
382    }
383
384    #[cfg(feature = "c")]
385    #[test]
386    fn c_nom() {
387        check_metrics::<CParser>(
388            "int foo();
389
390             int foo() {
391                 return 0;
392             }",
393            "foo.c",
394            |metric| {
395                // Number of spaces = 2
396                insta::assert_json_snapshot!(
397                    metric.nom,
398                    @r#"
399                {
400                  "functions": 1,
401                  "closures": 0,
402                  "functions_average": 0.5,
403                  "closures_average": 0.0,
404                  "total": 1,
405                  "average": 0.5,
406                  "functions_min": 0,
407                  "functions_max": 1,
408                  "closures_min": 0,
409                  "closures_max": 0
410                }
411                "#
412                );
413            },
414        );
415    }
416
417    #[cfg(feature = "cpp")]
418    #[test]
419    fn cpp_nom() {
420        check_metrics::<CppParser>(
421            "struct A {
422                 void foo(int) {}
423                 void foo(double) {}
424             };
425             int b = [](int x) -> int { return x + 42; };",
426            "foo.cpp",
427            |metric| {
428                // Number of spaces = 4
429                insta::assert_json_snapshot!(
430                    metric.nom,
431                    @r#"
432                {
433                  "functions": 2,
434                  "closures": 1,
435                  "functions_average": 0.5,
436                  "closures_average": 0.25,
437                  "total": 3,
438                  "average": 0.75,
439                  "functions_min": 0,
440                  "functions_max": 1,
441                  "closures_min": 0,
442                  "closures_max": 1
443                }
444                "#
445                );
446            },
447        );
448    }
449
450    /// Free functions and member functions both surface as
451    /// `Cpp::FunctionDefinition` and count toward `functions`.  Member
452    /// functions are nested inside a struct/class space; the count is on
453    /// the function-definition node itself, not on the enclosing scope.
454    #[cfg(feature = "cpp")]
455    #[test]
456    fn cpp_free_and_member_functions() {
457        check_metrics::<CppParser>(
458            "int free_fn(int x) { return x; }
459             struct S {
460                 int member_fn(int x) { return x + 1; }
461             };",
462            "foo.cpp",
463            |metric| {
464                // 2 functions: `free_fn`, `S::member_fn`.
465                let s = &metric.nom;
466                assert_eq!(s.functions_sum(), 2);
467                assert_eq!(s.closures_sum(), 0);
468                assert_eq!(s.total(), 2);
469                insta::assert_json_snapshot!(metric.nom);
470            },
471        );
472    }
473
474    /// `static` member functions still surface as `Cpp::FunctionDefinition`
475    /// — the `static` keyword is a storage-class specifier, not a separate
476    /// node kind — so they are counted just like non-static members.
477    #[cfg(feature = "cpp")]
478    #[test]
479    fn cpp_static_member_function() {
480        check_metrics::<CppParser>(
481            "struct S {
482                 static int factory(int x) { return x; }
483                 int method(int x) { return x + 1; }
484             };",
485            "foo.cpp",
486            |metric| {
487                let s = &metric.nom;
488                assert_eq!(s.functions_sum(), 2);
489                assert_eq!(s.closures_sum(), 0);
490                insta::assert_json_snapshot!(metric.nom);
491            },
492        );
493    }
494
495    /// Constructor and destructor definitions surface as
496    /// `Cpp::FunctionDefinition` nodes with a `function_declarator` whose
497    /// identifier is the class name (ctor) or `~ClassName` (dtor).  Both
498    /// count as functions.
499    #[cfg(feature = "cpp")]
500    #[test]
501    fn cpp_constructor_and_destructor() {
502        check_metrics::<CppParser>(
503            "struct S {
504                 S() {}
505                 ~S() {}
506                 int method() { return 0; }
507             };",
508            "foo.cpp",
509            |metric| {
510                let s = &metric.nom;
511                // 3 functions: S(), ~S(), method.
512                assert_eq!(s.functions_sum(), 3);
513                assert_eq!(s.closures_sum(), 0);
514                insta::assert_json_snapshot!(metric.nom);
515            },
516        );
517    }
518
519    /// Operator overloads surface as `FunctionDefinition` whose declarator
520    /// has an `OperatorName` identifier (`operator+`, `operator==`).  Both
521    /// inline overloads count toward `functions`.
522    #[cfg(feature = "cpp")]
523    #[test]
524    fn cpp_operator_overloads() {
525        check_metrics::<CppParser>(
526            "struct V {
527                 int x;
528                 V operator+(const V& o) const { return V{x + o.x}; }
529                 bool operator==(const V& o) const { return x == o.x; }
530             };",
531            "foo.cpp",
532            |metric| {
533                let s = &metric.nom;
534                assert_eq!(s.functions_sum(), 2);
535                assert_eq!(s.closures_sum(), 0);
536                insta::assert_json_snapshot!(metric.nom);
537            },
538        );
539    }
540
541    /// Function-template definition counts as a single function — the
542    /// `template<>` prefix wraps a `FunctionDefinition` and does not
543    /// produce additional function-definition nodes.
544    #[cfg(feature = "cpp")]
545    #[test]
546    fn cpp_function_template() {
547        check_metrics::<CppParser>(
548            "template<typename T>
549             T identity(T x) { return x; }",
550            "foo.cpp",
551            |metric| {
552                let s = &metric.nom;
553                assert_eq!(s.functions_sum(), 1);
554                assert_eq!(s.closures_sum(), 0);
555                insta::assert_json_snapshot!(metric.nom);
556            },
557        );
558    }
559
560    /// Class-template member functions defined in-line each count as one
561    /// function.  The `template<>` head wraps the class, and the methods
562    /// inside it surface as ordinary `FunctionDefinition` nodes.
563    #[cfg(feature = "cpp")]
564    #[test]
565    fn cpp_class_template_members() {
566        check_metrics::<CppParser>(
567            "template<typename T>
568             struct Box {
569                 T value;
570                 T get() const { return value; }
571                 void set(T v) { value = v; }
572             };",
573            "foo.cpp",
574            |metric| {
575                let s = &metric.nom;
576                assert_eq!(s.functions_sum(), 2);
577                assert_eq!(s.closures_sum(), 0);
578                insta::assert_json_snapshot!(metric.nom);
579            },
580        );
581    }
582
583    /// Lambdas inside a function body count as `closures`, not as
584    /// `functions` — Cpp::LambdaExpression is the closure kind.  The
585    /// enclosing function adds 1 to `functions`; each lambda adds 1 to
586    /// `closures`.
587    #[cfg(feature = "cpp")]
588    #[test]
589    fn cpp_lambdas_inside_function() {
590        check_metrics::<CppParser>(
591            "int run() {
592                 auto add = [](int a, int b) { return a + b; };
593                 auto mul = [](int a, int b) { return a * b; };
594                 return add(1, 2) + mul(3, 4);
595             }",
596            "foo.cpp",
597            |metric| {
598                let s = &metric.nom;
599                // 1 enclosing function + 2 closures.
600                assert_eq!(s.functions_sum(), 1);
601                assert_eq!(s.closures_sum(), 2);
602                assert_eq!(s.total(), 3);
603                insta::assert_json_snapshot!(metric.nom);
604            },
605        );
606    }
607
608    #[cfg(feature = "javascript")]
609    #[test]
610    fn javascript_nom() {
611        check_metrics::<JavascriptParser>(
612            "function f(a, b) {
613                 function foo(a) {
614                     return a;
615                 }
616                 var bar = (function () {
617                     var counter = 0;
618                     return function () {
619                         counter += 1;
620                         return counter
621                     }
622                 })();
623                 return bar(foo(a), a);
624             }",
625            "foo.js",
626            |metric| {
627                // Number of spaces = 5.
628                // functions: f, foo
629                // closures:  the IIFE assigned to `bar`, and the
630                //            function it returns
631                //
632                // The IIFE moved to the closure side in #1188. It used to
633                // count as the function `bar` purely because its *result*
634                // was bound to a name: written `(function(){…})()` with
635                // the assignment dropped, the identical construct was
636                // already a closure. Both spellings are closures now.
637                insta::assert_json_snapshot!(
638                    metric.nom,
639                    @r#"
640                {
641                  "functions": 2,
642                  "closures": 2,
643                  "functions_average": 0.4,
644                  "closures_average": 0.4,
645                  "total": 4,
646                  "average": 0.8,
647                  "functions_min": 0,
648                  "functions_max": 1,
649                  "closures_min": 0,
650                  "closures_max": 1
651                }
652                "#
653                );
654            },
655        );
656    }
657
658    /// The two anonymous-function forms must be classified by the same
659    /// rule (#1188).
660    ///
661    /// `check_if_func!` and `check_if_arrow_func!` walk the ancestor
662    /// chain to decide whether an expression is *bound to a name* or used
663    /// positionally, and they used to disagree in three ways. Each case
664    /// below is one measured divergence; the function-expression and
665    /// arrow spellings must now agree.
666    ///
667    /// Uses `metrics_verbatim` rather than the `check_metrics` shim,
668    /// whose bare-`fn` callback cannot capture the case's label or its
669    /// expectation.
670    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
671    fn check_js_binding_site_parity(lang: crate::LANG) {
672        let split = |source: &str| {
673            let m = crate::test_support::metrics_verbatim(
674                lang,
675                source.as_bytes(),
676                crate::MetricsOptions::default(),
677            );
678            (m.nom.functions_sum(), m.nom.closures_sum())
679        };
680
681        // (label, function-expression form, arrow form, is it a function?)
682        let cases: &[(&str, &str, &str, bool)] = &[
683            // An IIFE is the same construct whether or not its *result*
684            // is bound. It used to be a function in the bound spelling
685            // only, because `$stop` was `Arguments` for functions and
686            // `CallExpression` for arrows — different tree levels, and an
687            // IIFE's chain carries no `arguments` node at all.
688            (
689                "bare IIFE",
690                "(function (c) { return c; })(1);",
691                "(() => 1)(1);",
692                false,
693            ),
694            (
695                "bound IIFE",
696                "const v = (function (c) { return c; })(1);",
697                "const v = (() => 1)(1);",
698                false,
699            ),
700            // A pair with a *non-identifier* key. The arrow reached the
701            // right verdict for a bare key only, through `$extra`'s
702            // `property_identifier` sibling, which a string key does not
703            // provide; `Pair` was in the function `$up` alone.
704            (
705                "string-keyed pair",
706                "({ \"k\": function () { return 1; } });",
707                "({ \"k\": () => 1 });",
708                true,
709            ),
710            // The mirror: `has_sibling(PropertyIdentifier)` was in the
711            // arrow `$extra` alone, so a class field initialiser was a
712            // function as an arrow and a closure as a function expression.
713            (
714                "class field initialiser",
715                "class C { p = function () { return 1; }; }",
716                "class C { p = () => 1; }",
717                true,
718            ),
719            // The three field-name spellings `$extra`'s
720            // `property_identifier` sibling does *not* cover. The first
721            // fix reached the identifier case only, so these stayed
722            // closures as arrows while their `function` spellings were
723            // already functions — the same divergence, surviving in the
724            // shapes the test did not exercise. `p = …` above is the one
725            // spelling that worked before the field-definition kind
726            // joined both `$up` lists.
727            (
728                "computed-key class field",
729                "class C { [\"k\"] = function () { return 1; }; }",
730                "class C { [\"k\"] = () => 1; }",
731                true,
732            ),
733            (
734                "string-named class field",
735                "class C { \"s\" = function () { return 1; }; }",
736                "class C { \"s\" = () => 1; }",
737                true,
738            ),
739            (
740                "private class field",
741                "class C { #p = function () { return 1; }; }",
742                "class C { #p = () => 1; }",
743                true,
744            ),
745            // Plain positional callbacks, unchanged by all of the above.
746            (
747                "positional callback",
748                "run(function () { return 1; });",
749                "run(() => 1);",
750                false,
751            ),
752            // A positional callback that is the *object* of a member
753            // expression. The first fix for the class-field divergence
754            // used `has_sibling(PropertyIdentifier)`, which tests every
755            // sibling rather than a binding position — so `.bind(this)`
756            // supplied the sibling and made this a function while the
757            // arrow spelling (which needs parentheses, interposing a
758            // node) stayed a closure. That re-created the very func/arrow
759            // divergence this test exists to forbid, in a new shape, and
760            // it moved real pdf.js corpus counts before review caught it.
761            // The binding site is now named structurally, by kind.
762            (
763                "member-expression object",
764                "run(function () { return 1; }.bind(this));",
765                "run((() => 1).bind(this));",
766                false,
767            ),
768        ];
769
770        for &(label, func_form, arrow_form, expected) in cases {
771            for (form, source) in [("function", func_form), ("arrow", arrow_form)] {
772                let (functions, closures) = split(source);
773                assert_eq!(
774                    functions == 1,
775                    expected,
776                    "{lang:?} {label} / {form}: expected a {}, got functions {functions} closures {closures}",
777                    if expected { "function" } else { "closure" },
778                );
779            }
780        }
781
782        // The divergence that must **survive**: for a function expression
783        // an `identifier` child is its optional name, so
784        // `run(function g(){})` is a function; for an arrow it is the
785        // un-parenthesised parameter, so `run(x => x)` must stay a
786        // closure. Unifying that one would reclassify the commonest
787        // callback shape in any JS corpus. Asserted in both directions so
788        // a future "unify the last one too" cannot pass quietly.
789        assert_eq!(
790            split("run(function g() { return 1; });").0,
791            1,
792            "{lang:?}: a named function expression carries its own name",
793        );
794        assert_eq!(
795            split("run(x => x);").1,
796            1,
797            "{lang:?}: an arrow's identifier child is its parameter, not a name",
798        );
799    }
800
801    #[cfg(feature = "javascript")]
802    #[test]
803    fn javascript_binding_site_parity() {
804        check_js_binding_site_parity(crate::LANG::Javascript);
805    }
806
807    #[cfg(feature = "mozjs")]
808    #[test]
809    fn mozjs_binding_site_parity() {
810        check_js_binding_site_parity(crate::LANG::Mozjs);
811    }
812
813    #[cfg(feature = "typescript")]
814    #[test]
815    fn typescript_binding_site_parity() {
816        check_js_binding_site_parity(crate::LANG::Typescript);
817    }
818
819    #[cfg(feature = "typescript")]
820    #[test]
821    fn tsx_binding_site_parity() {
822        check_js_binding_site_parity(crate::LANG::Tsx);
823    }
824
825    #[cfg(feature = "javascript")]
826    #[test]
827    fn javascript_call_nom() {
828        check_metrics::<JavascriptParser>(
829            "add_task(async function test_safe_mode() {
830                 gAppInfo.inSafeMode = true;
831             });",
832            "foo.js",
833            |metric| {
834                // Number of spaces = 2
835                // functions: test_safe_mode
836                insta::assert_json_snapshot!(
837                    metric.nom,
838                    @r#"
839                {
840                  "functions": 1,
841                  "closures": 0,
842                  "functions_average": 0.5,
843                  "closures_average": 0.0,
844                  "total": 1,
845                  "average": 0.5,
846                  "functions_min": 0,
847                  "functions_max": 1,
848                  "closures_min": 0,
849                  "closures_max": 0
850                }
851                "#
852                );
853            },
854        );
855    }
856
857    #[cfg(feature = "javascript")]
858    #[test]
859    fn javascript_assignment_nom() {
860        check_metrics::<JavascriptParser>(
861            "AnimationTest.prototype.enableDisplay = function(element) {};",
862            "foo.js",
863            |metric| {
864                // Number of spaces = 2
865                insta::assert_json_snapshot!(
866                    metric.nom,
867                    @r#"
868                {
869                  "functions": 1,
870                  "closures": 0,
871                  "functions_average": 0.5,
872                  "closures_average": 0.0,
873                  "total": 1,
874                  "average": 0.5,
875                  "functions_min": 0,
876                  "functions_max": 1,
877                  "closures_min": 0,
878                  "closures_max": 0
879                }
880                "#
881                );
882            },
883        );
884    }
885
886    #[cfg(feature = "javascript")]
887    #[test]
888    fn javascript_labeled_nom() {
889        check_metrics::<JavascriptParser>(
890            "toJSON: function() {
891                 return this.inspect(true);
892             }",
893            "foo.js",
894            |metric| {
895                // Number of spaces = 2
896                insta::assert_json_snapshot!(
897                    metric.nom,
898                    @r#"
899                {
900                  "functions": 1,
901                  "closures": 0,
902                  "functions_average": 0.5,
903                  "closures_average": 0.0,
904                  "total": 1,
905                  "average": 0.5,
906                  "functions_min": 0,
907                  "functions_max": 1,
908                  "closures_min": 0,
909                  "closures_max": 0
910                }
911                "#
912                );
913            },
914        );
915    }
916
917    #[cfg(feature = "javascript")]
918    #[test]
919    fn javascript_labeled_arrow_nom() {
920        check_metrics::<JavascriptParser>(
921            "const dimConverters = {
922                pt: x => x,
923             };",
924            "foo.js",
925            |metric| {
926                // Number of spaces = 2
927                insta::assert_json_snapshot!(
928                    metric.nom,
929                    @r#"
930                {
931                  "functions": 1,
932                  "closures": 0,
933                  "functions_average": 0.5,
934                  "closures_average": 0.0,
935                  "total": 1,
936                  "average": 0.5,
937                  "functions_min": 0,
938                  "functions_max": 1,
939                  "closures_min": 0,
940                  "closures_max": 0
941                }
942                "#
943                );
944            },
945        );
946    }
947
948    #[cfg(feature = "javascript")]
949    #[test]
950    fn javascript_pair_nom() {
951        check_metrics::<JavascriptParser>(
952            "return {
953                 initialize: function(object) {
954                     this._object = object.toObject();
955                 },
956             }",
957            "foo.js",
958            |metric| {
959                // Number of spaces = 2
960                insta::assert_json_snapshot!(
961                    metric.nom,
962                    @r#"
963                {
964                  "functions": 1,
965                  "closures": 0,
966                  "functions_average": 0.5,
967                  "closures_average": 0.0,
968                  "total": 1,
969                  "average": 0.5,
970                  "functions_min": 0,
971                  "functions_max": 1,
972                  "closures_min": 0,
973                  "closures_max": 0
974                }
975                "#
976                );
977            },
978        );
979    }
980
981    #[cfg(any(feature = "javascript", feature = "mozjs", feature = "typescript"))]
982    fn check_returned_object_arrow_nom<T: MetricSuite>(file_name: &str) {
983        check_metrics::<T>(
984            "function f() { return { foo: x => x }; }",
985            file_name,
986            |metric| {
987                insta::allow_duplicates! {
988                    insta::assert_json_snapshot!(
989                        metric.nom,
990                        @r#"
991                    {
992                      "functions": 2,
993                      "closures": 0,
994                      "functions_average": 0.6666666666666666,
995                      "closures_average": 0.0,
996                      "total": 2,
997                      "average": 0.6666666666666666,
998                      "functions_min": 0,
999                      "functions_max": 1,
1000                      "closures_min": 0,
1001                      "closures_max": 0
1002                    }
1003                    "#
1004                    );
1005                }
1006            },
1007        );
1008    }
1009
1010    #[cfg(feature = "javascript")]
1011    #[test]
1012    fn javascript_returned_object_arrow_nom() {
1013        check_returned_object_arrow_nom::<JavascriptParser>("foo.js");
1014    }
1015
1016    #[cfg(feature = "mozjs")]
1017    #[test]
1018    fn mozjs_returned_object_arrow_nom() {
1019        check_returned_object_arrow_nom::<MozjsParser>("foo.js");
1020    }
1021
1022    #[cfg(feature = "typescript")]
1023    #[test]
1024    fn typescript_returned_object_arrow_nom() {
1025        check_returned_object_arrow_nom::<TypescriptParser>("foo.ts");
1026    }
1027
1028    #[cfg(feature = "typescript")]
1029    #[test]
1030    fn tsx_returned_object_arrow_nom() {
1031        check_returned_object_arrow_nom::<TsxParser>("foo.tsx");
1032    }
1033
1034    #[cfg(feature = "javascript")]
1035    #[test]
1036    fn javascript_unnamed_nom() {
1037        check_metrics::<JavascriptParser>(
1038            "Ajax.getTransport = Try.these(
1039                 function() {
1040                     return function(){ return new XMLHttpRequest()}
1041                 }
1042             );",
1043            "foo.js",
1044            |metric| {
1045                // Number of spaces = 3
1046                insta::assert_json_snapshot!(
1047                    metric.nom,
1048                    @r#"
1049                {
1050                  "functions": 0,
1051                  "closures": 2,
1052                  "functions_average": 0.0,
1053                  "closures_average": 0.6666666666666666,
1054                  "total": 2,
1055                  "average": 0.6666666666666666,
1056                  "functions_min": 0,
1057                  "functions_max": 0,
1058                  "closures_min": 0,
1059                  "closures_max": 1
1060                }
1061                "#
1062                );
1063            },
1064        );
1065    }
1066
1067    #[cfg(feature = "javascript")]
1068    #[test]
1069    fn javascript_arrow_nom() {
1070        check_metrics::<JavascriptParser>(
1071            "var materials = [\"Hydrogen\"];
1072             materials.map(material => material.length);
1073             let add = (a, b)  => a + b;",
1074            "foo.js",
1075            |metric| {
1076                // Number of spaces = 3
1077                // Functions: add
1078                // Closures: material.map
1079                insta::assert_json_snapshot!(
1080                    metric.nom,
1081                    @r#"
1082                {
1083                  "functions": 1,
1084                  "closures": 1,
1085                  "functions_average": 0.3333333333333333,
1086                  "closures_average": 0.3333333333333333,
1087                  "total": 2,
1088                  "average": 0.6666666666666666,
1089                  "functions_min": 0,
1090                  "functions_max": 1,
1091                  "closures_min": 0,
1092                  "closures_max": 1
1093                }
1094                "#
1095                );
1096            },
1097        );
1098    }
1099
1100    #[cfg(feature = "javascript")]
1101    #[test]
1102    fn javascript_arrow_assignment_nom() {
1103        check_metrics::<JavascriptParser>("sink.onPull = () => { };", "foo.js", |metric| {
1104            // Number of spaces = 2
1105            insta::assert_json_snapshot!(
1106                metric.nom,
1107                @r#"
1108            {
1109              "functions": 1,
1110              "closures": 0,
1111              "functions_average": 0.5,
1112              "closures_average": 0.0,
1113              "total": 1,
1114              "average": 0.5,
1115              "functions_min": 0,
1116              "functions_max": 1,
1117              "closures_min": 0,
1118              "closures_max": 0
1119            }
1120            "#
1121            );
1122        });
1123    }
1124
1125    #[cfg(feature = "javascript")]
1126    #[test]
1127    fn javascript_arrow_new_nom() {
1128        check_metrics::<JavascriptParser>(
1129            "const response = new Promise(resolve => channel.port1.onmessage = resolve);",
1130            "foo.js",
1131            |metric| {
1132                // Number of spaces = 2
1133                insta::assert_json_snapshot!(
1134                    metric.nom,
1135                    @r#"
1136                {
1137                  "functions": 0,
1138                  "closures": 1,
1139                  "functions_average": 0.0,
1140                  "closures_average": 0.5,
1141                  "total": 1,
1142                  "average": 0.5,
1143                  "functions_min": 0,
1144                  "functions_max": 0,
1145                  "closures_min": 0,
1146                  "closures_max": 1
1147                }
1148                "#
1149                );
1150            },
1151        );
1152    }
1153
1154    #[cfg(feature = "javascript")]
1155    #[test]
1156    fn javascript_arrow_call_nom() {
1157        check_metrics::<JavascriptParser>(
1158            "let notDisabled = TestUtils.waitForCondition(
1159                 () => !backbutton.hasAttribute(\"disabled\")
1160             );",
1161            "foo.js",
1162            |metric| {
1163                // Number of spaces = 2
1164                insta::assert_json_snapshot!(
1165                    metric.nom,
1166                    @r#"
1167                {
1168                  "functions": 0,
1169                  "closures": 1,
1170                  "functions_average": 0.0,
1171                  "closures_average": 0.5,
1172                  "total": 1,
1173                  "average": 0.5,
1174                  "functions_min": 0,
1175                  "functions_max": 0,
1176                  "closures_min": 0,
1177                  "closures_max": 1
1178                }
1179                "#
1180                );
1181            },
1182        );
1183    }
1184
1185    #[cfg(feature = "java")]
1186    #[test]
1187    fn java_nom() {
1188        check_metrics::<JavaParser>(
1189            "class A {
1190                public void foo(){
1191                    return;
1192                }
1193                public void bar(){
1194                    return;
1195                }
1196            }",
1197            "foo.java",
1198            |metric| {
1199                // Number of spaces = 4
1200                insta::assert_json_snapshot!(
1201                    metric.nom,
1202                    @r#"
1203                {
1204                  "functions": 2,
1205                  "closures": 0,
1206                  "functions_average": 0.5,
1207                  "closures_average": 0.0,
1208                  "total": 2,
1209                  "average": 0.5,
1210                  "functions_min": 0,
1211                  "functions_max": 1,
1212                  "closures_min": 0,
1213                  "closures_max": 0
1214                }
1215                "#
1216                );
1217            },
1218        );
1219    }
1220
1221    #[cfg(feature = "csharp")]
1222    #[test]
1223    fn csharp_nom() {
1224        check_metrics::<CsharpParser>(
1225            "class A {
1226                public void Foo() {
1227                    return;
1228                }
1229                public void Bar() {
1230                    return;
1231                }
1232                public int X { get; set; }
1233                public void Outer() {
1234                    void Local() { return; }
1235                    Local();
1236                }
1237            }",
1238            "foo.cs",
1239            |metric| {
1240                // Methods: Foo, Bar, Outer (=3 explicit)
1241                // Plus accessors `get`, `set` on X = 2 more functions
1242                // Plus `Local` local function = 1 more
1243                // Total functions = 6
1244                insta::assert_json_snapshot!(
1245                    metric.nom,
1246                    @r#"
1247                {
1248                  "functions": 6,
1249                  "closures": 0,
1250                  "functions_average": 0.75,
1251                  "closures_average": 0.0,
1252                  "total": 6,
1253                  "average": 0.75,
1254                  "functions_min": 0,
1255                  "functions_max": 1,
1256                  "closures_min": 0,
1257                  "closures_max": 0
1258                }
1259                "#
1260                );
1261            },
1262        );
1263    }
1264
1265    #[cfg(feature = "csharp")]
1266    #[test]
1267    fn csharp_closure_nom() {
1268        check_metrics::<CsharpParser>(
1269            "class A {
1270                public void Run() {
1271                    System.Func<int, int> f = x => x + 1;
1272                    System.Action g = delegate(int y) { System.Console.WriteLine(y); };
1273                }
1274            }",
1275            "foo.cs",
1276            |metric| {
1277                // 1 method (Run), 1 lambda, 1 anonymous_method = 1 func + 2 closures.
1278                insta::assert_json_snapshot!(
1279                    metric.nom,
1280                    @r#"
1281                {
1282                  "functions": 1,
1283                  "closures": 2,
1284                  "functions_average": 0.2,
1285                  "closures_average": 0.4,
1286                  "total": 3,
1287                  "average": 0.6,
1288                  "functions_min": 0,
1289                  "functions_max": 1,
1290                  "closures_min": 0,
1291                  "closures_max": 1
1292                }
1293                "#
1294                );
1295            },
1296        );
1297    }
1298
1299    #[cfg(feature = "csharp")]
1300    #[test]
1301    fn csharp_indexer_nom() {
1302        // A bodied indexer defines two callable accessors (`get`, `set`).
1303        // Before #464 the `indexer_declaration` node itself ALSO opened a
1304        // function space, triple-counting the indexer as 3 functions. The
1305        // correct count is 2 — the accessor count — matching the npm path
1306        // (`csharp_count_member`) which reports `class_methods == 2`.
1307        check_nom_and_npm::<CsharpParser>(
1308            "class A {
1309                private int[] _d;
1310                public int this[int i] { get => _d[i]; set => _d[i] = value; }
1311            }",
1312            "foo.cs",
1313            |metric| {
1314                // expected: get + set accessors = 2 functions; the
1315                // IndexerDeclaration node no longer opens its own space.
1316                assert_eq!(metric.nom.functions_sum(), 2);
1317                assert_eq!(metric.npm.class_nm_sum(), 2);
1318                insta::assert_json_snapshot!(
1319                    metric.nom,
1320                    @r#"
1321                {
1322                  "functions": 2,
1323                  "closures": 0,
1324                  "functions_average": 0.5,
1325                  "closures_average": 0.0,
1326                  "total": 2,
1327                  "average": 0.5,
1328                  "functions_min": 0,
1329                  "functions_max": 1,
1330                  "closures_min": 0,
1331                  "closures_max": 0
1332                }
1333                "#
1334                );
1335            },
1336        );
1337    }
1338
1339    #[cfg(feature = "csharp")]
1340    #[test]
1341    fn csharp_expression_bodied_indexer_nom() {
1342        // An expression-bodied indexer (`this[int i] => _d[i];`) has NO
1343        // `accessor_declaration` child — it defines a single implicit
1344        // getter. Removing the IndexerDeclaration entry from is_func /
1345        // is_func_space outright would drop this to 0; the #464 fix gates
1346        // the entry on the absence of accessors so this form still counts
1347        // as 1, matching the npm `.max(1)` fallback.
1348        check_nom_and_npm::<CsharpParser>(
1349            "class A {
1350                private int[] _d;
1351                public int this[int i] => _d[i];
1352            }",
1353            "foo.cs",
1354            |metric| {
1355                // expected: one implicit getter, no accessor nodes => 1.
1356                assert_eq!(metric.nom.functions_sum(), 1);
1357                assert_eq!(metric.npm.class_nm_sum(), 1);
1358                insta::assert_json_snapshot!(
1359                    metric.nom,
1360                    @r#"
1361                {
1362                  "functions": 1,
1363                  "closures": 0,
1364                  "functions_average": 0.3333333333333333,
1365                  "closures_average": 0.0,
1366                  "total": 1,
1367                  "average": 0.3333333333333333,
1368                  "functions_min": 0,
1369                  "functions_max": 1,
1370                  "closures_min": 0,
1371                  "closures_max": 0
1372                }
1373                "#
1374                );
1375            },
1376        );
1377    }
1378
1379    #[cfg(feature = "csharp")]
1380    #[test]
1381    fn csharp_property_nom() {
1382        // A bodied property (`int W { get => _w; set => _w = value; }`)
1383        // defines two callable accessors. The `property_declaration` node
1384        // must NOT open its own space on top of them, else it double-counts
1385        // (the property analogue of #464). The correct count is 2 — the
1386        // accessor count — matching the npm path which reports 2.
1387        check_nom_and_npm::<CsharpParser>(
1388            "class A {
1389                private int _w;
1390                public int W { get => _w; set => _w = value; }
1391            }",
1392            "foo.cs",
1393            |metric| {
1394                // expected: get + set accessors = 2 functions; the
1395                // PropertyDeclaration node defers and opens no space (#472).
1396                assert_eq!(metric.nom.functions_sum(), 2);
1397                assert_eq!(metric.npm.class_nm_sum(), 2);
1398            },
1399        );
1400    }
1401
1402    #[cfg(feature = "csharp")]
1403    #[test]
1404    fn csharp_auto_property_nom() {
1405        // An auto-property (`int Y { get; set; }`) still has two
1406        // `accessor_declaration` children, so it defers to them exactly like
1407        // a bodied property — the #472 gate must not change this count.
1408        check_nom_and_npm::<CsharpParser>(
1409            "class A {
1410                public int Y { get; set; }
1411            }",
1412            "foo.cs",
1413            |metric| {
1414                // expected: get + set accessors = 2 functions, unchanged.
1415                assert_eq!(metric.nom.functions_sum(), 2);
1416                assert_eq!(metric.npm.class_nm_sum(), 2);
1417            },
1418        );
1419    }
1420
1421    #[cfg(feature = "csharp")]
1422    #[test]
1423    fn csharp_expression_bodied_property_nom() {
1424        // An expression-bodied property (`int W => _w;`) has NO
1425        // `accessor_declaration` child — it defines a single implicit
1426        // getter via an `arrow_expression_clause`. With no accessor to
1427        // defer to, the `property_declaration` opened no space at all
1428        // before #472 (0 functions). The fix gates the entry on the
1429        // absence of accessors so it counts as 1, matching the npm
1430        // `.max(1)` fallback.
1431        check_nom_and_npm::<CsharpParser>(
1432            "class A {
1433                private int _w;
1434                public int W => _w;
1435            }",
1436            "foo.cs",
1437            |metric| {
1438                // expected: one implicit getter, no accessor nodes => 1.
1439                assert_eq!(metric.nom.functions_sum(), 1);
1440                assert_eq!(metric.npm.class_nm_sum(), 1);
1441            },
1442        );
1443    }
1444
1445    #[cfg(feature = "go")]
1446    #[test]
1447    fn go_top_level_funcs() {
1448        check_metrics::<GoParser>(
1449            "package main
1450            func a() {}
1451            func b() {}
1452            func c() {}",
1453            "foo.go",
1454            |metric| {
1455                // Number of spaces = 4 (file unit + 3 funcs).
1456                insta::assert_json_snapshot!(
1457                    metric.nom,
1458                    @r#"
1459                {
1460                  "functions": 3,
1461                  "closures": 0,
1462                  "functions_average": 0.75,
1463                  "closures_average": 0.0,
1464                  "total": 3,
1465                  "average": 0.75,
1466                  "functions_min": 0,
1467                  "functions_max": 1,
1468                  "closures_min": 0,
1469                  "closures_max": 0
1470                }
1471                "#
1472                );
1473            },
1474        );
1475    }
1476
1477    #[cfg(feature = "go")]
1478    #[test]
1479    fn go_method_declaration() {
1480        check_metrics::<GoParser>(
1481            "package main
1482            type T struct{}
1483            func (r *T) M() {}",
1484            "foo.go",
1485            |metric| {
1486                // method_declaration is counted as a function.
1487                insta::assert_json_snapshot!(
1488                    metric.nom,
1489                    @r#"
1490                {
1491                  "functions": 1,
1492                  "closures": 0,
1493                  "functions_average": 0.5,
1494                  "closures_average": 0.0,
1495                  "total": 1,
1496                  "average": 0.5,
1497                  "functions_min": 0,
1498                  "functions_max": 1,
1499                  "closures_min": 0,
1500                  "closures_max": 0
1501                }
1502                "#
1503                );
1504            },
1505        );
1506    }
1507
1508    #[cfg(feature = "go")]
1509    #[test]
1510    fn go_func_literal_is_closure() {
1511        check_metrics::<GoParser>(
1512            "package main
1513            var f = func() {}",
1514            "foo.go",
1515            |metric| {
1516                // func_literal increments closure count, not function count.
1517                insta::assert_json_snapshot!(
1518                    metric.nom,
1519                    @r#"
1520                {
1521                  "functions": 0,
1522                  "closures": 1,
1523                  "functions_average": 0.0,
1524                  "closures_average": 0.5,
1525                  "total": 1,
1526                  "average": 0.5,
1527                  "functions_min": 0,
1528                  "functions_max": 0,
1529                  "closures_min": 0,
1530                  "closures_max": 1
1531                }
1532                "#
1533                );
1534            },
1535        );
1536    }
1537
1538    #[cfg(feature = "go")]
1539    #[test]
1540    fn go_nested_closures() {
1541        check_metrics::<GoParser>(
1542            "package main
1543            func f() {
1544                inner := func() {
1545                    deeper := func() {}
1546                    _ = deeper
1547                }
1548                _ = inner
1549            }",
1550            "foo.go",
1551            |metric| {
1552                // 1 function (f) + 2 closures (inner, deeper).
1553                insta::assert_json_snapshot!(
1554                    metric.nom,
1555                    @r#"
1556                {
1557                  "functions": 1,
1558                  "closures": 2,
1559                  "functions_average": 0.25,
1560                  "closures_average": 0.5,
1561                  "total": 3,
1562                  "average": 0.75,
1563                  "functions_min": 0,
1564                  "functions_max": 1,
1565                  "closures_min": 0,
1566                  "closures_max": 1
1567                }
1568                "#
1569                );
1570            },
1571        );
1572    }
1573
1574    /// Regression for #1160: `compact_constructor_declaration` was absent
1575    /// from `JavaCode::is_func`, so a record's compact constructor was
1576    /// invisible to every metric keyed on that predicate — `nom` included.
1577    ///
1578    /// The delegating `R(int a)` is the control: it is an ordinary
1579    /// `constructor_declaration` and was always counted, so a fixture with
1580    /// only the compact form could not distinguish "counted once" from
1581    /// "counted as the other constructor".
1582    #[cfg(feature = "java")]
1583    #[test]
1584    fn java_record_compact_constructor_counts_as_a_function() {
1585        check_metrics::<JavaParser>(
1586            "record R(int a, int b) {
1587                 R { if (a < 0) { throw new IllegalArgumentException(); } }
1588                 R(int a) { this(a, 0); }
1589                 int sum() { return a + b; }
1590             }",
1591            "R.java",
1592            |metric| {
1593                // compact constructor + delegating constructor + sum().
1594                // Pre-fix this reported 2.
1595                assert_eq!(metric.nom.functions_sum(), 3);
1596                assert_eq!(metric.nom.closures_sum(), 0);
1597            },
1598        );
1599    }
1600
1601    #[cfg(feature = "java")]
1602    #[test]
1603    fn java_closure_nom() {
1604        check_metrics::<JavaParser>(
1605            "interface printable{
1606                void print();
1607              }
1608
1609              interface IntFunc {
1610                int func(int n);
1611              }
1612
1613              class Printer implements printable{
1614                public void print(){System.out.println(\"Hello\");}
1615
1616                public static void main(String args[]){
1617                  Printer  obj = new Printer();
1618                  obj.print();
1619                  IntFunc meaning = (i) -> i + 42;
1620                  int i = meaning.func(1);
1621                }
1622              }",
1623            "foo.java",
1624            |metric| {
1625                // Number of spaces = 8
1626                insta::assert_json_snapshot!(
1627                    metric.nom,
1628                    @r#"
1629                {
1630                  "functions": 4,
1631                  "closures": 1,
1632                  "functions_average": 0.5,
1633                  "closures_average": 0.125,
1634                  "total": 5,
1635                  "average": 0.625,
1636                  "functions_min": 0,
1637                  "functions_max": 1,
1638                  "closures_min": 0,
1639                  "closures_max": 1
1640                }
1641                "#
1642                );
1643            },
1644        );
1645    }
1646
1647    #[cfg(feature = "groovy")]
1648    #[test]
1649    fn groovy_nom() {
1650        check_metrics::<GroovyParser>(
1651            "class Printer {
1652                void print() {
1653                    println 'hello'
1654                }
1655                static void main(String[] args) {
1656                    def p = new Printer()
1657                    p.print()
1658                    def doubler = { x -> x * 2 }
1659                    int r = doubler(21)
1660                }
1661            }",
1662            "foo.groovy",
1663            |metric| {
1664                // Two methods declared. The dekobon grammar parses
1665                // method bodies as `block`, distinct from `closure`, so
1666                // the explicit `doubler = { x -> x * 2 }` literal is the
1667                // only closure here (unlike the prior amaanq grammar
1668                // which mis-parsed each method body as a `closure`
1669                // node too).
1670                assert_eq!(metric.nom.functions_sum(), 2);
1671                assert_eq!(metric.nom.closures_sum(), 1);
1672            },
1673        );
1674    }
1675
1676    #[cfg(feature = "groovy")]
1677    #[test]
1678    fn groovy_nom_function_definition() {
1679        // `def foo() {}` at top level uses `function_definition`, not
1680        // `method_declaration`. Nom must count it as a function.
1681        check_metrics::<GroovyParser>(
1682            "def greet(name) {
1683                println(name)
1684            }
1685            greet('world')",
1686            "foo.groovy",
1687            |metric| {
1688                assert_eq!(metric.nom.functions_sum(), 1);
1689            },
1690        );
1691    }
1692
1693    #[cfg(feature = "perl")]
1694    #[test]
1695    fn perl_nom() {
1696        check_metrics::<PerlParser>(
1697            "sub a { 1 }
1698             sub b { 2 }
1699             my $c = sub { 3 };
1700             sub outer {
1701                 my $inner = sub { 4 };
1702                 return $inner;
1703             }",
1704            "foo.pl",
1705            |metric| {
1706                insta::assert_json_snapshot!(
1707                    metric.nom,
1708                    @r#"
1709                {
1710                  "functions": 3,
1711                  "closures": 2,
1712                  "functions_average": 0.5,
1713                  "closures_average": 0.3333333333333333,
1714                  "total": 5,
1715                  "average": 0.8333333333333334,
1716                  "functions_min": 0,
1717                  "functions_max": 1,
1718                  "closures_min": 0,
1719                  "closures_max": 1
1720                }
1721                "#
1722                );
1723            },
1724        );
1725    }
1726
1727    #[cfg(feature = "typescript")]
1728    #[test]
1729    fn tsx_named_and_arrow_functions() {
1730        check_metrics::<TsxParser>(
1731            "function greet(name: string): string {
1732                 return `Hello, ${name}`;
1733             }
1734             const add = (a: number, b: number) => a + b;
1735             const log = () => { console.log('done'); };",
1736            "foo.tsx",
1737            |metric| {
1738                insta::assert_json_snapshot!(
1739                    metric.nom,
1740                    @r#"
1741                {
1742                  "functions": 3,
1743                  "closures": 0,
1744                  "functions_average": 0.75,
1745                  "closures_average": 0.0,
1746                  "total": 3,
1747                  "average": 0.75,
1748                  "functions_min": 0,
1749                  "functions_max": 1,
1750                  "closures_min": 0,
1751                  "closures_max": 0
1752                }
1753                "#
1754                );
1755            },
1756        );
1757    }
1758
1759    #[cfg(feature = "typescript")]
1760    #[test]
1761    fn typescript_named_arrow_and_class_methods() {
1762        check_metrics::<TypescriptParser>(
1763            "function compute(x: number): number {
1764                 return x * 2;
1765             }
1766             const double = (n: number): number => n * 2;
1767             class Calculator {
1768                 add(a: number, b: number): number {
1769                     return a + b;
1770                 }
1771             }",
1772            "foo.ts",
1773            |metric| {
1774                insta::assert_json_snapshot!(
1775                    metric.nom,
1776                    @r#"
1777                {
1778                  "functions": 3,
1779                  "closures": 0,
1780                  "functions_average": 0.6,
1781                  "closures_average": 0.0,
1782                  "total": 3,
1783                  "average": 0.6,
1784                  "functions_min": 0,
1785                  "functions_max": 1,
1786                  "closures_min": 0,
1787                  "closures_max": 0
1788                }
1789                "#
1790                );
1791            },
1792        );
1793    }
1794
1795    #[cfg(feature = "mozjs")]
1796    #[test]
1797    fn mozjs_nom() {
1798        check_metrics::<MozjsParser>(
1799            "function f(a, b) {
1800                 function foo(a) {
1801                     return a;
1802                 }
1803                 var bar = (function () {
1804                     var counter = 0;
1805                     return function () {
1806                         counter += 1;
1807                         return counter
1808                     }
1809                 })();
1810                 return bar(foo(a), a);
1811             }",
1812            "foo.js",
1813            |metric| {
1814                insta::assert_json_snapshot!(
1815                    metric.nom,
1816                    @r#"
1817                {
1818                  "functions": 2,
1819                  "closures": 2,
1820                  "functions_average": 0.4,
1821                  "closures_average": 0.4,
1822                  "total": 4,
1823                  "average": 0.8,
1824                  "functions_min": 0,
1825                  "functions_max": 1,
1826                  "closures_min": 0,
1827                  "closures_max": 1
1828                }
1829                "#
1830                );
1831            },
1832        );
1833    }
1834
1835    #[cfg(feature = "mozjs")]
1836    #[test]
1837    fn mozjs_arrow_and_method() {
1838        check_metrics::<MozjsParser>(
1839            "let add = (a, b) => a + b;
1840             class Counter {
1841                 increment() {
1842                     this.count++;
1843                 }
1844             }",
1845            "foo.js",
1846            |metric| {
1847                insta::assert_json_snapshot!(
1848                    metric.nom,
1849                    @r#"
1850                {
1851                  "functions": 2,
1852                  "closures": 0,
1853                  "functions_average": 0.5,
1854                  "closures_average": 0.0,
1855                  "total": 2,
1856                  "average": 0.5,
1857                  "functions_min": 0,
1858                  "functions_max": 1,
1859                  "closures_min": 0,
1860                  "closures_max": 0
1861                }
1862                "#
1863                );
1864            },
1865        );
1866    }
1867
1868    #[cfg(feature = "kotlin")]
1869    #[test]
1870    fn kotlin_nom_class_with_methods() {
1871        check_metrics::<KotlinParser>(
1872            "class Calculator {
1873                fun add(a: Int, b: Int): Int {
1874                    return a + b
1875                }
1876                fun subtract(a: Int, b: Int): Int {
1877                    return a - b
1878                }
1879            }",
1880            "foo.kt",
1881            |metric| {
1882                insta::assert_json_snapshot!(
1883                    metric.nom,
1884                    @r#"
1885                {
1886                  "functions": 2,
1887                  "closures": 0,
1888                  "functions_average": 0.5,
1889                  "closures_average": 0.0,
1890                  "total": 2,
1891                  "average": 0.5,
1892                  "functions_min": 0,
1893                  "functions_max": 1,
1894                  "closures_min": 0,
1895                  "closures_max": 0
1896                }
1897                "#
1898                );
1899            },
1900        );
1901    }
1902
1903    #[cfg(feature = "lua")]
1904    #[test]
1905    fn lua_nom() {
1906        check_metrics::<LuaParser>(
1907            "function greet(name)
1908  return \"hello \" .. name
1909end
1910
1911local add = function(a, b)
1912  return a + b
1913end
1914
1915local function outer()
1916  local inner = function()
1917    return 42
1918  end
1919  return inner()
1920end",
1921            "foo.lua",
1922            |metric| {
1923                // 2 named functions (greet, outer), 2 closures (add, inner)
1924                insta::assert_json_snapshot!(metric.nom, @r#"
1925                {
1926                  "functions": 2,
1927                  "closures": 2,
1928                  "functions_average": 0.4,
1929                  "closures_average": 0.4,
1930                  "total": 4,
1931                  "average": 0.8,
1932                  "functions_min": 0,
1933                  "functions_max": 1,
1934                  "closures_min": 0,
1935                  "closures_max": 1
1936                }
1937                "#);
1938            },
1939        );
1940    }
1941
1942    #[cfg(feature = "bash")]
1943    #[test]
1944    fn bash_nom() {
1945        check_metrics::<BashParser>(
1946            "#!/bin/bash
1947foo() {
1948    echo 'hello'
1949}
1950bar() {
1951    echo 'world'
1952}
1953foo
1954bar",
1955            "foo.sh",
1956            |metric| {
1957                insta::assert_json_snapshot!(
1958                    metric.nom,
1959                    @r#"
1960                {
1961                  "functions": 2,
1962                  "closures": 0,
1963                  "functions_average": 0.6666666666666666,
1964                  "closures_average": 0.0,
1965                  "total": 2,
1966                  "average": 0.6666666666666666,
1967                  "functions_min": 0,
1968                  "functions_max": 1,
1969                  "closures_min": 0,
1970                  "closures_max": 0
1971                }
1972                "#
1973                );
1974            },
1975        );
1976    }
1977
1978    #[cfg(feature = "tcl")]
1979    #[test]
1980    fn tcl_nom() {
1981        check_metrics::<TclParser>(
1982            "proc foo {a} { puts $a }
1983proc bar {x y} { puts $x }
1984foo 1
1985bar 2 3",
1986            "foo.tcl",
1987            |metric| {
1988                assert_eq!(metric.nom.functions_sum(), 2);
1989                assert_eq!(metric.nom.closures_sum(), 0);
1990                insta::assert_json_snapshot!(metric.nom);
1991            },
1992        );
1993    }
1994
1995    #[cfg(feature = "tcl")]
1996    #[test]
1997    fn tcl_nested_nom() {
1998        check_metrics::<TclParser>(
1999            "proc outer {a} {
2000    proc inner {x} { puts $x }
2001    inner $a
2002}",
2003            "foo.tcl",
2004            |metric| {
2005                assert_eq!(metric.nom.functions_sum(), 2);
2006                assert_eq!(metric.nom.closures_sum(), 0);
2007                insta::assert_json_snapshot!(metric.nom);
2008            },
2009        );
2010    }
2011
2012    #[cfg(feature = "typescript")]
2013    #[test]
2014    fn typescript_class_methods() {
2015        check_metrics::<TypescriptParser>(
2016            "class Calc {
2017             add(a: number, b: number): number { return a + b; }
2018             sub(a: number, b: number): number { return a - b; }
2019         }",
2020            "foo.ts",
2021            |metric| {
2022                assert_eq!(metric.nom.functions_sum(), 2);
2023                assert_eq!(metric.nom.closures_sum(), 0);
2024                insta::assert_json_snapshot!(metric.nom);
2025            },
2026        );
2027    }
2028
2029    #[cfg(feature = "typescript")]
2030    #[test]
2031    fn typescript_arrow_and_function() {
2032        check_metrics::<TypescriptParser>(
2033            "function f(): number { return 1; }
2034         const g = (): number => 2;
2035         const h = (x: number): number => x * 2;",
2036            "foo.ts",
2037            |metric| {
2038                assert_eq!(metric.nom.functions_sum(), 3);
2039                assert_eq!(metric.nom.closures_sum(), 0);
2040                insta::assert_json_snapshot!(metric.nom);
2041            },
2042        );
2043    }
2044
2045    #[cfg(feature = "typescript")]
2046    #[test]
2047    fn tsx_class_methods() {
2048        check_metrics::<TsxParser>(
2049            "class Calc {
2050             add(a: number, b: number): number { return a + b; }
2051             sub(a: number, b: number): number { return a - b; }
2052         }",
2053            "foo.tsx",
2054            |metric| {
2055                assert_eq!(metric.nom.functions_sum(), 2);
2056                assert_eq!(metric.nom.closures_sum(), 0);
2057                insta::assert_json_snapshot!(metric.nom);
2058            },
2059        );
2060    }
2061
2062    #[cfg(feature = "typescript")]
2063    #[test]
2064    fn tsx_arrow_and_function() {
2065        check_metrics::<TsxParser>(
2066            "function f(): number { return 1; }
2067         const g = (): number => 2;
2068         const h = (x: number): number => x * 2;",
2069            "foo.tsx",
2070            |metric| {
2071                assert_eq!(metric.nom.functions_sum(), 3);
2072                assert_eq!(metric.nom.closures_sum(), 0);
2073                insta::assert_json_snapshot!(metric.nom);
2074            },
2075        );
2076    }
2077
2078    #[cfg(feature = "bash")]
2079    #[test]
2080    fn bash_multiple_functions_nom() {
2081        check_metrics::<BashParser>(
2082            "#!/bin/bash
2083f() {
2084    echo hello
2085}
2086g() {
2087    echo world
2088}",
2089            "foo.sh",
2090            |metric| {
2091                assert_eq!(metric.nom.functions_sum(), 2);
2092                assert_eq!(metric.nom.closures_sum(), 0);
2093                insta::assert_json_snapshot!(metric.nom);
2094            },
2095        );
2096    }
2097
2098    #[cfg(feature = "bash")]
2099    #[test]
2100    fn bash_nested_functions_nom() {
2101        check_metrics::<BashParser>(
2102            "#!/bin/bash
2103outer() {
2104    inner() {
2105        echo inner
2106    }
2107    inner
2108}",
2109            "foo.sh",
2110            |metric| {
2111                assert_eq!(metric.nom.functions_sum(), 2);
2112                assert_eq!(metric.nom.closures_sum(), 0);
2113                insta::assert_json_snapshot!(metric.nom);
2114            },
2115        );
2116    }
2117
2118    #[cfg(feature = "mozjs")]
2119    #[test]
2120    fn mozjs_nested_function_nom() {
2121        check_metrics::<MozjsParser>(
2122            "function outer() {
2123             function inner() {
2124                 return 1;
2125             }
2126             return inner();
2127         }",
2128            "foo.js",
2129            |metric| {
2130                assert_eq!(metric.nom.functions_sum(), 2);
2131                assert_eq!(metric.nom.closures_sum(), 0);
2132                insta::assert_json_snapshot!(metric.nom);
2133            },
2134        );
2135    }
2136
2137    #[cfg(feature = "mozjs")]
2138    #[test]
2139    fn mozjs_class_methods_nom() {
2140        check_metrics::<MozjsParser>(
2141            "class Calc {
2142             add(a, b) { return a + b; }
2143             sub(a, b) { return a - b; }
2144         }",
2145            "foo.js",
2146            |metric| {
2147                assert_eq!(metric.nom.functions_sum(), 2);
2148                assert_eq!(metric.nom.closures_sum(), 0);
2149                insta::assert_json_snapshot!(metric.nom);
2150            },
2151        );
2152    }
2153
2154    #[cfg(feature = "mozjs")]
2155    #[test]
2156    fn mozjs_iife_nom() {
2157        check_metrics::<MozjsParser>(
2158            "(function() {
2159             var x = 1;
2160             return x;
2161         })();",
2162            "foo.js",
2163            |metric| {
2164                assert_eq!(metric.nom.functions_sum(), 0);
2165                assert_eq!(metric.nom.closures_sum(), 1);
2166                insta::assert_json_snapshot!(metric.nom);
2167            },
2168        );
2169    }
2170
2171    #[cfg(feature = "kotlin")]
2172    #[test]
2173    fn kotlin_class_methods_nom() {
2174        check_metrics::<KotlinParser>(
2175            "class Calc {
2176             fun add(a: Int, b: Int): Int = a + b
2177             fun sub(a: Int, b: Int): Int = a - b
2178         }",
2179            "foo.kt",
2180            |metric| {
2181                assert_eq!(metric.nom.functions_sum(), 2);
2182                assert_eq!(metric.nom.closures_sum(), 0);
2183                insta::assert_json_snapshot!(metric.nom);
2184            },
2185        );
2186    }
2187
2188    #[cfg(feature = "kotlin")]
2189    #[test]
2190    fn kotlin_lambda_nom() {
2191        check_metrics::<KotlinParser>(
2192            "fun f(list: List<Int>): Int {
2193             val double = { x: Int -> x * 2 }
2194             return list.sumOf(double)
2195         }",
2196            "foo.kt",
2197            |metric| {
2198                assert_eq!(metric.nom.functions_sum(), 1);
2199                assert_eq!(metric.nom.closures_sum(), 1);
2200                insta::assert_json_snapshot!(metric.nom);
2201            },
2202        );
2203    }
2204
2205    #[cfg(feature = "php")]
2206    #[test]
2207    fn php_nom() {
2208        // Top-level function + 2 methods inside a class + 1 anonymous +
2209        // 1 arrow = 3 functions, 2 closures.
2210        check_metrics::<PhpParser>(
2211            "<?php
2212            function top(): void {}
2213            class A {
2214                public function m1(): void {}
2215                public function m2(): int {
2216                    $f = function () { return 1; };
2217                    $g = fn () => 2;
2218                    return $f() + $g();
2219                }
2220            }",
2221            "foo.php",
2222            |metric| {
2223                insta::assert_json_snapshot!(
2224                    metric.nom,
2225                    @r#"
2226                {
2227                  "functions": 3,
2228                  "closures": 2,
2229                  "functions_average": 0.42857142857142855,
2230                  "closures_average": 0.2857142857142857,
2231                  "total": 5,
2232                  "average": 0.7142857142857143,
2233                  "functions_min": 0,
2234                  "functions_max": 1,
2235                  "closures_min": 0,
2236                  "closures_max": 1
2237                }
2238                "#
2239                );
2240            },
2241        );
2242    }
2243
2244    #[cfg(feature = "php")]
2245    #[test]
2246    fn php_nom_anonymous_class() {
2247        // Methods inside `new class { … }` count toward the closure-style
2248        // space mechanism: anonymous_class is its own space and its
2249        // method_declaration children are counted as functions.
2250        check_metrics::<PhpParser>(
2251            "<?php
2252            function f(): object {
2253                return new class {
2254                    public function inner(): int { return 1; }
2255                };
2256            }",
2257            "foo.php",
2258            |metric| {
2259                insta::assert_json_snapshot!(
2260                    metric.nom,
2261                    @r#"
2262                {
2263                  "functions": 2,
2264                  "closures": 0,
2265                  "functions_average": 0.5,
2266                  "closures_average": 0.0,
2267                  "total": 2,
2268                  "average": 0.5,
2269                  "functions_min": 0,
2270                  "functions_max": 1,
2271                  "closures_min": 0,
2272                  "closures_max": 0
2273                }
2274                "#
2275                );
2276            },
2277        );
2278    }
2279
2280    #[cfg(feature = "php")]
2281    #[test]
2282    fn php_function_after_leading_backslash_group_use_is_counted() {
2283        // tree-sitter-php 0.24.2 parsed `use \Foo\{…};` as ERROR and the
2284        // recovery swallowed the function after it, so `u` scored nom 0.
2285        // 0.25.1 accepts the leading backslash; the function is counted.
2286        check_metrics::<PhpParser>(
2287            "<?php\nuse \\Foo\\{Bar, Baz};\nfunction u() { return 1; }\n",
2288            "foo.php",
2289            |metric| {
2290                assert_eq!(metric.nom.functions_sum(), 1);
2291            },
2292        );
2293    }
2294
2295    // Elixir's `def`/`defp`/`defmacro`/`defmacrop` declarations surface
2296    // as `Call` nodes whose target is an `Identifier`, so the byte-less
2297    // `is_func` cannot see them. Since #696, `Nom::compute` consults the
2298    // source-aware `is_func_with_code`, so the three named declarations
2299    // (`public_fn`, `private_fn`, `with_anon`) now count as FUNCTIONS and
2300    // the two `fn x -> … end` literals count as CLOSURES — the same split
2301    // every other language already produced. `functions_sum` was pinned at
2302    // 0 before the fix (the bug this test now guards against regressing).
2303    #[cfg(feature = "elixir")]
2304    #[test]
2305    fn elixir_nom_counts_def_as_functions_and_fn_as_closures() {
2306        check_metrics::<ElixirParser>(
2307            "defmodule Foo do\n  def public_fn(x), do: x + 1\n  defp private_fn(x), do: x - 1\n  def with_anon do\n    inc = fn x -> x + 1 end\n    dec = fn x -> x - 1 end\n    {inc, dec}\n  end\nend\n",
2308            "foo.ex",
2309            |metric| {
2310                assert_eq!(metric.nom.functions_sum(), 3);
2311                assert_eq!(metric.nom.closures_sum(), 2);
2312                assert_eq!(metric.nom.total(), 5);
2313            },
2314        );
2315    }
2316
2317    #[test]
2318    fn stats_display_commas_between_all_fields() {
2319        let stats = Stats {
2320            functions: 0,
2321            closures: 0,
2322            functions_sum: 3,
2323            closures_sum: 1,
2324            functions_min: 0,
2325            functions_max: 2,
2326            closures_min: 0,
2327            closures_max: 1,
2328            space_count: 2,
2329        };
2330        let formatted = format!("{stats}");
2331
2332        // Every adjacent pair of labels must appear with ", " between the
2333        // previous field's value and the next label.
2334        let expected_fragments = [
2335            "functions: 3, closures: 1",
2336            "closures: 1, functions_average:",
2337            "functions_average: 1.5, closures_average:",
2338            "closures_average: 0.5, total:",
2339            "total: 4, average:",
2340            "average: 2, functions_min:",
2341            "functions_min: 0, functions_max:",
2342            "functions_max: 2, closures_min:",
2343            "closures_min: 0, closures_max:",
2344        ];
2345
2346        for fragment in expected_fragments {
2347            assert!(
2348                formatted.contains(fragment),
2349                "missing fragment {fragment:?} in: {formatted}"
2350            );
2351        }
2352    }
2353
2354    #[cfg(feature = "ruby")]
2355    #[test]
2356    fn ruby_nom() {
2357        // expected: total = 4 (2 methods `add`/`mul` + 1 singleton
2358        // method `self.factory` + 1 block argument to `each`).
2359        // `functions` counts only the named `Method` / `SingletonMethod`
2360        // forms (3); `closures` counts `Block` / `DoBlock` / `Lambda`
2361        // (1).
2362        check_metrics::<RubyParser>(
2363            "class C\n  def add(a, b)\n    a + b\n  end\n  def mul(a, b)\n    a * b\n  end\n  def self.factory\n    new\n  end\nend\n\n[1, 2, 3].each { |x| puts x }\n",
2364            "foo.rb",
2365            |metric| {
2366                assert_eq!(metric.nom.functions_sum(), 3);
2367                assert_eq!(metric.nom.closures_sum(), 1);
2368                assert_eq!(metric.nom.total(), 4);
2369            },
2370        );
2371    }
2372
2373    #[cfg(feature = "ruby")]
2374    #[test]
2375    fn ruby_stabby_lambda_single_closure() {
2376        // A stabby lambda `->(z) { … }` parses as a `Lambda` node that
2377        // contains a `Block` for its body. `is_closure` must count the
2378        // pair as ONE closure, not two (#465). Revert-verified: counting
2379        // the inner `Block` again yields closures_sum == 2.0.
2380        check_metrics::<RubyParser>("f = ->(z) { z + 1 }\n", "stabby.rb", |metric| {
2381            assert_eq!(metric.nom.functions_sum(), 0);
2382            assert_eq!(metric.nom.closures_sum(), 1);
2383        });
2384    }
2385
2386    #[cfg(feature = "ruby")]
2387    #[test]
2388    fn ruby_stabby_lambda_multi_statement_single_closure() {
2389        // A multi-statement body does not change the structure: still one
2390        // `Lambda` wrapping one `Block`, so still one closure.
2391        check_metrics::<RubyParser>(
2392            "f = ->(z) {\n  y = z + 1\n  y * 2\n}\n",
2393            "stabby_multi.rb",
2394            |metric| {
2395                assert_eq!(metric.nom.closures_sum(), 1);
2396            },
2397        );
2398    }
2399
2400    #[cfg(feature = "ruby")]
2401    #[test]
2402    fn ruby_stabby_lambda_do_block_single_closure() {
2403        // The `do … end` body form of a stabby lambda parses as a `Lambda`
2404        // wrapping a `DoBlock`; both must collapse to one closure.
2405        check_metrics::<RubyParser>("f = ->(z) do\n  z + 1\nend\n", "stabby_do.rb", |metric| {
2406            assert_eq!(metric.nom.closures_sum(), 1);
2407        });
2408    }
2409
2410    #[cfg(feature = "ruby")]
2411    #[test]
2412    fn ruby_keyword_lambda_single_closure() {
2413        // The keyword forms `lambda { }` / `proc { }` parse as a `Call`
2414        // carrying a `Block` argument (the parent is a `Call`, not a
2415        // `Lambda`), so they must still count exactly one closure. Guards
2416        // against the #465 fix regressing the keyword form to zero.
2417        check_metrics::<RubyParser>(
2418            "g = lambda { |z| z + 1 }\nh = proc { |z| z + 1 }\n",
2419            "keyword.rb",
2420            |metric| {
2421                assert_eq!(metric.nom.closures_sum(), 2);
2422            },
2423        );
2424    }
2425
2426    /// iRules counts `when` event handlers and `proc` definitions as
2427    /// functions; the language has no closures. A file with two handlers
2428    /// and one proc reports three functions, zero closures. Confirms the
2429    /// handlers-as-functions decision end to end. (`try`'s `on` / `trap`
2430    /// handlers are branch points, not functions — issue #1266.)
2431    #[cfg(feature = "irules")]
2432    #[test]
2433    fn irules_nom_handlers_and_procs() {
2434        check_metrics::<IrulesParser>(
2435            "when CLIENT_ACCEPTED {
2436    log local0. \"connected\"
2437}
2438when HTTP_REQUEST {
2439    log local0. [HTTP::uri]
2440}
2441proc helper { x } {
2442    return $x
2443}
2444",
2445            "foo.irule",
2446            |metric| {
2447                assert_eq!(metric.nom.functions_sum(), 3);
2448                assert_eq!(metric.nom.closures_sum(), 0);
2449                assert_eq!(metric.nom.total(), 3);
2450            },
2451        );
2452    }
2453
2454    /// iRules `try` handlers (`on error` / `trap`) are error-handler
2455    /// clauses, not functions: a proc containing both must report exactly
2456    /// one function (issue #1266). Before the fix each handler's dedicated
2457    /// `on_handler` / `trap_handler` node was classified as a function
2458    /// space, so this fixture reported three.
2459    #[cfg(feature = "irules")]
2460    #[test]
2461    fn irules_try_handlers_are_not_functions() {
2462        check_metrics::<IrulesParser>(
2463            "proc f {} {
2464    try {
2465        risky
2466    } on error {msg} {
2467        puts $msg
2468    } trap {POSIX} {msg} {
2469        puts $msg
2470    }
2471}
2472",
2473            "foo.irule",
2474            |metric| {
2475                assert_eq!(metric.nom.functions_sum(), 1);
2476                assert_eq!(metric.nom.closures_sum(), 0);
2477                assert_eq!(metric.nom.total(), 1);
2478            },
2479        );
2480    }
2481
2482    /// Objective-C `@implementation` with two `method_definition`s and a
2483    /// `block_literal`: the two methods are functions, the block is a
2484    /// closure (it does not open its own space), so functions = 2,
2485    /// closures = 1, total = 3.
2486    #[cfg(feature = "objc")]
2487    #[test]
2488    fn objc_nom() {
2489        check_metrics::<ObjcParser>(
2490            "@implementation Foo
2491- (void)one {
2492    void (^blk)(void) = ^{
2493        [self two];
2494    };
2495    blk();
2496}
2497- (void)two {
2498    [self one];
2499}
2500@end
2501",
2502            "foo.m",
2503            |metric| {
2504                assert_eq!(metric.nom.functions_sum(), 2);
2505                assert_eq!(metric.nom.closures_sum(), 1);
2506                assert_eq!(metric.nom.total(), 3);
2507                insta::assert_json_snapshot!(metric.nom, @r#"
2508                {
2509                  "functions": 2,
2510                  "closures": 1,
2511                  "functions_average": 0.5,
2512                  "closures_average": 0.25,
2513                  "total": 3,
2514                  "average": 0.75,
2515                  "functions_min": 0,
2516                  "functions_max": 1,
2517                  "closures_min": 0,
2518                  "closures_max": 1
2519                }
2520                "#);
2521            },
2522        );
2523    }
2524}