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