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