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 serde::Serialize;
21use serde::ser::{SerializeStruct, Serializer};
22use std::fmt;
23
24use crate::checker::Checker;
25use crate::macros::implement_metric_trait;
26
27use crate::*;
28
29/// The `Nom` metric suite.
30#[derive(Clone, Debug)]
31pub struct Stats {
32    functions: usize,
33    closures: usize,
34    functions_sum: usize,
35    closures_sum: usize,
36    functions_min: usize,
37    functions_max: usize,
38    closures_min: usize,
39    closures_max: usize,
40    space_count: usize,
41}
42
43impl Default for Stats {
44    fn default() -> Self {
45        Self {
46            functions: 0,
47            closures: 0,
48            functions_sum: 0,
49            closures_sum: 0,
50            functions_min: usize::MAX,
51            functions_max: 0,
52            closures_min: usize::MAX,
53            closures_max: 0,
54            space_count: 1,
55        }
56    }
57}
58
59impl Serialize for Stats {
60    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
61    where
62        S: Serializer,
63    {
64        let mut st = serializer.serialize_struct("nom", 10)?;
65        st.serialize_field("functions", &self.functions_sum())?;
66        st.serialize_field("closures", &self.closures_sum())?;
67        st.serialize_field("functions_average", &self.functions_average())?;
68        st.serialize_field("closures_average", &self.closures_average())?;
69        st.serialize_field("total", &self.total())?;
70        st.serialize_field("average", &self.average())?;
71        st.serialize_field("functions_min", &self.functions_min())?;
72        st.serialize_field("functions_max", &self.functions_max())?;
73        st.serialize_field("closures_min", &self.closures_min())?;
74        st.serialize_field("closures_max", &self.closures_max())?;
75        st.end()
76    }
77}
78
79impl fmt::Display for Stats {
80    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
81        write!(
82            f,
83            "functions: {}, \
84             closures: {}, \
85             functions_average: {}, \
86             closures_average: {}, \
87             total: {}, \
88             average: {}, \
89             functions_min: {}, \
90             functions_max: {}, \
91             closures_min: {}, \
92             closures_max: {}",
93            self.functions_sum(),
94            self.closures_sum(),
95            self.functions_average(),
96            self.closures_average(),
97            self.total(),
98            self.average(),
99            self.functions_min(),
100            self.functions_max(),
101            self.closures_min(),
102            self.closures_max(),
103        )
104    }
105}
106
107impl Stats {
108    /// Merges a second `Nom` metric suite into the first one
109    pub fn merge(&mut self, other: &Stats) {
110        self.functions_min = self.functions_min.min(other.functions_min);
111        self.functions_max = self.functions_max.max(other.functions_max);
112        self.closures_min = self.closures_min.min(other.closures_min);
113        self.closures_max = self.closures_max.max(other.closures_max);
114        self.functions_sum += other.functions_sum;
115        self.closures_sum += other.closures_sum;
116        self.space_count += other.space_count;
117    }
118
119    /// Counts the number of function definitions in a scope
120    #[inline]
121    #[must_use]
122    pub fn functions(&self) -> f64 {
123        // Only function definitions are considered, not general declarations
124        self.functions as f64
125    }
126
127    /// Counts the number of closures in a scope
128    #[inline]
129    #[must_use]
130    pub fn closures(&self) -> f64 {
131        self.closures as f64
132    }
133
134    /// Return the sum metric for functions
135    #[inline]
136    #[must_use]
137    pub fn functions_sum(&self) -> f64 {
138        // Only function definitions are considered, not general declarations
139        self.functions_sum as f64
140    }
141
142    /// Return the sum metric for closures
143    #[inline]
144    #[must_use]
145    pub fn closures_sum(&self) -> f64 {
146        self.closures_sum as f64
147    }
148
149    /// Returns the average number of function definitions over all spaces
150    #[inline]
151    #[must_use]
152    pub fn functions_average(&self) -> f64 {
153        self.functions_sum() / self.space_count as f64
154    }
155
156    /// Returns the average number of closures over all spaces
157    #[inline]
158    #[must_use]
159    pub fn closures_average(&self) -> f64 {
160        self.closures_sum() / self.space_count as f64
161    }
162
163    /// Returns the average number of function definitions and closures over all spaces
164    #[inline]
165    #[must_use]
166    pub fn average(&self) -> f64 {
167        self.total() / self.space_count as f64
168    }
169
170    /// Counts the number of function definitions in a scope.
171    ///
172    /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
173    /// into `functions_min` to `0.0`, so a never-observed space
174    /// serializes to a meaningful number rather than `1.8446744e19`.
175    #[inline]
176    #[must_use]
177    pub fn functions_min(&self) -> f64 {
178        // Only function definitions are considered, not general declarations
179        if self.functions_min == usize::MAX {
180            0.0
181        } else {
182            self.functions_min as f64
183        }
184    }
185
186    /// Counts the number of closures in a scope.
187    ///
188    /// Same `usize::MAX` sentinel collapse as `functions_min`.
189    #[inline]
190    #[must_use]
191    pub fn closures_min(&self) -> f64 {
192        if self.closures_min == usize::MAX {
193            0.0
194        } else {
195            self.closures_min as f64
196        }
197    }
198    /// Counts the number of function definitions in a scope
199    #[inline]
200    #[must_use]
201    pub fn functions_max(&self) -> f64 {
202        // Only function definitions are considered, not general declarations
203        self.functions_max as f64
204    }
205
206    /// Counts the number of closures in a scope
207    #[inline]
208    #[must_use]
209    pub fn closures_max(&self) -> f64 {
210        self.closures_max as f64
211    }
212    /// Returns the total number of function definitions and
213    /// closures in a scope
214    #[inline]
215    #[must_use]
216    pub fn total(&self) -> f64 {
217        self.functions_sum() + self.closures_sum()
218    }
219    #[inline]
220    pub(crate) fn compute_sum(&mut self) {
221        self.functions_sum += self.functions;
222        self.closures_sum += self.closures;
223    }
224    #[inline]
225    pub(crate) fn compute_minmax(&mut self) {
226        self.functions_min = self.functions_min.min(self.functions);
227        self.functions_max = self.functions_max.max(self.functions);
228        self.closures_min = self.closures_min.min(self.closures);
229        self.closures_max = self.closures_max.max(self.closures);
230        self.compute_sum();
231    }
232}
233
234#[doc(hidden)]
235/// Per-language counting of methods (functions + closures).
236pub trait Nom
237where
238    Self: Checker,
239{
240    /// Walk `node` and update `stats` with this metric for the language
241    /// implementing the trait.
242    fn compute(node: &Node, stats: &mut Stats) {
243        if Self::is_func(node) {
244            stats.functions += 1;
245            return;
246        }
247        if Self::is_closure(node) {
248            stats.closures += 1;
249        }
250    }
251}
252
253implement_metric_trait!(
254    [Nom],
255    PythonCode,
256    MozjsCode,
257    JavascriptCode,
258    TypescriptCode,
259    TsxCode,
260    CppCode,
261    RustCode,
262    PreprocCode,
263    CcommentCode,
264    JavaCode,
265    KotlinCode,
266    GoCode,
267    PerlCode,
268    BashCode,
269    LuaCode,
270    TclCode,
271    PhpCode,
272    CsharpCode,
273    ElixirCode,
274    RubyCode,
275    GroovyCode
276);
277
278#[cfg(test)]
279#[allow(
280    clippy::float_cmp,
281    clippy::cast_precision_loss,
282    clippy::cast_possible_truncation,
283    clippy::cast_sign_loss,
284    clippy::similar_names,
285    clippy::doc_markdown,
286    clippy::needless_raw_string_hashes,
287    clippy::too_many_lines
288)]
289mod tests {
290    use crate::tools::check_metrics;
291
292    use super::*;
293
294    /// Regression for #227: a `Stats::default()` that never sees an
295    /// observation must not leak the `usize::MAX` sentinel for
296    /// `functions_min` or `closures_min`. Both getters collapse the
297    /// sentinel to `0.0` so JSON never emits `1.8446744e19`.
298    #[test]
299    fn nom_empty_file_min_is_zero() {
300        let stats = Stats::default();
301        assert_eq!(stats.functions_min(), 0.0);
302        assert_eq!(stats.closures_min(), 0.0);
303    }
304
305    #[test]
306    fn python_nom() {
307        check_metrics::<PythonParser>(
308            "def a():
309                 pass
310             def b():
311                 pass
312             def c():
313                 pass
314             x = lambda a : a + 42",
315            "foo.py",
316            |metric| {
317                // Number of spaces = 4
318                insta::assert_json_snapshot!(
319                    metric.nom,
320                    @r###"
321                    {
322                      "functions": 3.0,
323                      "closures": 1.0,
324                      "functions_average": 0.75,
325                      "closures_average": 0.25,
326                      "total": 4.0,
327                      "average": 1.0,
328                      "functions_min": 0.0,
329                      "functions_max": 1.0,
330                      "closures_min": 0.0,
331                      "closures_max": 1.0
332                    }"###
333                );
334            },
335        );
336    }
337
338    #[test]
339    fn rust_nom() {
340        check_metrics::<RustParser>(
341            "mod A { fn foo() {}}
342             mod B { fn foo() {}}
343             let closure = |i: i32| -> i32 { i + 42 };",
344            "foo.rs",
345            |metric| {
346                // Number of spaces = 4
347                insta::assert_json_snapshot!(
348                    metric.nom,
349                    @r###"
350                    {
351                      "functions": 2.0,
352                      "closures": 1.0,
353                      "functions_average": 0.5,
354                      "closures_average": 0.25,
355                      "total": 3.0,
356                      "average": 0.75,
357                      "functions_min": 0.0,
358                      "functions_max": 1.0,
359                      "closures_min": 0.0,
360                      "closures_max": 1.0
361                    }"###
362                );
363            },
364        );
365    }
366
367    #[test]
368    fn c_nom() {
369        check_metrics::<CppParser>(
370            "int foo();
371
372             int foo() {
373                 return 0;
374             }",
375            "foo.c",
376            |metric| {
377                // Number of spaces = 2
378                insta::assert_json_snapshot!(
379                    metric.nom,
380                    @r###"
381                    {
382                      "functions": 1.0,
383                      "closures": 0.0,
384                      "functions_average": 0.5,
385                      "closures_average": 0.0,
386                      "total": 1.0,
387                      "average": 0.5,
388                      "functions_min": 0.0,
389                      "functions_max": 1.0,
390                      "closures_min": 0.0,
391                      "closures_max": 0.0
392                    }"###
393                );
394            },
395        );
396    }
397
398    #[test]
399    fn cpp_nom() {
400        check_metrics::<CppParser>(
401            "struct A {
402                 void foo(int) {}
403                 void foo(double) {}
404             };
405             int b = [](int x) -> int { return x + 42; };",
406            "foo.cpp",
407            |metric| {
408                // Number of spaces = 4
409                insta::assert_json_snapshot!(
410                    metric.nom,
411                    @r###"
412                    {
413                      "functions": 2.0,
414                      "closures": 1.0,
415                      "functions_average": 0.5,
416                      "closures_average": 0.25,
417                      "total": 3.0,
418                      "average": 0.75,
419                      "functions_min": 0.0,
420                      "functions_max": 1.0,
421                      "closures_min": 0.0,
422                      "closures_max": 1.0
423                    }"###
424                );
425            },
426        );
427    }
428
429    /// Free functions and member functions both surface as
430    /// `Cpp::FunctionDefinition` and count toward `functions`.  Member
431    /// functions are nested inside a struct/class space; the count is on
432    /// the function-definition node itself, not on the enclosing scope.
433    #[test]
434    fn cpp_free_and_member_functions() {
435        check_metrics::<CppParser>(
436            "int free_fn(int x) { return x; }
437             struct S {
438                 int member_fn(int x) { return x + 1; }
439             };",
440            "foo.cpp",
441            |metric| {
442                // 2 functions: `free_fn`, `S::member_fn`.
443                let s = &metric.nom;
444                assert_eq!(s.functions_sum(), 2.0);
445                assert_eq!(s.closures_sum(), 0.0);
446                assert_eq!(s.total(), 2.0);
447                insta::assert_json_snapshot!(metric.nom);
448            },
449        );
450    }
451
452    /// `static` member functions still surface as `Cpp::FunctionDefinition`
453    /// — the `static` keyword is a storage-class specifier, not a separate
454    /// node kind — so they are counted just like non-static members.
455    #[test]
456    fn cpp_static_member_function() {
457        check_metrics::<CppParser>(
458            "struct S {
459                 static int factory(int x) { return x; }
460                 int method(int x) { return x + 1; }
461             };",
462            "foo.cpp",
463            |metric| {
464                let s = &metric.nom;
465                assert_eq!(s.functions_sum(), 2.0);
466                assert_eq!(s.closures_sum(), 0.0);
467                insta::assert_json_snapshot!(metric.nom);
468            },
469        );
470    }
471
472    /// Constructor and destructor definitions surface as
473    /// `Cpp::FunctionDefinition` nodes with a `function_declarator` whose
474    /// identifier is the class name (ctor) or `~ClassName` (dtor).  Both
475    /// count as functions.
476    #[test]
477    fn cpp_constructor_and_destructor() {
478        check_metrics::<CppParser>(
479            "struct S {
480                 S() {}
481                 ~S() {}
482                 int method() { return 0; }
483             };",
484            "foo.cpp",
485            |metric| {
486                let s = &metric.nom;
487                // 3 functions: S(), ~S(), method.
488                assert_eq!(s.functions_sum(), 3.0);
489                assert_eq!(s.closures_sum(), 0.0);
490                insta::assert_json_snapshot!(metric.nom);
491            },
492        );
493    }
494
495    /// Operator overloads surface as `FunctionDefinition` whose declarator
496    /// has an `OperatorName` identifier (`operator+`, `operator==`).  Both
497    /// inline overloads count toward `functions`.
498    #[test]
499    fn cpp_operator_overloads() {
500        check_metrics::<CppParser>(
501            "struct V {
502                 int x;
503                 V operator+(const V& o) const { return V{x + o.x}; }
504                 bool operator==(const V& o) const { return x == o.x; }
505             };",
506            "foo.cpp",
507            |metric| {
508                let s = &metric.nom;
509                assert_eq!(s.functions_sum(), 2.0);
510                assert_eq!(s.closures_sum(), 0.0);
511                insta::assert_json_snapshot!(metric.nom);
512            },
513        );
514    }
515
516    /// Function-template definition counts as a single function — the
517    /// `template<>` prefix wraps a `FunctionDefinition` and does not
518    /// produce additional function-definition nodes.
519    #[test]
520    fn cpp_function_template() {
521        check_metrics::<CppParser>(
522            "template<typename T>
523             T identity(T x) { return x; }",
524            "foo.cpp",
525            |metric| {
526                let s = &metric.nom;
527                assert_eq!(s.functions_sum(), 1.0);
528                assert_eq!(s.closures_sum(), 0.0);
529                insta::assert_json_snapshot!(metric.nom);
530            },
531        );
532    }
533
534    /// Class-template member functions defined in-line each count as one
535    /// function.  The `template<>` head wraps the class, and the methods
536    /// inside it surface as ordinary `FunctionDefinition` nodes.
537    #[test]
538    fn cpp_class_template_members() {
539        check_metrics::<CppParser>(
540            "template<typename T>
541             struct Box {
542                 T value;
543                 T get() const { return value; }
544                 void set(T v) { value = v; }
545             };",
546            "foo.cpp",
547            |metric| {
548                let s = &metric.nom;
549                assert_eq!(s.functions_sum(), 2.0);
550                assert_eq!(s.closures_sum(), 0.0);
551                insta::assert_json_snapshot!(metric.nom);
552            },
553        );
554    }
555
556    /// Lambdas inside a function body count as `closures`, not as
557    /// `functions` — Cpp::LambdaExpression is the closure kind.  The
558    /// enclosing function adds 1 to `functions`; each lambda adds 1 to
559    /// `closures`.
560    #[test]
561    fn cpp_lambdas_inside_function() {
562        check_metrics::<CppParser>(
563            "int run() {
564                 auto add = [](int a, int b) { return a + b; };
565                 auto mul = [](int a, int b) { return a * b; };
566                 return add(1, 2) + mul(3, 4);
567             }",
568            "foo.cpp",
569            |metric| {
570                let s = &metric.nom;
571                // 1 enclosing function + 2 closures.
572                assert_eq!(s.functions_sum(), 1.0);
573                assert_eq!(s.closures_sum(), 2.0);
574                assert_eq!(s.total(), 3.0);
575                insta::assert_json_snapshot!(metric.nom);
576            },
577        );
578    }
579
580    #[test]
581    fn javascript_nom() {
582        check_metrics::<JavascriptParser>(
583            "function f(a, b) {
584                 function foo(a) {
585                     return a;
586                 }
587                 var bar = (function () {
588                     var counter = 0;
589                     return function () {
590                         counter += 1;
591                         return counter
592                     }
593                 })();
594                 return bar(foo(a), a);
595             }",
596            "foo.js",
597            |metric| {
598                // Number of spaces = 5
599                // functions: f, foo, bar
600                // closures:  return function ()
601                insta::assert_json_snapshot!(
602                    metric.nom,
603                    @r###"
604                    {
605                      "functions": 3.0,
606                      "closures": 1.0,
607                      "functions_average": 0.6,
608                      "closures_average": 0.2,
609                      "total": 4.0,
610                      "average": 0.8,
611                      "functions_min": 0.0,
612                      "functions_max": 1.0,
613                      "closures_min": 0.0,
614                      "closures_max": 1.0
615                    }"###
616                );
617            },
618        );
619    }
620
621    #[test]
622    fn javascript_call_nom() {
623        check_metrics::<JavascriptParser>(
624            "add_task(async function test_safe_mode() {
625                 gAppInfo.inSafeMode = true;
626             });",
627            "foo.js",
628            |metric| {
629                // Number of spaces = 2
630                // functions: test_safe_mode
631                insta::assert_json_snapshot!(
632                    metric.nom,
633                    @r###"
634                    {
635                      "functions": 1.0,
636                      "closures": 0.0,
637                      "functions_average": 0.5,
638                      "closures_average": 0.0,
639                      "total": 1.0,
640                      "average": 0.5,
641                      "functions_min": 0.0,
642                      "functions_max": 1.0,
643                      "closures_min": 0.0,
644                      "closures_max": 0.0
645                    }"###
646                );
647            },
648        );
649    }
650
651    #[test]
652    fn javascript_assignment_nom() {
653        check_metrics::<JavascriptParser>(
654            "AnimationTest.prototype.enableDisplay = function(element) {};",
655            "foo.js",
656            |metric| {
657                // Number of spaces = 2
658                insta::assert_json_snapshot!(
659                    metric.nom,
660                    @r###"
661                    {
662                      "functions": 1.0,
663                      "closures": 0.0,
664                      "functions_average": 0.5,
665                      "closures_average": 0.0,
666                      "total": 1.0,
667                      "average": 0.5,
668                      "functions_min": 0.0,
669                      "functions_max": 1.0,
670                      "closures_min": 0.0,
671                      "closures_max": 0.0
672                    }"###
673                );
674            },
675        );
676    }
677
678    #[test]
679    fn javascript_labeled_nom() {
680        check_metrics::<JavascriptParser>(
681            "toJSON: function() {
682                 return this.inspect(true);
683             }",
684            "foo.js",
685            |metric| {
686                // Number of spaces = 2
687                insta::assert_json_snapshot!(
688                    metric.nom,
689                    @r###"
690                    {
691                      "functions": 1.0,
692                      "closures": 0.0,
693                      "functions_average": 0.5,
694                      "closures_average": 0.0,
695                      "total": 1.0,
696                      "average": 0.5,
697                      "functions_min": 0.0,
698                      "functions_max": 1.0,
699                      "closures_min": 0.0,
700                      "closures_max": 0.0
701                    }"###
702                );
703            },
704        );
705    }
706
707    #[test]
708    fn javascript_labeled_arrow_nom() {
709        check_metrics::<JavascriptParser>(
710            "const dimConverters = {
711                pt: x => x,
712             };",
713            "foo.js",
714            |metric| {
715                // Number of spaces = 2
716                insta::assert_json_snapshot!(
717                    metric.nom,
718                    @r###"
719                    {
720                      "functions": 1.0,
721                      "closures": 0.0,
722                      "functions_average": 0.5,
723                      "closures_average": 0.0,
724                      "total": 1.0,
725                      "average": 0.5,
726                      "functions_min": 0.0,
727                      "functions_max": 1.0,
728                      "closures_min": 0.0,
729                      "closures_max": 0.0
730                    }"###
731                );
732            },
733        );
734    }
735
736    #[test]
737    fn javascript_pair_nom() {
738        check_metrics::<JavascriptParser>(
739            "return {
740                 initialize: function(object) {
741                     this._object = object.toObject();
742                 },
743             }",
744            "foo.js",
745            |metric| {
746                // Number of spaces = 2
747                insta::assert_json_snapshot!(
748                    metric.nom,
749                    @r###"
750                    {
751                      "functions": 1.0,
752                      "closures": 0.0,
753                      "functions_average": 0.5,
754                      "closures_average": 0.0,
755                      "total": 1.0,
756                      "average": 0.5,
757                      "functions_min": 0.0,
758                      "functions_max": 1.0,
759                      "closures_min": 0.0,
760                      "closures_max": 0.0
761                    }"###
762                );
763            },
764        );
765    }
766
767    fn check_returned_object_arrow_nom<T: ParserTrait>(file_name: &str) {
768        check_metrics::<T>(
769            "function f() { return { foo: x => x }; }",
770            file_name,
771            |metric| {
772                insta::allow_duplicates! {
773                    insta::assert_json_snapshot!(
774                        metric.nom,
775                        @r###"
776                        {
777                          "functions": 2.0,
778                          "closures": 0.0,
779                          "functions_average": 0.6666666666666666,
780                          "closures_average": 0.0,
781                          "total": 2.0,
782                          "average": 0.6666666666666666,
783                          "functions_min": 0.0,
784                          "functions_max": 1.0,
785                          "closures_min": 0.0,
786                          "closures_max": 0.0
787                        }"###
788                    );
789                }
790            },
791        );
792    }
793
794    #[test]
795    fn javascript_returned_object_arrow_nom() {
796        check_returned_object_arrow_nom::<JavascriptParser>("foo.js");
797    }
798
799    #[test]
800    fn mozjs_returned_object_arrow_nom() {
801        check_returned_object_arrow_nom::<MozjsParser>("foo.js");
802    }
803
804    #[test]
805    fn typescript_returned_object_arrow_nom() {
806        check_returned_object_arrow_nom::<TypescriptParser>("foo.ts");
807    }
808
809    #[test]
810    fn tsx_returned_object_arrow_nom() {
811        check_returned_object_arrow_nom::<TsxParser>("foo.tsx");
812    }
813
814    #[test]
815    fn javascript_unnamed_nom() {
816        check_metrics::<JavascriptParser>(
817            "Ajax.getTransport = Try.these(
818                 function() {
819                     return function(){ return new XMLHttpRequest()}
820                 }
821             );",
822            "foo.js",
823            |metric| {
824                // Number of spaces = 3
825                insta::assert_json_snapshot!(
826                    metric.nom,
827                    @r###"
828                    {
829                      "functions": 0.0,
830                      "closures": 2.0,
831                      "functions_average": 0.0,
832                      "closures_average": 0.6666666666666666,
833                      "total": 2.0,
834                      "average": 0.6666666666666666,
835                      "functions_min": 0.0,
836                      "functions_max": 0.0,
837                      "closures_min": 0.0,
838                      "closures_max": 1.0
839                    }"###
840                );
841            },
842        );
843    }
844
845    #[test]
846    fn javascript_arrow_nom() {
847        check_metrics::<JavascriptParser>(
848            "var materials = [\"Hydrogen\"];
849             materials.map(material => material.length);
850             let add = (a, b)  => a + b;",
851            "foo.js",
852            |metric| {
853                // Number of spaces = 3
854                // Functions: add
855                // Closures: material.map
856                insta::assert_json_snapshot!(
857                    metric.nom,
858                    @r###"
859                    {
860                      "functions": 1.0,
861                      "closures": 1.0,
862                      "functions_average": 0.3333333333333333,
863                      "closures_average": 0.3333333333333333,
864                      "total": 2.0,
865                      "average": 0.6666666666666666,
866                      "functions_min": 0.0,
867                      "functions_max": 1.0,
868                      "closures_min": 0.0,
869                      "closures_max": 1.0
870                    }"###
871                );
872            },
873        );
874    }
875
876    #[test]
877    fn javascript_arrow_assignment_nom() {
878        check_metrics::<JavascriptParser>("sink.onPull = () => { };", "foo.js", |metric| {
879            // Number of spaces = 2
880            insta::assert_json_snapshot!(
881                metric.nom,
882                @r###"
883                    {
884                      "functions": 1.0,
885                      "closures": 0.0,
886                      "functions_average": 0.5,
887                      "closures_average": 0.0,
888                      "total": 1.0,
889                      "average": 0.5,
890                      "functions_min": 0.0,
891                      "functions_max": 1.0,
892                      "closures_min": 0.0,
893                      "closures_max": 0.0
894                    }"###
895            );
896        });
897    }
898
899    #[test]
900    fn javascript_arrow_new_nom() {
901        check_metrics::<JavascriptParser>(
902            "const response = new Promise(resolve => channel.port1.onmessage = resolve);",
903            "foo.js",
904            |metric| {
905                // Number of spaces = 2
906                insta::assert_json_snapshot!(
907                    metric.nom,
908                    @r###"
909                    {
910                      "functions": 0.0,
911                      "closures": 1.0,
912                      "functions_average": 0.0,
913                      "closures_average": 0.5,
914                      "total": 1.0,
915                      "average": 0.5,
916                      "functions_min": 0.0,
917                      "functions_max": 0.0,
918                      "closures_min": 0.0,
919                      "closures_max": 1.0
920                    }"###
921                );
922            },
923        );
924    }
925
926    #[test]
927    fn javascript_arrow_call_nom() {
928        check_metrics::<JavascriptParser>(
929            "let notDisabled = TestUtils.waitForCondition(
930                 () => !backbutton.hasAttribute(\"disabled\")
931             );",
932            "foo.js",
933            |metric| {
934                // Number of spaces = 2
935                insta::assert_json_snapshot!(
936                    metric.nom,
937                    @r###"
938                    {
939                      "functions": 0.0,
940                      "closures": 1.0,
941                      "functions_average": 0.0,
942                      "closures_average": 0.5,
943                      "total": 1.0,
944                      "average": 0.5,
945                      "functions_min": 0.0,
946                      "functions_max": 0.0,
947                      "closures_min": 0.0,
948                      "closures_max": 1.0
949                    }"###
950                );
951            },
952        );
953    }
954
955    #[test]
956    fn java_nom() {
957        check_metrics::<JavaParser>(
958            "class A {
959                public void foo(){
960                    return;
961                }
962                public void bar(){
963                    return;
964                }
965            }",
966            "foo.java",
967            |metric| {
968                // Number of spaces = 4
969                insta::assert_json_snapshot!(
970                    metric.nom,
971                    @r###"
972                    {
973                      "functions": 2.0,
974                      "closures": 0.0,
975                      "functions_average": 0.5,
976                      "closures_average": 0.0,
977                      "total": 2.0,
978                      "average": 0.5,
979                      "functions_min": 0.0,
980                      "functions_max": 1.0,
981                      "closures_min": 0.0,
982                      "closures_max": 0.0
983                    }"###
984                );
985            },
986        );
987    }
988
989    #[test]
990    fn csharp_nom() {
991        check_metrics::<CsharpParser>(
992            "class A {
993                public void Foo() {
994                    return;
995                }
996                public void Bar() {
997                    return;
998                }
999                public int X { get; set; }
1000                public void Outer() {
1001                    void Local() { return; }
1002                    Local();
1003                }
1004            }",
1005            "foo.cs",
1006            |metric| {
1007                // Methods: Foo, Bar, Outer (=3 explicit)
1008                // Plus accessors `get`, `set` on X = 2 more functions
1009                // Plus `Local` local function = 1 more
1010                // Total functions = 6
1011                insta::assert_json_snapshot!(
1012                    metric.nom,
1013                    @r###"
1014                    {
1015                      "functions": 6.0,
1016                      "closures": 0.0,
1017                      "functions_average": 0.75,
1018                      "closures_average": 0.0,
1019                      "total": 6.0,
1020                      "average": 0.75,
1021                      "functions_min": 0.0,
1022                      "functions_max": 1.0,
1023                      "closures_min": 0.0,
1024                      "closures_max": 0.0
1025                    }"###
1026                );
1027            },
1028        );
1029    }
1030
1031    #[test]
1032    fn csharp_closure_nom() {
1033        check_metrics::<CsharpParser>(
1034            "class A {
1035                public void Run() {
1036                    System.Func<int, int> f = x => x + 1;
1037                    System.Action g = delegate(int y) { System.Console.WriteLine(y); };
1038                }
1039            }",
1040            "foo.cs",
1041            |metric| {
1042                // 1 method (Run), 1 lambda, 1 anonymous_method = 1 func + 2 closures.
1043                insta::assert_json_snapshot!(
1044                    metric.nom,
1045                    @r###"
1046                    {
1047                      "functions": 1.0,
1048                      "closures": 2.0,
1049                      "functions_average": 0.2,
1050                      "closures_average": 0.4,
1051                      "total": 3.0,
1052                      "average": 0.6,
1053                      "functions_min": 0.0,
1054                      "functions_max": 1.0,
1055                      "closures_min": 0.0,
1056                      "closures_max": 1.0
1057                    }"###
1058                );
1059            },
1060        );
1061    }
1062
1063    #[test]
1064    fn go_top_level_funcs() {
1065        check_metrics::<GoParser>(
1066            "package main
1067            func a() {}
1068            func b() {}
1069            func c() {}",
1070            "foo.go",
1071            |metric| {
1072                // Number of spaces = 4 (file unit + 3 funcs).
1073                insta::assert_json_snapshot!(
1074                    metric.nom,
1075                    @r###"
1076                    {
1077                      "functions": 3.0,
1078                      "closures": 0.0,
1079                      "functions_average": 0.75,
1080                      "closures_average": 0.0,
1081                      "total": 3.0,
1082                      "average": 0.75,
1083                      "functions_min": 0.0,
1084                      "functions_max": 1.0,
1085                      "closures_min": 0.0,
1086                      "closures_max": 0.0
1087                    }"###
1088                );
1089            },
1090        );
1091    }
1092
1093    #[test]
1094    fn go_method_declaration() {
1095        check_metrics::<GoParser>(
1096            "package main
1097            type T struct{}
1098            func (r *T) M() {}",
1099            "foo.go",
1100            |metric| {
1101                // method_declaration is counted as a function.
1102                insta::assert_json_snapshot!(
1103                    metric.nom,
1104                    @r###"
1105                    {
1106                      "functions": 1.0,
1107                      "closures": 0.0,
1108                      "functions_average": 0.5,
1109                      "closures_average": 0.0,
1110                      "total": 1.0,
1111                      "average": 0.5,
1112                      "functions_min": 0.0,
1113                      "functions_max": 1.0,
1114                      "closures_min": 0.0,
1115                      "closures_max": 0.0
1116                    }"###
1117                );
1118            },
1119        );
1120    }
1121
1122    #[test]
1123    fn go_func_literal_is_closure() {
1124        check_metrics::<GoParser>(
1125            "package main
1126            var f = func() {}",
1127            "foo.go",
1128            |metric| {
1129                // func_literal increments closure count, not function count.
1130                insta::assert_json_snapshot!(
1131                    metric.nom,
1132                    @r###"
1133                    {
1134                      "functions": 0.0,
1135                      "closures": 1.0,
1136                      "functions_average": 0.0,
1137                      "closures_average": 0.5,
1138                      "total": 1.0,
1139                      "average": 0.5,
1140                      "functions_min": 0.0,
1141                      "functions_max": 0.0,
1142                      "closures_min": 0.0,
1143                      "closures_max": 1.0
1144                    }"###
1145                );
1146            },
1147        );
1148    }
1149
1150    #[test]
1151    fn go_nested_closures() {
1152        check_metrics::<GoParser>(
1153            "package main
1154            func f() {
1155                inner := func() {
1156                    deeper := func() {}
1157                    _ = deeper
1158                }
1159                _ = inner
1160            }",
1161            "foo.go",
1162            |metric| {
1163                // 1 function (f) + 2 closures (inner, deeper).
1164                insta::assert_json_snapshot!(
1165                    metric.nom,
1166                    @r###"
1167                    {
1168                      "functions": 1.0,
1169                      "closures": 2.0,
1170                      "functions_average": 0.25,
1171                      "closures_average": 0.5,
1172                      "total": 3.0,
1173                      "average": 0.75,
1174                      "functions_min": 0.0,
1175                      "functions_max": 1.0,
1176                      "closures_min": 0.0,
1177                      "closures_max": 1.0
1178                    }"###
1179                );
1180            },
1181        );
1182    }
1183
1184    #[test]
1185    fn java_closure_nom() {
1186        check_metrics::<JavaParser>(
1187            "interface printable{
1188                void print();
1189              }
1190
1191              interface IntFunc {
1192                int func(int n);
1193              }
1194
1195              class Printer implements printable{
1196                public void print(){System.out.println(\"Hello\");}
1197
1198                public static void main(String args[]){
1199                  Printer  obj = new Printer();
1200                  obj.print();
1201                  IntFunc meaning = (i) -> i + 42;
1202                  int i = meaning.func(1);
1203                }
1204              }",
1205            "foo.java",
1206            |metric| {
1207                // Number of spaces = 8
1208                insta::assert_json_snapshot!(
1209                    metric.nom,
1210                    @r###"
1211                    {
1212                      "functions": 4.0,
1213                      "closures": 1.0,
1214                      "functions_average": 0.5,
1215                      "closures_average": 0.125,
1216                      "total": 5.0,
1217                      "average": 0.625,
1218                      "functions_min": 0.0,
1219                      "functions_max": 1.0,
1220                      "closures_min": 0.0,
1221                      "closures_max": 1.0
1222                    }"###
1223                );
1224            },
1225        );
1226    }
1227
1228    #[test]
1229    fn groovy_nom() {
1230        check_metrics::<GroovyParser>(
1231            "class Printer {
1232                void print() {
1233                    println 'hello'
1234                }
1235                static void main(String[] args) {
1236                    def p = new Printer()
1237                    p.print()
1238                    def doubler = { x -> x * 2 }
1239                    int r = doubler(21)
1240                }
1241            }",
1242            "foo.groovy",
1243            |metric| {
1244                // Two methods declared. The dekobon grammar parses
1245                // method bodies as `block`, distinct from `closure`, so
1246                // the explicit `doubler = { x -> x * 2 }` literal is the
1247                // only closure here (unlike the prior amaanq grammar
1248                // which mis-parsed each method body as a `closure`
1249                // node too).
1250                assert_eq!(metric.nom.functions_sum(), 2.0);
1251                assert_eq!(metric.nom.closures_sum(), 1.0);
1252            },
1253        );
1254    }
1255
1256    #[test]
1257    fn groovy_nom_function_definition() {
1258        // `def foo() {}` at top level uses `function_definition`, not
1259        // `method_declaration`. Nom must count it as a function.
1260        check_metrics::<GroovyParser>(
1261            "def greet(name) {
1262                println(name)
1263            }
1264            greet('world')",
1265            "foo.groovy",
1266            |metric| {
1267                assert_eq!(metric.nom.functions_sum(), 1.0);
1268            },
1269        );
1270    }
1271
1272    #[test]
1273    fn perl_nom() {
1274        check_metrics::<PerlParser>(
1275            "sub a { 1 }
1276             sub b { 2 }
1277             my $c = sub { 3 };
1278             sub outer {
1279                 my $inner = sub { 4 };
1280                 return $inner;
1281             }",
1282            "foo.pl",
1283            |metric| {
1284                insta::assert_json_snapshot!(
1285                    metric.nom,
1286                    @r#"
1287                {
1288                  "functions": 3.0,
1289                  "closures": 2.0,
1290                  "functions_average": 0.5,
1291                  "closures_average": 0.3333333333333333,
1292                  "total": 5.0,
1293                  "average": 0.8333333333333334,
1294                  "functions_min": 0.0,
1295                  "functions_max": 1.0,
1296                  "closures_min": 0.0,
1297                  "closures_max": 1.0
1298                }
1299                 "#
1300                );
1301            },
1302        );
1303    }
1304
1305    #[test]
1306    fn tsx_named_and_arrow_functions() {
1307        check_metrics::<TsxParser>(
1308            "function greet(name: string): string {
1309                 return `Hello, ${name}`;
1310             }
1311             const add = (a: number, b: number) => a + b;
1312             const log = () => { console.log('done'); };",
1313            "foo.tsx",
1314            |metric| {
1315                insta::assert_json_snapshot!(
1316                    metric.nom,
1317                    @r###"
1318                    {
1319                      "functions": 3.0,
1320                      "closures": 0.0,
1321                      "functions_average": 0.75,
1322                      "closures_average": 0.0,
1323                      "total": 3.0,
1324                      "average": 0.75,
1325                      "functions_min": 0.0,
1326                      "functions_max": 1.0,
1327                      "closures_min": 0.0,
1328                      "closures_max": 0.0
1329                    }"###
1330                );
1331            },
1332        );
1333    }
1334
1335    #[test]
1336    fn typescript_named_arrow_and_class_methods() {
1337        check_metrics::<TypescriptParser>(
1338            "function compute(x: number): number {
1339                 return x * 2;
1340             }
1341             const double = (n: number): number => n * 2;
1342             class Calculator {
1343                 add(a: number, b: number): number {
1344                     return a + b;
1345                 }
1346             }",
1347            "foo.ts",
1348            |metric| {
1349                insta::assert_json_snapshot!(
1350                    metric.nom,
1351                    @r###"
1352                    {
1353                      "functions": 3.0,
1354                      "closures": 0.0,
1355                      "functions_average": 0.6,
1356                      "closures_average": 0.0,
1357                      "total": 3.0,
1358                      "average": 0.6,
1359                      "functions_min": 0.0,
1360                      "functions_max": 1.0,
1361                      "closures_min": 0.0,
1362                      "closures_max": 0.0
1363                    }"###
1364                );
1365            },
1366        );
1367    }
1368
1369    #[test]
1370    fn mozjs_nom() {
1371        check_metrics::<MozjsParser>(
1372            "function f(a, b) {
1373                 function foo(a) {
1374                     return a;
1375                 }
1376                 var bar = (function () {
1377                     var counter = 0;
1378                     return function () {
1379                         counter += 1;
1380                         return counter
1381                     }
1382                 })();
1383                 return bar(foo(a), a);
1384             }",
1385            "foo.js",
1386            |metric| {
1387                insta::assert_json_snapshot!(
1388                    metric.nom,
1389                    @r###"
1390                    {
1391                      "functions": 3.0,
1392                      "closures": 1.0,
1393                      "functions_average": 0.6,
1394                      "closures_average": 0.2,
1395                      "total": 4.0,
1396                      "average": 0.8,
1397                      "functions_min": 0.0,
1398                      "functions_max": 1.0,
1399                      "closures_min": 0.0,
1400                      "closures_max": 1.0
1401                    }"###
1402                );
1403            },
1404        );
1405    }
1406
1407    #[test]
1408    fn mozjs_arrow_and_method() {
1409        check_metrics::<MozjsParser>(
1410            "let add = (a, b) => a + b;
1411             class Counter {
1412                 increment() {
1413                     this.count++;
1414                 }
1415             }",
1416            "foo.js",
1417            |metric| {
1418                insta::assert_json_snapshot!(
1419                    metric.nom,
1420                    @r###"
1421                    {
1422                      "functions": 2.0,
1423                      "closures": 0.0,
1424                      "functions_average": 0.5,
1425                      "closures_average": 0.0,
1426                      "total": 2.0,
1427                      "average": 0.5,
1428                      "functions_min": 0.0,
1429                      "functions_max": 1.0,
1430                      "closures_min": 0.0,
1431                      "closures_max": 0.0
1432                    }"###
1433                );
1434            },
1435        );
1436    }
1437
1438    #[test]
1439    fn kotlin_nom_class_with_methods() {
1440        check_metrics::<KotlinParser>(
1441            "class Calculator {
1442                fun add(a: Int, b: Int): Int {
1443                    return a + b
1444                }
1445                fun subtract(a: Int, b: Int): Int {
1446                    return a - b
1447                }
1448            }",
1449            "foo.kt",
1450            |metric| {
1451                insta::assert_json_snapshot!(
1452                    metric.nom,
1453                    @r###"
1454                    {
1455                      "functions": 2.0,
1456                      "closures": 0.0,
1457                      "functions_average": 0.5,
1458                      "closures_average": 0.0,
1459                      "total": 2.0,
1460                      "average": 0.5,
1461                      "functions_min": 0.0,
1462                      "functions_max": 1.0,
1463                      "closures_min": 0.0,
1464                      "closures_max": 0.0
1465                    }
1466                    "###
1467                );
1468            },
1469        );
1470    }
1471
1472    #[test]
1473    fn lua_nom() {
1474        check_metrics::<LuaParser>(
1475            "function greet(name)
1476  return \"hello \" .. name
1477end
1478
1479local add = function(a, b)
1480  return a + b
1481end
1482
1483local function outer()
1484  local inner = function()
1485    return 42
1486  end
1487  return inner()
1488end",
1489            "foo.lua",
1490            |metric| {
1491                // 2 named functions (greet, outer), 2 closures (add, inner)
1492                insta::assert_json_snapshot!(metric.nom, @r###"
1493                    {
1494                      "functions": 2.0,
1495                      "closures": 2.0,
1496                      "functions_average": 0.4,
1497                      "closures_average": 0.4,
1498                      "total": 4.0,
1499                      "average": 0.8,
1500                      "functions_min": 0.0,
1501                      "functions_max": 1.0,
1502                      "closures_min": 0.0,
1503                      "closures_max": 1.0
1504                    }
1505                    "###);
1506            },
1507        );
1508    }
1509
1510    #[test]
1511    fn bash_nom() {
1512        check_metrics::<BashParser>(
1513            "#!/bin/bash
1514foo() {
1515    echo 'hello'
1516}
1517bar() {
1518    echo 'world'
1519}
1520foo
1521bar",
1522            "foo.sh",
1523            |metric| {
1524                insta::assert_json_snapshot!(
1525                    metric.nom,
1526                    @r#"
1527                    {
1528                      "functions": 2.0,
1529                      "closures": 0.0,
1530                      "functions_average": 0.6666666666666666,
1531                      "closures_average": 0.0,
1532                      "total": 2.0,
1533                      "average": 0.6666666666666666,
1534                      "functions_min": 0.0,
1535                      "functions_max": 1.0,
1536                      "closures_min": 0.0,
1537                      "closures_max": 0.0
1538                    }
1539                    "#
1540                );
1541            },
1542        );
1543    }
1544
1545    #[test]
1546    fn tcl_nom() {
1547        check_metrics::<TclParser>(
1548            "proc foo {a} { puts $a }
1549proc bar {x y} { puts $x }
1550foo 1
1551bar 2 3",
1552            "foo.tcl",
1553            |metric| {
1554                assert_eq!(metric.nom.functions_sum(), 2.0);
1555                assert_eq!(metric.nom.closures_sum(), 0.0);
1556                insta::assert_json_snapshot!(metric.nom);
1557            },
1558        );
1559    }
1560
1561    #[test]
1562    fn tcl_nested_nom() {
1563        check_metrics::<TclParser>(
1564            "proc outer {a} {
1565    proc inner {x} { puts $x }
1566    inner $a
1567}",
1568            "foo.tcl",
1569            |metric| {
1570                assert_eq!(metric.nom.functions_sum(), 2.0);
1571                assert_eq!(metric.nom.closures_sum(), 0.0);
1572                insta::assert_json_snapshot!(metric.nom);
1573            },
1574        );
1575    }
1576
1577    #[test]
1578    fn typescript_class_methods() {
1579        check_metrics::<TypescriptParser>(
1580            "class Calc {
1581             add(a: number, b: number): number { return a + b; }
1582             sub(a: number, b: number): number { return a - b; }
1583         }",
1584            "foo.ts",
1585            |metric| {
1586                assert_eq!(metric.nom.functions_sum(), 2.0);
1587                assert_eq!(metric.nom.closures_sum(), 0.0);
1588                insta::assert_json_snapshot!(metric.nom);
1589            },
1590        );
1591    }
1592
1593    #[test]
1594    fn typescript_arrow_and_function() {
1595        check_metrics::<TypescriptParser>(
1596            "function f(): number { return 1; }
1597         const g = (): number => 2;
1598         const h = (x: number): number => x * 2;",
1599            "foo.ts",
1600            |metric| {
1601                assert_eq!(metric.nom.functions_sum(), 3.0);
1602                assert_eq!(metric.nom.closures_sum(), 0.0);
1603                insta::assert_json_snapshot!(metric.nom);
1604            },
1605        );
1606    }
1607
1608    #[test]
1609    fn tsx_class_methods() {
1610        check_metrics::<TsxParser>(
1611            "class Calc {
1612             add(a: number, b: number): number { return a + b; }
1613             sub(a: number, b: number): number { return a - b; }
1614         }",
1615            "foo.tsx",
1616            |metric| {
1617                assert_eq!(metric.nom.functions_sum(), 2.0);
1618                assert_eq!(metric.nom.closures_sum(), 0.0);
1619                insta::assert_json_snapshot!(metric.nom);
1620            },
1621        );
1622    }
1623
1624    #[test]
1625    fn tsx_arrow_and_function() {
1626        check_metrics::<TsxParser>(
1627            "function f(): number { return 1; }
1628         const g = (): number => 2;
1629         const h = (x: number): number => x * 2;",
1630            "foo.tsx",
1631            |metric| {
1632                assert_eq!(metric.nom.functions_sum(), 3.0);
1633                assert_eq!(metric.nom.closures_sum(), 0.0);
1634                insta::assert_json_snapshot!(metric.nom);
1635            },
1636        );
1637    }
1638
1639    #[test]
1640    fn bash_multiple_functions_nom() {
1641        check_metrics::<BashParser>(
1642            "#!/bin/bash
1643f() {
1644    echo hello
1645}
1646g() {
1647    echo world
1648}",
1649            "foo.sh",
1650            |metric| {
1651                assert_eq!(metric.nom.functions_sum(), 2.0);
1652                assert_eq!(metric.nom.closures_sum(), 0.0);
1653                insta::assert_json_snapshot!(metric.nom);
1654            },
1655        );
1656    }
1657
1658    #[test]
1659    fn bash_nested_functions_nom() {
1660        check_metrics::<BashParser>(
1661            "#!/bin/bash
1662outer() {
1663    inner() {
1664        echo inner
1665    }
1666    inner
1667}",
1668            "foo.sh",
1669            |metric| {
1670                assert_eq!(metric.nom.functions_sum(), 2.0);
1671                assert_eq!(metric.nom.closures_sum(), 0.0);
1672                insta::assert_json_snapshot!(metric.nom);
1673            },
1674        );
1675    }
1676
1677    #[test]
1678    fn mozjs_nested_function_nom() {
1679        check_metrics::<MozjsParser>(
1680            "function outer() {
1681             function inner() {
1682                 return 1;
1683             }
1684             return inner();
1685         }",
1686            "foo.js",
1687            |metric| {
1688                assert_eq!(metric.nom.functions_sum(), 2.0);
1689                assert_eq!(metric.nom.closures_sum(), 0.0);
1690                insta::assert_json_snapshot!(metric.nom);
1691            },
1692        );
1693    }
1694
1695    #[test]
1696    fn mozjs_class_methods_nom() {
1697        check_metrics::<MozjsParser>(
1698            "class Calc {
1699             add(a, b) { return a + b; }
1700             sub(a, b) { return a - b; }
1701         }",
1702            "foo.js",
1703            |metric| {
1704                assert_eq!(metric.nom.functions_sum(), 2.0);
1705                assert_eq!(metric.nom.closures_sum(), 0.0);
1706                insta::assert_json_snapshot!(metric.nom);
1707            },
1708        );
1709    }
1710
1711    #[test]
1712    fn mozjs_iife_nom() {
1713        check_metrics::<MozjsParser>(
1714            "(function() {
1715             var x = 1;
1716             return x;
1717         })();",
1718            "foo.js",
1719            |metric| {
1720                assert_eq!(metric.nom.functions_sum(), 0.0);
1721                assert_eq!(metric.nom.closures_sum(), 1.0);
1722                insta::assert_json_snapshot!(metric.nom);
1723            },
1724        );
1725    }
1726
1727    #[test]
1728    fn kotlin_class_methods_nom() {
1729        check_metrics::<KotlinParser>(
1730            "class Calc {
1731             fun add(a: Int, b: Int): Int = a + b
1732             fun sub(a: Int, b: Int): Int = a - b
1733         }",
1734            "foo.kt",
1735            |metric| {
1736                assert_eq!(metric.nom.functions_sum(), 2.0);
1737                assert_eq!(metric.nom.closures_sum(), 0.0);
1738                insta::assert_json_snapshot!(metric.nom);
1739            },
1740        );
1741    }
1742
1743    #[test]
1744    fn kotlin_lambda_nom() {
1745        check_metrics::<KotlinParser>(
1746            "fun f(list: List<Int>): Int {
1747             val double = { x: Int -> x * 2 }
1748             return list.sumOf(double)
1749         }",
1750            "foo.kt",
1751            |metric| {
1752                assert_eq!(metric.nom.functions_sum(), 1.0);
1753                assert_eq!(metric.nom.closures_sum(), 1.0);
1754                insta::assert_json_snapshot!(metric.nom);
1755            },
1756        );
1757    }
1758
1759    #[test]
1760    fn php_nom() {
1761        // Top-level function + 2 methods inside a class + 1 anonymous +
1762        // 1 arrow = 3 functions, 2 closures.
1763        check_metrics::<PhpParser>(
1764            "<?php
1765            function top(): void {}
1766            class A {
1767                public function m1(): void {}
1768                public function m2(): int {
1769                    $f = function () { return 1; };
1770                    $g = fn () => 2;
1771                    return $f() + $g();
1772                }
1773            }",
1774            "foo.php",
1775            |metric| {
1776                insta::assert_json_snapshot!(
1777                    metric.nom,
1778                    @r###"
1779                    {
1780                      "functions": 3.0,
1781                      "closures": 2.0,
1782                      "functions_average": 0.42857142857142855,
1783                      "closures_average": 0.2857142857142857,
1784                      "total": 5.0,
1785                      "average": 0.7142857142857143,
1786                      "functions_min": 0.0,
1787                      "functions_max": 1.0,
1788                      "closures_min": 0.0,
1789                      "closures_max": 1.0
1790                    }"###
1791                );
1792            },
1793        );
1794    }
1795
1796    #[test]
1797    fn php_nom_anonymous_class() {
1798        // Methods inside `new class { … }` count toward the closure-style
1799        // space mechanism: anonymous_class is its own space and its
1800        // method_declaration children are counted as functions.
1801        check_metrics::<PhpParser>(
1802            "<?php
1803            function f(): object {
1804                return new class {
1805                    public function inner(): int { return 1; }
1806                };
1807            }",
1808            "foo.php",
1809            |metric| {
1810                insta::assert_json_snapshot!(
1811                    metric.nom,
1812                    @r###"
1813                    {
1814                      "functions": 2.0,
1815                      "closures": 0.0,
1816                      "functions_average": 0.5,
1817                      "closures_average": 0.0,
1818                      "total": 2.0,
1819                      "average": 0.5,
1820                      "functions_min": 0.0,
1821                      "functions_max": 1.0,
1822                      "closures_min": 0.0,
1823                      "closures_max": 0.0
1824                    }"###
1825                );
1826            },
1827        );
1828    }
1829
1830    // Documents Elixir's current default-impl behaviour: `def`/`defp`
1831    // surface as `Call` nodes whose target is an `Identifier`, and
1832    // `is_func` returns `false`, so Nom only counts `AnonymousFunction`
1833    // closures. Two anon functions + zero functions is the load-bearing
1834    // claim — a future real impl that started counting `def` calls
1835    // would flip this. The same call-target text-inspection pattern
1836    // that #179 introduced for `Cyclomatic` would apply here once
1837    // `Nom::compute` is widened to take the source bytes.
1838    #[test]
1839    fn elixir_default_nom_counts_only_anonymous_functions() {
1840        check_metrics::<ElixirParser>(
1841            "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",
1842            "foo.ex",
1843            |metric| {
1844                assert_eq!(metric.nom.functions_sum(), 0.0);
1845                assert_eq!(metric.nom.closures_sum(), 2.0);
1846            },
1847        );
1848    }
1849
1850    #[test]
1851    fn stats_display_commas_between_all_fields() {
1852        let stats = Stats {
1853            functions: 0,
1854            closures: 0,
1855            functions_sum: 3,
1856            closures_sum: 1,
1857            functions_min: 0,
1858            functions_max: 2,
1859            closures_min: 0,
1860            closures_max: 1,
1861            space_count: 2,
1862        };
1863        let formatted = format!("{stats}");
1864
1865        // Every adjacent pair of labels must appear with ", " between the
1866        // previous field's value and the next label.
1867        let expected_fragments = [
1868            "functions: 3, closures: 1",
1869            "closures: 1, functions_average:",
1870            "functions_average: 1.5, closures_average:",
1871            "closures_average: 0.5, total:",
1872            "total: 4, average:",
1873            "average: 2, functions_min:",
1874            "functions_min: 0, functions_max:",
1875            "functions_max: 2, closures_min:",
1876            "closures_min: 0, closures_max:",
1877        ];
1878
1879        for fragment in expected_fragments {
1880            assert!(
1881                formatted.contains(fragment),
1882                "missing fragment {fragment:?} in: {formatted}"
1883            );
1884        }
1885    }
1886
1887    #[test]
1888    fn ruby_nom() {
1889        // expected: total = 4 (2 methods `add`/`mul` + 1 singleton
1890        // method `self.factory` + 1 block argument to `each`).
1891        // `functions` counts only the named `Method` / `SingletonMethod`
1892        // forms (3); `closures` counts `Block` / `DoBlock` / `Lambda`
1893        // (1).
1894        check_metrics::<RubyParser>(
1895            "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",
1896            "foo.rb",
1897            |metric| {
1898                assert_eq!(metric.nom.functions_sum(), 3.0);
1899                assert_eq!(metric.nom.closures_sum(), 1.0);
1900                assert_eq!(metric.nom.total(), 4.0);
1901            },
1902        );
1903    }
1904}