Skip to main content

big_code_analysis/metrics/
nargs.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;
26use crate::*;
27
28/// The `NArgs` metric.
29///
30/// This metric counts the number of arguments
31/// of functions/closures.
32#[derive(Debug, Clone)]
33pub struct Stats {
34    fn_nargs: usize,
35    closure_nargs: usize,
36    fn_nargs_sum: usize,
37    closure_nargs_sum: usize,
38    fn_nargs_min: usize,
39    closure_nargs_min: usize,
40    fn_nargs_max: usize,
41    closure_nargs_max: usize,
42    total_functions: usize,
43    total_closures: usize,
44}
45
46impl Default for Stats {
47    fn default() -> Self {
48        Self {
49            fn_nargs: 0,
50            closure_nargs: 0,
51            fn_nargs_sum: 0,
52            closure_nargs_sum: 0,
53            fn_nargs_min: usize::MAX,
54            closure_nargs_min: usize::MAX,
55            fn_nargs_max: 0,
56            closure_nargs_max: 0,
57            total_functions: 0,
58            total_closures: 0,
59        }
60    }
61}
62
63impl Serialize for Stats {
64    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
65    where
66        S: Serializer,
67    {
68        let mut st = serializer.serialize_struct("nargs", 10)?;
69        st.serialize_field("total_functions", &self.fn_args_sum())?;
70        st.serialize_field("total_closures", &self.closure_args_sum())?;
71        st.serialize_field("average_functions", &self.fn_args_average())?;
72        st.serialize_field("average_closures", &self.closure_args_average())?;
73        st.serialize_field("total", &self.nargs_total())?;
74        st.serialize_field("average", &self.nargs_average())?;
75        st.serialize_field("functions_min", &self.fn_args_min())?;
76        st.serialize_field("functions_max", &self.fn_args_max())?;
77        st.serialize_field("closures_min", &self.closure_args_min())?;
78        st.serialize_field("closures_max", &self.closure_args_max())?;
79        st.end()
80    }
81}
82
83impl fmt::Display for Stats {
84    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85        write!(
86            f,
87            "total_functions: {}, total_closures: {}, average_functions: {}, average_closures: {}, total: {}, average: {}, functions_min: {}, functions_max: {}, closures_min: {}, closures_max: {}",
88            self.fn_args(),
89            self.closure_args(),
90            self.fn_args_average(),
91            self.closure_args_average(),
92            self.nargs_total(),
93            self.nargs_average(),
94            self.fn_args_min(),
95            self.fn_args_max(),
96            self.closure_args_min(),
97            self.closure_args_max()
98        )
99    }
100}
101
102impl Stats {
103    /// Merges a second `NArgs` metric into the first one
104    pub fn merge(&mut self, other: &Stats) {
105        self.closure_nargs_min = self.closure_nargs_min.min(other.closure_nargs_min);
106        self.closure_nargs_max = self.closure_nargs_max.max(other.closure_nargs_max);
107        self.fn_nargs_min = self.fn_nargs_min.min(other.fn_nargs_min);
108        self.fn_nargs_max = self.fn_nargs_max.max(other.fn_nargs_max);
109        self.fn_nargs_sum += other.fn_nargs_sum;
110        self.closure_nargs_sum += other.closure_nargs_sum;
111    }
112
113    /// Returns the number of function arguments in a space.
114    #[inline]
115    #[must_use]
116    pub fn fn_args(&self) -> f64 {
117        self.fn_nargs as f64
118    }
119
120    /// Returns the number of closure arguments in a space.
121    #[inline]
122    #[must_use]
123    pub fn closure_args(&self) -> f64 {
124        self.closure_nargs as f64
125    }
126
127    /// Returns the number of function arguments sum in a space.
128    #[inline]
129    #[must_use]
130    pub fn fn_args_sum(&self) -> f64 {
131        self.fn_nargs_sum as f64
132    }
133
134    /// Returns the number of closure arguments sum in a space.
135    #[inline]
136    #[must_use]
137    pub fn closure_args_sum(&self) -> f64 {
138        self.closure_nargs_sum as f64
139    }
140
141    /// Returns the average number of functions arguments in a space.
142    #[inline]
143    #[must_use]
144    pub fn fn_args_average(&self) -> f64 {
145        self.fn_nargs_sum as f64 / self.total_functions.max(1) as f64
146    }
147
148    /// Returns the average number of closures arguments in a space.
149    #[inline]
150    #[must_use]
151    pub fn closure_args_average(&self) -> f64 {
152        self.closure_nargs_sum as f64 / self.total_closures.max(1) as f64
153    }
154
155    /// Returns the total number of arguments of each function and
156    /// closure in a space.
157    #[inline]
158    #[must_use]
159    pub fn nargs_total(&self) -> f64 {
160        self.fn_args_sum() + self.closure_args_sum()
161    }
162
163    /// Returns the `NArgs` metric average value
164    ///
165    /// This value is computed dividing the `NArgs` value
166    /// for the total number of functions/closures in a space.
167    #[inline]
168    #[must_use]
169    pub fn nargs_average(&self) -> f64 {
170        self.nargs_total() / (self.total_functions + self.total_closures).max(1) as f64
171    }
172    /// Returns the minimum number of function arguments in a space.
173    ///
174    /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
175    /// into `fn_nargs_min` to `0.0`, so a never-observed space
176    /// serializes to a meaningful number rather than `1.8446744e19`.
177    #[inline]
178    #[must_use]
179    pub fn fn_args_min(&self) -> f64 {
180        if self.fn_nargs_min == usize::MAX {
181            0.0
182        } else {
183            self.fn_nargs_min as f64
184        }
185    }
186    /// Returns the maximum number of function arguments in a space.
187    #[inline]
188    #[must_use]
189    pub fn fn_args_max(&self) -> f64 {
190        self.fn_nargs_max as f64
191    }
192    /// Returns the minimum number of closure arguments in a space.
193    ///
194    /// Same `usize::MAX` sentinel collapse as `fn_args_min`.
195    #[inline]
196    #[must_use]
197    pub fn closure_args_min(&self) -> f64 {
198        if self.closure_nargs_min == usize::MAX {
199            0.0
200        } else {
201            self.closure_nargs_min as f64
202        }
203    }
204    /// Returns the maximum number of closure arguments in a space.
205    #[inline]
206    #[must_use]
207    pub fn closure_args_max(&self) -> f64 {
208        self.closure_nargs_max as f64
209    }
210    #[inline]
211    pub(crate) fn compute_sum(&mut self) {
212        self.closure_nargs_sum += self.closure_nargs;
213        self.fn_nargs_sum += self.fn_nargs;
214    }
215    #[inline]
216    pub(crate) fn compute_minmax(&mut self) {
217        self.closure_nargs_min = self.closure_nargs_min.min(self.closure_nargs);
218        self.closure_nargs_max = self.closure_nargs_max.max(self.closure_nargs);
219        self.fn_nargs_min = self.fn_nargs_min.min(self.fn_nargs);
220        self.fn_nargs_max = self.fn_nargs_max.max(self.fn_nargs);
221        self.compute_sum();
222    }
223    pub(crate) fn finalize(&mut self, total_functions: usize, total_closures: usize) {
224        self.total_functions = total_functions;
225        self.total_closures = total_closures;
226    }
227}
228
229#[inline]
230fn compute_args<T: Checker>(node: &Node, nargs: &mut usize) {
231    if let Some(params) = node.child_by_field_name("parameters") {
232        let node_params = params;
233        node_params.act_on_child(&mut |n| {
234            if !T::is_non_arg(n) {
235                *nargs += 1;
236            }
237        });
238    } else if node.child_by_field_name("parameter").is_some() {
239        // JS/TS/TSX/MozJS arrow functions with a bare identifier parameter
240        // (`x => …`) use the singular `parameter` field instead of the plural
241        // `parameters` field. The grammar guarantees this is exactly one
242        // identifier, so count it as one argument.
243        *nargs += 1;
244    }
245}
246
247#[doc(hidden)]
248/// Per-language counting of function arguments.
249pub trait NArgs
250where
251    Self: Checker,
252    Self: std::marker::Sized,
253{
254    /// Walk `node` and update `stats` with this metric for the language
255    /// implementing the trait.
256    fn compute(node: &Node, stats: &mut Stats) {
257        if Self::is_func(node) {
258            compute_args::<Self>(node, &mut stats.fn_nargs);
259            return;
260        }
261
262        if Self::is_closure(node) {
263            compute_args::<Self>(node, &mut stats.closure_nargs);
264        }
265    }
266}
267
268impl NArgs for CppCode {
269    fn compute(node: &Node, stats: &mut Stats) {
270        if Self::is_func(node) {
271            if let Some(declarator) = node.child_by_field_name("declarator") {
272                let new_node = declarator;
273                compute_args::<Self>(&new_node, &mut stats.fn_nargs);
274            }
275            return;
276        }
277
278        if Self::is_closure(node)
279            && let Some(declarator) = node.child_by_field_name("declarator")
280        {
281            let new_node = declarator;
282            compute_args::<Self>(&new_node, &mut stats.closure_nargs);
283        }
284    }
285}
286
287// Go's `parameter_declaration` allows multiple names to share one type
288// (`func f(a, b int)` is one parameter_declaration with two `name` children
289// but two formal parameters). Count names rather than declarations so the
290// reported nargs matches Go's parameter count.
291fn compute_go_args(node: &Node, nargs: &mut usize) {
292    let Some(params) = node.child_by_field_name("parameters") else {
293        return;
294    };
295    *nargs += params
296        .children()
297        .map(|child| match child.kind_id().into() {
298            Go::ParameterDeclaration => child
299                .children()
300                .filter(|c| c.kind_id() == Go::Identifier)
301                .count()
302                .max(1),
303            Go::VariadicParameterDeclaration => 1,
304            _ => 0,
305        })
306        .sum::<usize>();
307}
308
309impl NArgs for GoCode {
310    fn compute(node: &Node, stats: &mut Stats) {
311        if Self::is_func(node) {
312            compute_go_args(node, &mut stats.fn_nargs);
313            return;
314        }
315
316        if Self::is_closure(node) {
317            compute_go_args(node, &mut stats.closure_nargs);
318        }
319    }
320}
321
322fn compute_kotlin_func_args(node: &Node, nargs: &mut usize) {
323    if let Some(params) = node
324        .children()
325        .find(|c| c.kind_id() == Kotlin::FunctionValueParameters)
326    {
327        params.act_on_child(&mut |n| {
328            if n.kind_id() == Kotlin::Parameter {
329                *nargs += 1;
330            }
331        });
332    }
333}
334
335fn compute_kotlin_lambda_args(node: &Node, nargs: &mut usize) {
336    // Lambda parameters are plain identifiers or destructuring patterns separated
337    // by commas; there is no typed `Parameter` wrapper node (unlike function
338    // value parameters), so a negative COMMA filter is the correct predicate here.
339    if let Some(params) = node
340        .children()
341        .find(|c| c.kind_id() == Kotlin::LambdaParameters)
342    {
343        params.act_on_child(&mut |n| {
344            if n.kind_id() != Kotlin::COMMA {
345                *nargs += 1;
346            }
347        });
348    }
349}
350
351impl NArgs for KotlinCode {
352    fn compute(node: &Node, stats: &mut Stats) {
353        if Self::is_func(node) {
354            compute_kotlin_func_args(node, &mut stats.fn_nargs);
355            return;
356        }
357
358        if Self::is_closure(node) {
359            if node.kind_id() == Kotlin::LambdaLiteral {
360                compute_kotlin_lambda_args(node, &mut stats.closure_nargs);
361            } else {
362                compute_kotlin_func_args(node, &mut stats.closure_nargs);
363            }
364        }
365    }
366}
367
368fn compute_lua_args(node: &Node, nargs: &mut usize) {
369    let Some(params) = node.child_by_field_name("parameters") else {
370        return;
371    };
372    *nargs += params
373        .children()
374        .filter(|c| matches!(c.kind_id().into(), Lua::Identifier | Lua::VarargExpression))
375        .count();
376}
377
378impl NArgs for LuaCode {
379    fn compute(node: &Node, stats: &mut Stats) {
380        if Self::is_func(node) {
381            compute_lua_args(node, &mut stats.fn_nargs);
382        } else if Self::is_closure(node) {
383            compute_lua_args(node, &mut stats.closure_nargs);
384        }
385    }
386}
387
388fn compute_tcl_args(node: &Node, nargs: &mut usize) {
389    let Some(params) = node.child_by_field_name("arguments") else {
390        return;
391    };
392    *nargs += params
393        .children()
394        .filter(|c| c.kind_id() == Tcl::Argument)
395        .count();
396}
397
398impl NArgs for TclCode {
399    fn compute(node: &Node, stats: &mut Stats) {
400        if Self::is_func(node) {
401            compute_tcl_args(node, &mut stats.fn_nargs);
402        }
403    }
404}
405
406implement_metric_trait!(
407    [NArgs],
408    PythonCode,
409    MozjsCode,
410    JavascriptCode,
411    TypescriptCode,
412    TsxCode,
413    RustCode,
414    PreprocCode,
415    CcommentCode,
416    JavaCode,
417    PerlCode,
418    BashCode,
419    PhpCode,
420    CsharpCode,
421    ElixirCode,
422    RubyCode
423);
424
425// Groovy closures use `closure_parameters` as an unnamed child rather
426// than a `parameters` field, so the default NArgs walker (which looks
427// for a `parameters` field) misses them. Match the closure_parameters
428// child directly and count its `closure_parameter` grand-children.
429impl NArgs for GroovyCode {
430    fn compute(node: &Node, stats: &mut Stats) {
431        use crate::languages::language_groovy::Groovy;
432
433        if Self::is_func(node) {
434            compute_args::<Self>(node, &mut stats.fn_nargs);
435            return;
436        }
437
438        if Self::is_closure(node)
439            && let Some(params) = node.first_child(|id| id == Groovy::ClosureParameters)
440        {
441            params.act_on_child(&mut |n| {
442                if n.kind_id() == Groovy::ClosureParameter {
443                    stats.closure_nargs += 1;
444                }
445            });
446        }
447    }
448}
449
450#[cfg(test)]
451#[allow(
452    clippy::float_cmp,
453    clippy::cast_precision_loss,
454    clippy::cast_possible_truncation,
455    clippy::cast_sign_loss,
456    clippy::similar_names,
457    clippy::doc_markdown,
458    clippy::needless_raw_string_hashes,
459    clippy::too_many_lines
460)]
461mod tests {
462    use crate::tools::check_metrics;
463
464    use super::*;
465
466    /// Regression for #227: a `Stats::default()` that never sees an
467    /// observation must not leak the `usize::MAX` sentinel for
468    /// `fn_args_min` or `closure_args_min`. Both getters collapse
469    /// the sentinel to `0.0` so JSON never emits `1.8446744e19`.
470    #[test]
471    fn nargs_empty_file_min_is_zero() {
472        let stats = Stats::default();
473        assert_eq!(stats.fn_args_min(), 0.0);
474        assert_eq!(stats.closure_args_min(), 0.0);
475    }
476
477    #[test]
478    fn python_no_functions_and_closures() {
479        check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
480            // 0 functions + 0 closures
481            insta::assert_json_snapshot!(
482                metric.nargs,
483                @r###"
484                    {
485                      "total_functions": 0.0,
486                      "total_closures": 0.0,
487                      "average_functions": 0.0,
488                      "average_closures": 0.0,
489                      "total": 0.0,
490                      "average": 0.0,
491                      "functions_min": 0.0,
492                      "functions_max": 0.0,
493                      "closures_min": 0.0,
494                      "closures_max": 0.0
495                    }"###
496            );
497        });
498    }
499
500    #[test]
501    fn rust_no_functions_and_closures() {
502        check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
503            // 0 functions + 0 closures
504            insta::assert_json_snapshot!(
505                metric.nargs,
506                @r###"
507                    {
508                      "total_functions": 0.0,
509                      "total_closures": 0.0,
510                      "average_functions": 0.0,
511                      "average_closures": 0.0,
512                      "total": 0.0,
513                      "average": 0.0,
514                      "functions_min": 0.0,
515                      "functions_max": 0.0,
516                      "closures_min": 0.0,
517                      "closures_max": 0.0
518                    }"###
519            );
520        });
521    }
522
523    #[test]
524    fn cpp_no_functions_and_closures() {
525        check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
526            // 0 functions + 0 closures
527            insta::assert_json_snapshot!(
528                metric.nargs,
529                @r###"
530                    {
531                      "total_functions": 0.0,
532                      "total_closures": 0.0,
533                      "average_functions": 0.0,
534                      "average_closures": 0.0,
535                      "total": 0.0,
536                      "average": 0.0,
537                      "functions_min": 0.0,
538                      "functions_max": 0.0,
539                      "closures_min": 0.0,
540                      "closures_max": 0.0
541                    }"###
542            );
543        });
544    }
545
546    #[test]
547    fn javascript_no_functions_and_closures() {
548        check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
549            // 0 functions + 0 closures
550            insta::assert_json_snapshot!(
551                metric.nargs,
552                @r###"
553                    {
554                      "total_functions": 0.0,
555                      "total_closures": 0.0,
556                      "average_functions": 0.0,
557                      "average_closures": 0.0,
558                      "total": 0.0,
559                      "average": 0.0,
560                      "functions_min": 0.0,
561                      "functions_max": 0.0,
562                      "closures_min": 0.0,
563                      "closures_max": 0.0
564                    }"###
565            );
566        });
567    }
568
569    #[test]
570    fn python_single_function() {
571        check_metrics::<PythonParser>(
572            "def f(a, b):
573                 if a:
574                     return a",
575            "foo.py",
576            |metric| {
577                // 1 function
578                insta::assert_json_snapshot!(
579                    metric.nargs,
580                    @r###"
581                    {
582                      "total_functions": 2.0,
583                      "total_closures": 0.0,
584                      "average_functions": 2.0,
585                      "average_closures": 0.0,
586                      "total": 2.0,
587                      "average": 2.0,
588                      "functions_min": 0.0,
589                      "functions_max": 2.0,
590                      "closures_min": 0.0,
591                      "closures_max": 0.0
592                    }"###
593                );
594            },
595        );
596    }
597
598    #[test]
599    fn rust_single_function() {
600        check_metrics::<RustParser>(
601            "fn f(a: bool, b: usize) {
602                 if a {
603                     return a;
604                }
605             }",
606            "foo.rs",
607            |metric| {
608                // 1 function
609                insta::assert_json_snapshot!(
610                    metric.nargs,
611                    @r###"
612                    {
613                      "total_functions": 2.0,
614                      "total_closures": 0.0,
615                      "average_functions": 2.0,
616                      "average_closures": 0.0,
617                      "total": 2.0,
618                      "average": 2.0,
619                      "functions_min": 0.0,
620                      "functions_max": 2.0,
621                      "closures_min": 0.0,
622                      "closures_max": 0.0
623                    }"###
624                );
625            },
626        );
627    }
628
629    #[test]
630    fn c_single_function() {
631        check_metrics::<CppParser>(
632            "int f(int a, int b) {
633                 if (a) {
634                     return a;
635                }
636             }",
637            "foo.c",
638            |metric| {
639                // 1 function
640                insta::assert_json_snapshot!(
641                    metric.nargs,
642                    @r###"
643                    {
644                      "total_functions": 2.0,
645                      "total_closures": 0.0,
646                      "average_functions": 2.0,
647                      "average_closures": 0.0,
648                      "total": 2.0,
649                      "average": 2.0,
650                      "functions_min": 0.0,
651                      "functions_max": 2.0,
652                      "closures_min": 0.0,
653                      "closures_max": 0.0
654                    }"###
655                );
656            },
657        );
658    }
659
660    #[test]
661    fn javascript_single_function() {
662        check_metrics::<JavascriptParser>(
663            "function f(a, b) {
664                 return a * b;
665             }",
666            "foo.js",
667            |metric| {
668                // 1 function
669                insta::assert_json_snapshot!(
670                    metric.nargs,
671                    @r###"
672                    {
673                      "total_functions": 2.0,
674                      "total_closures": 0.0,
675                      "average_functions": 2.0,
676                      "average_closures": 0.0,
677                      "total": 2.0,
678                      "average": 2.0,
679                      "functions_min": 0.0,
680                      "functions_max": 2.0,
681                      "closures_min": 0.0,
682                      "closures_max": 0.0
683                    }"###
684                );
685            },
686        );
687    }
688
689    #[test]
690    fn python_single_lambda() {
691        check_metrics::<PythonParser>("bar = lambda a: True", "foo.py", |metric| {
692            // 1 lambda
693            insta::assert_json_snapshot!(
694                metric.nargs,
695                @r###"
696                    {
697                      "total_functions": 0.0,
698                      "total_closures": 1.0,
699                      "average_functions": 0.0,
700                      "average_closures": 1.0,
701                      "total": 1.0,
702                      "average": 1.0,
703                      "functions_min": 0.0,
704                      "functions_max": 0.0,
705                      "closures_min": 1.0,
706                      "closures_max": 1.0
707                    }"###
708            );
709        });
710    }
711
712    #[test]
713    fn rust_single_closure() {
714        check_metrics::<RustParser>("let bar = |i: i32| -> i32 { i + 1 };", "foo.rs", |metric| {
715            // 1 lambda
716            insta::assert_json_snapshot!(
717                metric.nargs,
718                @r###"
719                    {
720                      "total_functions": 0.0,
721                      "total_closures": 1.0,
722                      "average_functions": 0.0,
723                      "average_closures": 1.0,
724                      "total": 1.0,
725                      "average": 1.0,
726                      "functions_min": 0.0,
727                      "functions_max": 0.0,
728                      "closures_min": 0.0,
729                      "closures_max": 1.0
730                    }"###
731            );
732        });
733    }
734
735    #[test]
736    fn cpp_single_lambda() {
737        check_metrics::<CppParser>(
738            "auto bar = [](int x, int y) -> int { return x + y; };",
739            "foo.cpp",
740            |metric| {
741                // 1 lambda
742                insta::assert_json_snapshot!(
743                    metric.nargs,
744                    @r###"
745                    {
746                      "total_functions": 0.0,
747                      "total_closures": 2.0,
748                      "average_functions": 0.0,
749                      "average_closures": 2.0,
750                      "total": 2.0,
751                      "average": 2.0,
752                      "functions_min": 0.0,
753                      "functions_max": 0.0,
754                      "closures_min": 2.0,
755                      "closures_max": 2.0
756                    }"###
757                );
758            },
759        );
760    }
761
762    #[test]
763    fn javascript_single_closure() {
764        check_metrics::<JavascriptParser>("function (a, b) {return a + b};", "foo.js", |metric| {
765            // 1 lambda
766            insta::assert_json_snapshot!(
767                metric.nargs,
768                @r###"
769                    {
770                      "total_functions": 0.0,
771                      "total_closures": 2.0,
772                      "average_functions": 0.0,
773                      "average_closures": 2.0,
774                      "total": 2.0,
775                      "average": 2.0,
776                      "functions_min": 0.0,
777                      "functions_max": 0.0,
778                      "closures_min": 0.0,
779                      "closures_max": 2.0
780                    }"###
781            );
782        });
783    }
784
785    #[test]
786    fn python_functions() {
787        check_metrics::<PythonParser>(
788            "def f(a, b):
789                 if a:
790                     return a
791            def f(a, b):
792                 if b:
793                     return b",
794            "foo.py",
795            |metric| {
796                // 2 functions
797                insta::assert_json_snapshot!(
798                    metric.nargs,
799                    @r###"
800                    {
801                      "total_functions": 4.0,
802                      "total_closures": 0.0,
803                      "average_functions": 2.0,
804                      "average_closures": 0.0,
805                      "total": 4.0,
806                      "average": 2.0,
807                      "functions_min": 0.0,
808                      "functions_max": 2.0,
809                      "closures_min": 0.0,
810                      "closures_max": 0.0
811                    }"###
812                );
813            },
814        );
815
816        check_metrics::<PythonParser>(
817            "def f(a, b):
818                 if a:
819                     return a
820            def f(a, b, c):
821                 if b:
822                     return b",
823            "foo.py",
824            |metric| {
825                // 2 functions
826                insta::assert_json_snapshot!(
827                    metric.nargs,
828                    @r###"
829                    {
830                      "total_functions": 5.0,
831                      "total_closures": 0.0,
832                      "average_functions": 2.5,
833                      "average_closures": 0.0,
834                      "total": 5.0,
835                      "average": 2.5,
836                      "functions_min": 0.0,
837                      "functions_max": 3.0,
838                      "closures_min": 0.0,
839                      "closures_max": 0.0
840                    }"###
841                );
842            },
843        );
844    }
845
846    #[test]
847    fn rust_functions() {
848        check_metrics::<RustParser>(
849            "fn f(a: bool, b: usize) {
850                 if a {
851                     return a;
852                }
853             }
854             fn f1(a: bool, b: usize) {
855                 if a {
856                     return a;
857                }
858             }",
859            "foo.rs",
860            |metric| {
861                // 2 functions
862                insta::assert_json_snapshot!(
863                    metric.nargs,
864                    @r###"
865                    {
866                      "total_functions": 4.0,
867                      "total_closures": 0.0,
868                      "average_functions": 2.0,
869                      "average_closures": 0.0,
870                      "total": 4.0,
871                      "average": 2.0,
872                      "functions_min": 0.0,
873                      "functions_max": 2.0,
874                      "closures_min": 0.0,
875                      "closures_max": 0.0
876                    }"###
877                );
878            },
879        );
880
881        check_metrics::<RustParser>(
882            "fn f(a: bool, b: usize) {
883                 if a {
884                     return a;
885                }
886             }
887             fn f1(a: bool, b: usize, c: usize) {
888                 if a {
889                     return a;
890                }
891             }",
892            "foo.rs",
893            |metric| {
894                // 2 functions
895                insta::assert_json_snapshot!(
896                    metric.nargs,
897                    @r###"
898                    {
899                      "total_functions": 5.0,
900                      "total_closures": 0.0,
901                      "average_functions": 2.5,
902                      "average_closures": 0.0,
903                      "total": 5.0,
904                      "average": 2.5,
905                      "functions_min": 0.0,
906                      "functions_max": 3.0,
907                      "closures_min": 0.0,
908                      "closures_max": 0.0
909                    }"###
910                );
911            },
912        );
913    }
914
915    #[test]
916    fn c_functions() {
917        check_metrics::<CppParser>(
918            "int f(int a, int b) {
919                 if (a) {
920                     return a;
921                }
922             }
923             int f1(int a, int b) {
924                 if (a) {
925                     return a;
926                }
927             }",
928            "foo.c",
929            |metric| {
930                // 2 functions
931                insta::assert_json_snapshot!(
932                    metric.nargs,
933                    @r###"
934                    {
935                      "total_functions": 4.0,
936                      "total_closures": 0.0,
937                      "average_functions": 2.0,
938                      "average_closures": 0.0,
939                      "total": 4.0,
940                      "average": 2.0,
941                      "functions_min": 0.0,
942                      "functions_max": 2.0,
943                      "closures_min": 0.0,
944                      "closures_max": 0.0
945                    }"###
946                );
947            },
948        );
949
950        check_metrics::<CppParser>(
951            "int f(int a, int b) {
952                 if (a) {
953                     return a;
954                }
955             }
956             int f1(int a, int b, int c) {
957                 if (a) {
958                     return a;
959                }
960             }",
961            "foo.c",
962            |metric| {
963                // 2 functions
964                insta::assert_json_snapshot!(
965                    metric.nargs,
966                    @r###"
967                    {
968                      "total_functions": 5.0,
969                      "total_closures": 0.0,
970                      "average_functions": 2.5,
971                      "average_closures": 0.0,
972                      "total": 5.0,
973                      "average": 2.5,
974                      "functions_min": 0.0,
975                      "functions_max": 3.0,
976                      "closures_min": 0.0,
977                      "closures_max": 0.0
978                    }"###
979                );
980            },
981        );
982    }
983
984    #[test]
985    fn javascript_functions() {
986        check_metrics::<JavascriptParser>(
987            "function f(a, b) {
988                 return a * b;
989             }
990             function f1(a, b) {
991                 return a * b;
992             }",
993            "foo.js",
994            |metric| {
995                // 2 functions
996                insta::assert_json_snapshot!(
997                    metric.nargs,
998                    @r###"
999                    {
1000                      "total_functions": 4.0,
1001                      "total_closures": 0.0,
1002                      "average_functions": 2.0,
1003                      "average_closures": 0.0,
1004                      "total": 4.0,
1005                      "average": 2.0,
1006                      "functions_min": 0.0,
1007                      "functions_max": 2.0,
1008                      "closures_min": 0.0,
1009                      "closures_max": 0.0
1010                    }"###
1011                );
1012            },
1013        );
1014
1015        check_metrics::<JavascriptParser>(
1016            "function f(a, b) {
1017                 return a * b;
1018             }
1019             function f1(a, b, c) {
1020                 return a * b;
1021             }",
1022            "foo.js",
1023            |metric| {
1024                // 2 functions
1025                insta::assert_json_snapshot!(
1026                    metric.nargs,
1027                    @r###"
1028                    {
1029                      "total_functions": 5.0,
1030                      "total_closures": 0.0,
1031                      "average_functions": 2.5,
1032                      "average_closures": 0.0,
1033                      "total": 5.0,
1034                      "average": 2.5,
1035                      "functions_min": 0.0,
1036                      "functions_max": 3.0,
1037                      "closures_min": 0.0,
1038                      "closures_max": 0.0
1039                    }"###
1040                );
1041            },
1042        );
1043    }
1044
1045    #[test]
1046    fn python_nested_functions() {
1047        check_metrics::<PythonParser>(
1048            "def f(a, b):
1049                 def foo(a):
1050                     if a:
1051                         return 1
1052                 bar = lambda a: lambda b: b or True or True
1053                 return bar(foo(a))(a)",
1054            "foo.py",
1055            |metric| {
1056                // 2 functions + 2 lambdas = 4
1057                insta::assert_json_snapshot!(
1058                    metric.nargs,
1059                    @r###"
1060                    {
1061                      "total_functions": 3.0,
1062                      "total_closures": 2.0,
1063                      "average_functions": 1.5,
1064                      "average_closures": 1.0,
1065                      "total": 5.0,
1066                      "average": 1.25,
1067                      "functions_min": 0.0,
1068                      "functions_max": 2.0,
1069                      "closures_min": 0.0,
1070                      "closures_max": 2.0
1071                    }"###
1072                );
1073            },
1074        );
1075    }
1076
1077    #[test]
1078    fn rust_nested_functions() {
1079        check_metrics::<RustParser>(
1080            "fn f(a: i32, b: i32) -> i32 {
1081                 fn foo(a: i32) -> i32 {
1082                     return a;
1083                 }
1084                 let bar = |a: i32, b: i32| -> i32 { a + 1 };
1085                 let bar1 = |b: i32| -> i32 { b + 1 };
1086                 return bar(foo(a), a);
1087             }",
1088            "foo.rs",
1089            |metric| {
1090                // 2 functions + 2 lambdas = 4
1091                insta::assert_json_snapshot!(
1092                    metric.nargs,
1093                    @r###"
1094                    {
1095                      "total_functions": 3.0,
1096                      "total_closures": 3.0,
1097                      "average_functions": 1.5,
1098                      "average_closures": 1.5,
1099                      "total": 6.0,
1100                      "average": 1.5,
1101                      "functions_min": 0.0,
1102                      "functions_max": 2.0,
1103                      "closures_min": 0.0,
1104                      "closures_max": 2.0
1105                    }"###
1106                );
1107            },
1108        );
1109    }
1110
1111    #[test]
1112    fn cpp_nested_functions() {
1113        check_metrics::<CppParser>(
1114            "int f(int a, int b, int c) {
1115                 auto foo = [](int x) -> int { return x; };
1116                 auto bar = [](int x, int y) -> int { return x + y; };
1117                 return bar(foo(a), a);
1118             }",
1119            "foo.cpp",
1120            |metric| {
1121                // 1 functions + 2 lambdas = 3
1122                insta::assert_json_snapshot!(
1123                    metric.nargs,
1124                    @r###"
1125                    {
1126                      "total_functions": 3.0,
1127                      "total_closures": 3.0,
1128                      "average_functions": 3.0,
1129                      "average_closures": 1.5,
1130                      "total": 6.0,
1131                      "average": 2.0,
1132                      "functions_min": 0.0,
1133                      "functions_max": 3.0,
1134                      "closures_min": 0.0,
1135                      "closures_max": 3.0
1136                    }"###
1137                );
1138            },
1139        );
1140    }
1141
1142    /// Default arguments still surface as separate `parameter_declaration`
1143    /// nodes — defaults are not removed from the count.  A 3-param function
1144    /// whose third parameter has a default value reports `nargs = 3`.
1145    #[test]
1146    fn cpp_default_arguments() {
1147        check_metrics::<CppParser>(
1148            "int f(int a, int b, int c = 0) {
1149                 return a + b + c;
1150             }",
1151            "foo.cpp",
1152            |metric| {
1153                // 1 function, 3 parameters (defaults still count).
1154                let s = &metric.nargs;
1155                assert_eq!(s.fn_args_sum(), 3.0);
1156                assert_eq!(s.fn_args_max(), 3.0);
1157                insta::assert_json_snapshot!(
1158                    metric.nargs,
1159                    @r###"
1160                    {
1161                      "total_functions": 3.0,
1162                      "total_closures": 0.0,
1163                      "average_functions": 3.0,
1164                      "average_closures": 0.0,
1165                      "total": 3.0,
1166                      "average": 3.0,
1167                      "functions_min": 0.0,
1168                      "functions_max": 3.0,
1169                      "closures_min": 0.0,
1170                      "closures_max": 0.0
1171                    }"###
1172                );
1173            },
1174        );
1175    }
1176
1177    /// C-style variadic `...` parameter contributes +1 (one named declarator
1178    /// plus the `...` declarator).  The grammar emits the variadic ellipsis
1179    /// as a sibling parameter node that `compute_args` counts via the
1180    /// `is_non_arg` filter (which excludes only `(`, `)`, and `,`).
1181    #[test]
1182    fn c_variadic_function() {
1183        check_metrics::<CppParser>(
1184            "int printf(const char* fmt, ...) {
1185                 return 0;
1186             }",
1187            "foo.c",
1188            |metric| {
1189                // 1 function, 2 nargs: `fmt` and `...`
1190                let s = &metric.nargs;
1191                assert_eq!(s.fn_args_sum(), 2.0);
1192                assert_eq!(s.fn_args_max(), 2.0);
1193                insta::assert_json_snapshot!(
1194                    metric.nargs,
1195                    @r###"
1196                    {
1197                      "total_functions": 2.0,
1198                      "total_closures": 0.0,
1199                      "average_functions": 2.0,
1200                      "average_closures": 0.0,
1201                      "total": 2.0,
1202                      "average": 2.0,
1203                      "functions_min": 0.0,
1204                      "functions_max": 2.0,
1205                      "closures_min": 0.0,
1206                      "closures_max": 0.0
1207                    }"###
1208                );
1209            },
1210        );
1211    }
1212
1213    /// C++ template parameter packs (`Args... args`) count as one runtime
1214    /// parameter (the parameter pack itself), not as N — the template
1215    /// arguments are compile-time and live on the template-parameter list,
1216    /// not on `parameters`.  The tree-sitter-cpp grammar represents
1217    /// `Args... args` as a single `variadic_parameter_declaration` under
1218    /// `parameters`.
1219    #[test]
1220    fn cpp_template_parameter_pack() {
1221        check_metrics::<CppParser>(
1222            "template<typename... Args>
1223             int sum(int seed, Args... args) {
1224                 return seed;
1225             }",
1226            "foo.cpp",
1227            |metric| {
1228                // 1 function, 2 nargs: `seed` and `Args... args`
1229                let s = &metric.nargs;
1230                assert_eq!(s.fn_args_sum(), 2.0);
1231                assert_eq!(s.fn_args_max(), 2.0);
1232                insta::assert_json_snapshot!(
1233                    metric.nargs,
1234                    @r###"
1235                    {
1236                      "total_functions": 2.0,
1237                      "total_closures": 0.0,
1238                      "average_functions": 2.0,
1239                      "average_closures": 0.0,
1240                      "total": 2.0,
1241                      "average": 2.0,
1242                      "functions_min": 0.0,
1243                      "functions_max": 2.0,
1244                      "closures_min": 0.0,
1245                      "closures_max": 0.0
1246                    }"###
1247                );
1248            },
1249        );
1250    }
1251
1252    /// Lambda capture list (`[=, &x]`) is not part of the parameter list.
1253    /// `compute_args` reads the `declarator` field, which only contains the
1254    /// `( … )` parameter list.  Variables captured for the closure body do
1255    /// not inflate `nargs`.
1256    #[test]
1257    fn cpp_lambda_capture_not_counted() {
1258        check_metrics::<CppParser>(
1259            "int f() {
1260                 int x = 1;
1261                 int y = 2;
1262                 auto g = [=, &x](int a, int b) -> int { return a + b + x + y; };
1263                 return g(1, 2);
1264             }",
1265            "foo.cpp",
1266            |metric| {
1267                // 1 function (0 args), 1 lambda (2 args: a, b — captures `=, &x` excluded).
1268                let s = &metric.nargs;
1269                assert_eq!(s.fn_args_sum(), 0.0);
1270                assert_eq!(s.closure_args_sum(), 2.0);
1271                assert_eq!(s.closure_args_max(), 2.0);
1272                insta::assert_json_snapshot!(
1273                    metric.nargs,
1274                    @r###"
1275                    {
1276                      "total_functions": 0.0,
1277                      "total_closures": 2.0,
1278                      "average_functions": 0.0,
1279                      "average_closures": 2.0,
1280                      "total": 2.0,
1281                      "average": 1.0,
1282                      "functions_min": 0.0,
1283                      "functions_max": 0.0,
1284                      "closures_min": 0.0,
1285                      "closures_max": 2.0
1286                    }"###
1287                );
1288            },
1289        );
1290    }
1291
1292    /// Implicit `this` on a member function is not part of the AST
1293    /// parameter list — it is an implicit argument at the language level
1294    /// only.  A non-static member function `void M(int a)` reports
1295    /// `nargs = 1`, not 2.
1296    #[test]
1297    fn cpp_member_function_this_not_counted() {
1298        check_metrics::<CppParser>(
1299            "struct S {
1300                 int x;
1301                 int set(int a) {     // implicit `this` is NOT counted
1302                     this->x = a;
1303                     return a;
1304                 }
1305             };",
1306            "foo.cpp",
1307            |metric| {
1308                // 1 member function with 1 explicit parameter `a`.
1309                let s = &metric.nargs;
1310                assert_eq!(s.fn_args_sum(), 1.0);
1311                assert_eq!(s.fn_args_max(), 1.0);
1312                insta::assert_json_snapshot!(
1313                    metric.nargs,
1314                    @r###"
1315                    {
1316                      "total_functions": 1.0,
1317                      "total_closures": 0.0,
1318                      "average_functions": 1.0,
1319                      "average_closures": 0.0,
1320                      "total": 1.0,
1321                      "average": 1.0,
1322                      "functions_min": 0.0,
1323                      "functions_max": 1.0,
1324                      "closures_min": 0.0,
1325                      "closures_max": 0.0
1326                    }"###
1327                );
1328            },
1329        );
1330    }
1331
1332    #[test]
1333    fn go_zero_args() {
1334        check_metrics::<GoParser>(
1335            "package main
1336            func f() {}",
1337            "foo.go",
1338            |metric| {
1339                insta::assert_json_snapshot!(
1340                    metric.nargs,
1341                    @r###"
1342                    {
1343                      "total_functions": 0.0,
1344                      "total_closures": 0.0,
1345                      "average_functions": 0.0,
1346                      "average_closures": 0.0,
1347                      "total": 0.0,
1348                      "average": 0.0,
1349                      "functions_min": 0.0,
1350                      "functions_max": 0.0,
1351                      "closures_min": 0.0,
1352                      "closures_max": 0.0
1353                    }"###
1354                );
1355            },
1356        );
1357    }
1358
1359    #[test]
1360    fn go_multiple_args() {
1361        check_metrics::<GoParser>(
1362            "package main
1363            func f(a int, b string, c bool) {}",
1364            "foo.go",
1365            |metric| {
1366                insta::assert_json_snapshot!(
1367                    metric.nargs,
1368                    @r###"
1369                    {
1370                      "total_functions": 3.0,
1371                      "total_closures": 0.0,
1372                      "average_functions": 3.0,
1373                      "average_closures": 0.0,
1374                      "total": 3.0,
1375                      "average": 3.0,
1376                      "functions_min": 0.0,
1377                      "functions_max": 3.0,
1378                      "closures_min": 0.0,
1379                      "closures_max": 0.0
1380                    }"###
1381                );
1382            },
1383        );
1384    }
1385
1386    #[test]
1387    fn go_method_excludes_receiver() {
1388        check_metrics::<GoParser>(
1389            "package main
1390            type T struct{}
1391            func (t *T) Greet(name string) string {
1392                return name
1393            }",
1394            "foo.go",
1395            |metric| {
1396                // Receiver is in a separate `receiver` field and is not counted.
1397                insta::assert_json_snapshot!(
1398                    metric.nargs,
1399                    @r###"
1400                    {
1401                      "total_functions": 1.0,
1402                      "total_closures": 0.0,
1403                      "average_functions": 1.0,
1404                      "average_closures": 0.0,
1405                      "total": 1.0,
1406                      "average": 1.0,
1407                      "functions_min": 0.0,
1408                      "functions_max": 1.0,
1409                      "closures_min": 0.0,
1410                      "closures_max": 0.0
1411                    }"###
1412                );
1413            },
1414        );
1415    }
1416
1417    #[test]
1418    fn go_variadic() {
1419        check_metrics::<GoParser>(
1420            "package main
1421            func f(args ...int) {}",
1422            "foo.go",
1423            |metric| {
1424                insta::assert_json_snapshot!(
1425                    metric.nargs,
1426                    @r###"
1427                    {
1428                      "total_functions": 1.0,
1429                      "total_closures": 0.0,
1430                      "average_functions": 1.0,
1431                      "average_closures": 0.0,
1432                      "total": 1.0,
1433                      "average": 1.0,
1434                      "functions_min": 0.0,
1435                      "functions_max": 1.0,
1436                      "closures_min": 0.0,
1437                      "closures_max": 0.0
1438                    }"###
1439                );
1440            },
1441        );
1442    }
1443
1444    #[test]
1445    fn go_grouped_params() {
1446        check_metrics::<GoParser>(
1447            "package main
1448            func f(a, b int, c string) {}",
1449            "foo.go",
1450            |metric| {
1451                // `a, b int` is one parameter_declaration with two `name`
1452                // children — semantically two parameters.
1453                insta::assert_json_snapshot!(
1454                    metric.nargs,
1455                    @r###"
1456                    {
1457                      "total_functions": 3.0,
1458                      "total_closures": 0.0,
1459                      "average_functions": 3.0,
1460                      "average_closures": 0.0,
1461                      "total": 3.0,
1462                      "average": 3.0,
1463                      "functions_min": 0.0,
1464                      "functions_max": 3.0,
1465                      "closures_min": 0.0,
1466                      "closures_max": 0.0
1467                    }"###
1468                );
1469            },
1470        );
1471    }
1472
1473    #[test]
1474    fn go_func_literal_args() {
1475        check_metrics::<GoParser>(
1476            "package main
1477            var f = func(x, y int) int { return x + y }",
1478            "foo.go",
1479            |metric| {
1480                // Closure with grouped params: `x, y int` -> 2 closure args.
1481                insta::assert_json_snapshot!(
1482                    metric.nargs,
1483                    @r###"
1484                    {
1485                      "total_functions": 0.0,
1486                      "total_closures": 2.0,
1487                      "average_functions": 0.0,
1488                      "average_closures": 2.0,
1489                      "total": 2.0,
1490                      "average": 2.0,
1491                      "functions_min": 0.0,
1492                      "functions_max": 0.0,
1493                      "closures_min": 0.0,
1494                      "closures_max": 2.0
1495                    }"###
1496                );
1497            },
1498        );
1499    }
1500
1501    #[test]
1502    fn javascript_nested_functions() {
1503        check_metrics::<JavascriptParser>(
1504            "function f(a, b) {
1505                 function foo(a, c) {
1506                     return a;
1507                 }
1508                 var bar = function (a, b) {return a + b};
1509                 function (a) {return a};
1510                 return bar(foo(a), a);
1511             }",
1512            "foo.js",
1513            |metric| {
1514                // 3 functions + 1 lambdas = 4
1515                insta::assert_json_snapshot!(
1516                    metric.nargs,
1517                    @r###"
1518                    {
1519                      "total_functions": 6.0,
1520                      "total_closures": 1.0,
1521                      "average_functions": 2.0,
1522                      "average_closures": 1.0,
1523                      "total": 7.0,
1524                      "average": 1.75,
1525                      "functions_min": 0.0,
1526                      "functions_max": 2.0,
1527                      "closures_min": 0.0,
1528                      "closures_max": 1.0
1529                    }"###
1530                );
1531            },
1532        );
1533    }
1534
1535    #[test]
1536    fn perl_no_functions_and_closures() {
1537        check_metrics::<PerlParser>(
1538            "my $x = 1;
1539             print $x;",
1540            "foo.pl",
1541            |metric| {
1542                // Cross-check via nom that no spurious sub/closure was
1543                // recognised — symmetric with the other `perl_*` nargs
1544                // tests, and would catch a regression that miscounted
1545                // `print` (or similar) as a function.
1546                assert_eq!(metric.nom.functions_sum(), 0.0);
1547                assert_eq!(metric.nom.closures_sum(), 0.0);
1548                insta::assert_json_snapshot!(
1549                    metric.nargs,
1550                    @r#"
1551                {
1552                  "total_functions": 0.0,
1553                  "total_closures": 0.0,
1554                  "average_functions": 0.0,
1555                  "average_closures": 0.0,
1556                  "total": 0.0,
1557                  "average": 0.0,
1558                  "functions_min": 0.0,
1559                  "functions_max": 0.0,
1560                  "closures_min": 0.0,
1561                  "closures_max": 0.0
1562                }
1563                "#
1564                );
1565            },
1566        );
1567    }
1568
1569    #[test]
1570    fn perl_single_function() {
1571        // Perl args arrive via `@_` rather than as formal parameters in the
1572        // `sub` signature, so nargs is always 0. To make sure the test still
1573        // discriminates "function parsed" from "function silently dropped",
1574        // also assert nom recognised exactly one function.
1575        check_metrics::<PerlParser>(
1576            "sub greet {
1577                my ($name) = @_;
1578                print \"hi $name\";
1579            }",
1580            "foo.pl",
1581            |metric| {
1582                assert_eq!(metric.nom.functions_sum(), 1.0);
1583                assert_eq!(metric.nom.closures_sum(), 0.0);
1584                insta::assert_json_snapshot!(
1585                    metric.nargs,
1586                    @r#"
1587                {
1588                  "total_functions": 0.0,
1589                  "total_closures": 0.0,
1590                  "average_functions": 0.0,
1591                  "average_closures": 0.0,
1592                  "total": 0.0,
1593                  "average": 0.0,
1594                  "functions_min": 0.0,
1595                  "functions_max": 0.0,
1596                  "closures_min": 0.0,
1597                  "closures_max": 0.0
1598                }
1599                "#
1600                );
1601            },
1602        );
1603    }
1604
1605    #[test]
1606    fn perl_single_closure() {
1607        // Same caveat as `perl_single_function`: closures take their
1608        // arguments through `@_`, so nargs stays 0. Assert via nom that the
1609        // anonymous function was actually identified as a closure.
1610        check_metrics::<PerlParser>(
1611            "my $f = sub {
1612                my ($x) = @_;
1613                return $x + 1;
1614            };",
1615            "foo.pl",
1616            |metric| {
1617                assert_eq!(metric.nom.functions_sum(), 0.0);
1618                assert_eq!(metric.nom.closures_sum(), 1.0);
1619                insta::assert_json_snapshot!(
1620                    metric.nargs,
1621                    @r#"
1622                {
1623                  "total_functions": 0.0,
1624                  "total_closures": 0.0,
1625                  "average_functions": 0.0,
1626                  "average_closures": 0.0,
1627                  "total": 0.0,
1628                  "average": 0.0,
1629                  "functions_min": 0.0,
1630                  "functions_max": 0.0,
1631                  "closures_min": 0.0,
1632                  "closures_max": 0.0
1633                }
1634                "#
1635                );
1636            },
1637        );
1638    }
1639
1640    #[test]
1641    fn perl_multiple_functions() {
1642        // Same caveat as `perl_single_function`. Assert nom counted both
1643        // top-level subs so the test fails if either sub is dropped.
1644        check_metrics::<PerlParser>(
1645            "sub a { return 1; }
1646             sub b {
1647                 my ($x, $y) = @_;
1648                 return $x + $y;
1649             }",
1650            "foo.pl",
1651            |metric| {
1652                assert_eq!(metric.nom.functions_sum(), 2.0);
1653                assert_eq!(metric.nom.closures_sum(), 0.0);
1654                insta::assert_json_snapshot!(
1655                    metric.nargs,
1656                    @r#"
1657                {
1658                  "total_functions": 0.0,
1659                  "total_closures": 0.0,
1660                  "average_functions": 0.0,
1661                  "average_closures": 0.0,
1662                  "total": 0.0,
1663                  "average": 0.0,
1664                  "functions_min": 0.0,
1665                  "functions_max": 0.0,
1666                  "closures_min": 0.0,
1667                  "closures_max": 0.0
1668                }
1669                "#
1670                );
1671            },
1672        );
1673    }
1674
1675    #[test]
1676    fn perl_nested_closure() {
1677        // Same caveat as `perl_single_function`. Assert nom recognised one
1678        // outer sub plus one nested closure.
1679        check_metrics::<PerlParser>(
1680            "sub outer {
1681                my $inner = sub { return 42; };
1682                return $inner->();
1683            }",
1684            "foo.pl",
1685            |metric| {
1686                assert_eq!(metric.nom.functions_sum(), 1.0);
1687                assert_eq!(metric.nom.closures_sum(), 1.0);
1688                insta::assert_json_snapshot!(
1689                    metric.nargs,
1690                    @r#"
1691                {
1692                  "total_functions": 0.0,
1693                  "total_closures": 0.0,
1694                  "average_functions": 0.0,
1695                  "average_closures": 0.0,
1696                  "total": 0.0,
1697                  "average": 0.0,
1698                  "functions_min": 0.0,
1699                  "functions_max": 0.0,
1700                  "closures_min": 0.0,
1701                  "closures_max": 0.0
1702                }
1703                "#
1704                );
1705            },
1706        );
1707    }
1708
1709    #[test]
1710    fn java_no_functions() {
1711        check_metrics::<JavaParser>(
1712            "class Foo {
1713                 int x = 42;
1714                 String name = \"hello\";
1715             }",
1716            "foo.java",
1717            |metric| {
1718                insta::assert_json_snapshot!(
1719                    metric.nargs,
1720                    @r#"
1721                    {
1722                      "total_functions": 0.0,
1723                      "total_closures": 0.0,
1724                      "average_functions": 0.0,
1725                      "average_closures": 0.0,
1726                      "total": 0.0,
1727                      "average": 0.0,
1728                      "functions_min": 0.0,
1729                      "functions_max": 0.0,
1730                      "closures_min": 0.0,
1731                      "closures_max": 0.0
1732                    }
1733                    "#
1734                );
1735            },
1736        );
1737    }
1738
1739    #[test]
1740    fn java_single_method() {
1741        check_metrics::<JavaParser>(
1742            "class Foo {
1743                 void greet(String name, int count) {
1744                     return;
1745                 }
1746             }",
1747            "foo.java",
1748            |metric| {
1749                insta::assert_json_snapshot!(
1750                    metric.nargs,
1751                    @r#"
1752                {
1753                  "total_functions": 2.0,
1754                  "total_closures": 0.0,
1755                  "average_functions": 2.0,
1756                  "average_closures": 0.0,
1757                  "total": 2.0,
1758                  "average": 2.0,
1759                  "functions_min": 0.0,
1760                  "functions_max": 2.0,
1761                  "closures_min": 0.0,
1762                  "closures_max": 0.0
1763                }
1764                "#
1765                );
1766            },
1767        );
1768    }
1769
1770    #[test]
1771    fn java_multiple_methods() {
1772        check_metrics::<JavaParser>(
1773            "class Foo {
1774                 void a(int x) {
1775                     return;
1776                 }
1777                 void b(int x, int y, int z) {
1778                     return;
1779                 }
1780             }",
1781            "foo.java",
1782            |metric| {
1783                insta::assert_json_snapshot!(
1784                    metric.nargs,
1785                    @r#"
1786                {
1787                  "total_functions": 4.0,
1788                  "total_closures": 0.0,
1789                  "average_functions": 2.0,
1790                  "average_closures": 0.0,
1791                  "total": 4.0,
1792                  "average": 2.0,
1793                  "functions_min": 0.0,
1794                  "functions_max": 3.0,
1795                  "closures_min": 0.0,
1796                  "closures_max": 0.0
1797                }
1798                "#
1799                );
1800            },
1801        );
1802    }
1803
1804    #[test]
1805    fn java_constructor_args() {
1806        check_metrics::<JavaParser>(
1807            "class Foo {
1808                 Foo(String name, int age) {
1809                     return;
1810                 }
1811             }",
1812            "foo.java",
1813            |metric| {
1814                insta::assert_json_snapshot!(
1815                    metric.nargs,
1816                    @r#"
1817                {
1818                  "total_functions": 2.0,
1819                  "total_closures": 0.0,
1820                  "average_functions": 2.0,
1821                  "average_closures": 0.0,
1822                  "total": 2.0,
1823                  "average": 2.0,
1824                  "functions_min": 0.0,
1825                  "functions_max": 2.0,
1826                  "closures_min": 0.0,
1827                  "closures_max": 0.0
1828                }
1829                "#
1830                );
1831            },
1832        );
1833    }
1834
1835    #[test]
1836    fn java_lambda_args() {
1837        check_metrics::<JavaParser>(
1838            "class Foo {
1839                 void run() {
1840                     Runnable r = (int a, int b) -> a + b;
1841                 }
1842             }",
1843            "foo.java",
1844            |metric| {
1845                insta::assert_json_snapshot!(
1846                    metric.nargs,
1847                    @r#"
1848                {
1849                  "total_functions": 0.0,
1850                  "total_closures": 2.0,
1851                  "average_functions": 0.0,
1852                  "average_closures": 2.0,
1853                  "total": 2.0,
1854                  "average": 1.0,
1855                  "functions_min": 0.0,
1856                  "functions_max": 0.0,
1857                  "closures_min": 0.0,
1858                  "closures_max": 2.0
1859                }
1860                "#
1861                );
1862            },
1863        );
1864    }
1865
1866    #[test]
1867    fn groovy_no_functions_and_closures() {
1868        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
1869            assert_eq!(metric.nargs.nargs_total(), 0.0);
1870        });
1871    }
1872
1873    #[test]
1874    fn groovy_single_method() {
1875        check_metrics::<GroovyParser>(
1876            "class A {
1877                void greet(String name, int times) {
1878                    println(name)
1879                }
1880            }",
1881            "foo.groovy",
1882            |metric| {
1883                assert_eq!(metric.nargs.fn_args_sum(), 2.0);
1884                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
1885            },
1886        );
1887    }
1888
1889    #[test]
1890    fn groovy_multiple_methods() {
1891        check_metrics::<GroovyParser>(
1892            "class A {
1893                int add(int x, int y) { x + y }
1894                int sub(int x, int y, int z) { x - y - z }
1895            }",
1896            "foo.groovy",
1897            |metric| {
1898                assert_eq!(metric.nargs.fn_args_sum(), 5.0);
1899            },
1900        );
1901    }
1902
1903    #[test]
1904    fn groovy_lambda_args() {
1905        // Two-parameter Groovy closure inside a method body. Groovy's
1906        // primary lambda-shaped construct is the closure
1907        // (`{ params -> body }`); the dekobon grammar does not model
1908        // Java's `(params) -> body` arrow form because real-world
1909        // Groovy code rarely uses it.
1910        check_metrics::<GroovyParser>(
1911            "class Foo {
1912                void run() {
1913                    def f = { int a, int b -> a + b }
1914                }
1915            }",
1916            "foo.groovy",
1917            |metric| {
1918                assert_eq!(metric.nargs.closure_args_sum(), 2.0);
1919            },
1920        );
1921    }
1922
1923    #[test]
1924    fn groovy_implicit_it_not_counted() {
1925        // The `it` implicit closure parameter is just an identifier in
1926        // the grammar — no `formal_parameters` node. `nargs` counts
1927        // declared parameters only, so this closure has 0.
1928        check_metrics::<GroovyParser>(
1929            "class A {
1930                void apply() {
1931                    [1, 2, 3].each { println(it) }
1932                }
1933            }",
1934            "foo.groovy",
1935            |metric| {
1936                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
1937            },
1938        );
1939    }
1940
1941    #[test]
1942    fn csharp_no_functions() {
1943        check_metrics::<CsharpParser>(
1944            "class Foo {
1945                 int x = 42;
1946                 string Name = \"hello\";
1947             }",
1948            "foo.cs",
1949            |metric| {
1950                insta::assert_json_snapshot!(
1951                    metric.nargs,
1952                    @r#"
1953                    {
1954                      "total_functions": 0.0,
1955                      "total_closures": 0.0,
1956                      "average_functions": 0.0,
1957                      "average_closures": 0.0,
1958                      "total": 0.0,
1959                      "average": 0.0,
1960                      "functions_min": 0.0,
1961                      "functions_max": 0.0,
1962                      "closures_min": 0.0,
1963                      "closures_max": 0.0
1964                    }
1965                    "#
1966                );
1967            },
1968        );
1969    }
1970
1971    #[test]
1972    fn csharp_single_method() {
1973        check_metrics::<CsharpParser>(
1974            "class Foo {
1975                 void Greet(string name, int count) {
1976                     return;
1977                 }
1978             }",
1979            "foo.cs",
1980            |metric| {
1981                insta::assert_json_snapshot!(
1982                    metric.nargs,
1983                    @r#"
1984                {
1985                  "total_functions": 2.0,
1986                  "total_closures": 0.0,
1987                  "average_functions": 2.0,
1988                  "average_closures": 0.0,
1989                  "total": 2.0,
1990                  "average": 2.0,
1991                  "functions_min": 0.0,
1992                  "functions_max": 2.0,
1993                  "closures_min": 0.0,
1994                  "closures_max": 0.0
1995                }
1996                "#
1997                );
1998            },
1999        );
2000    }
2001
2002    #[test]
2003    fn csharp_multiple_methods() {
2004        check_metrics::<CsharpParser>(
2005            "class Foo {
2006                 void A(int x) {
2007                     return;
2008                 }
2009                 void B(int x, int y, int z) {
2010                     return;
2011                 }
2012             }",
2013            "foo.cs",
2014            |metric| {
2015                insta::assert_json_snapshot!(
2016                    metric.nargs,
2017                    @r#"
2018                {
2019                  "total_functions": 4.0,
2020                  "total_closures": 0.0,
2021                  "average_functions": 2.0,
2022                  "average_closures": 0.0,
2023                  "total": 4.0,
2024                  "average": 2.0,
2025                  "functions_min": 0.0,
2026                  "functions_max": 3.0,
2027                  "closures_min": 0.0,
2028                  "closures_max": 0.0
2029                }
2030                "#
2031                );
2032            },
2033        );
2034    }
2035
2036    #[test]
2037    fn csharp_constructor_args() {
2038        check_metrics::<CsharpParser>(
2039            "class Foo {
2040                 public Foo(string name, int age) {
2041                     return;
2042                 }
2043             }",
2044            "foo.cs",
2045            |metric| {
2046                insta::assert_json_snapshot!(
2047                    metric.nargs,
2048                    @r#"
2049                {
2050                  "total_functions": 2.0,
2051                  "total_closures": 0.0,
2052                  "average_functions": 2.0,
2053                  "average_closures": 0.0,
2054                  "total": 2.0,
2055                  "average": 2.0,
2056                  "functions_min": 0.0,
2057                  "functions_max": 2.0,
2058                  "closures_min": 0.0,
2059                  "closures_max": 0.0
2060                }
2061                "#
2062                );
2063            },
2064        );
2065    }
2066
2067    #[test]
2068    fn csharp_lambda_args() {
2069        check_metrics::<CsharpParser>(
2070            "class Foo {
2071                 void Run() {
2072                     System.Func<int, int, int> f = (int a, int b) => a + b;
2073                 }
2074             }",
2075            "foo.cs",
2076            |metric| {
2077                insta::assert_json_snapshot!(
2078                    metric.nargs,
2079                    @r#"
2080                {
2081                  "total_functions": 0.0,
2082                  "total_closures": 2.0,
2083                  "average_functions": 0.0,
2084                  "average_closures": 2.0,
2085                  "total": 2.0,
2086                  "average": 1.0,
2087                  "functions_min": 0.0,
2088                  "functions_max": 0.0,
2089                  "closures_min": 0.0,
2090                  "closures_max": 2.0
2091                }
2092                "#
2093                );
2094            },
2095        );
2096    }
2097
2098    #[test]
2099    fn tsx_function_and_arrow() {
2100        check_metrics::<TsxParser>(
2101            "function add(a: number, b: number): number {
2102                 return a + b;
2103             }
2104             const multiply = (x: number, y: number) => x * y;",
2105            "foo.tsx",
2106            |metric| {
2107                insta::assert_json_snapshot!(
2108                    metric.nargs,
2109                    @r###"
2110                    {
2111                      "total_functions": 4.0,
2112                      "total_closures": 0.0,
2113                      "average_functions": 2.0,
2114                      "average_closures": 0.0,
2115                      "total": 4.0,
2116                      "average": 2.0,
2117                      "functions_min": 0.0,
2118                      "functions_max": 2.0,
2119                      "closures_min": 0.0,
2120                      "closures_max": 0.0
2121                    }"###
2122                );
2123            },
2124        );
2125    }
2126
2127    #[test]
2128    fn typescript_typed_and_optional_params() {
2129        check_metrics::<TypescriptParser>(
2130            "function format(value: number, prefix?: string, suffix?: string): string {
2131                 return (prefix ?? '') + value.toString() + (suffix ?? '');
2132             }
2133             const identity = (x: number): number => x;",
2134            "foo.ts",
2135            |metric| {
2136                insta::assert_json_snapshot!(
2137                    metric.nargs,
2138                    @r###"
2139                    {
2140                      "total_functions": 4.0,
2141                      "total_closures": 0.0,
2142                      "average_functions": 2.0,
2143                      "average_closures": 0.0,
2144                      "total": 4.0,
2145                      "average": 2.0,
2146                      "functions_min": 0.0,
2147                      "functions_max": 3.0,
2148                      "closures_min": 0.0,
2149                      "closures_max": 0.0
2150                    }"###
2151                );
2152            },
2153        );
2154    }
2155
2156    #[test]
2157    fn mozjs_single_function() {
2158        check_metrics::<MozjsParser>(
2159            "function f(a, b) {
2160                 return a * b;
2161             }",
2162            "foo.js",
2163            |metric| {
2164                insta::assert_json_snapshot!(
2165                    metric.nargs,
2166                    @r###"
2167                    {
2168                      "total_functions": 2.0,
2169                      "total_closures": 0.0,
2170                      "average_functions": 2.0,
2171                      "average_closures": 0.0,
2172                      "total": 2.0,
2173                      "average": 2.0,
2174                      "functions_min": 0.0,
2175                      "functions_max": 2.0,
2176                      "closures_min": 0.0,
2177                      "closures_max": 0.0
2178                    }"###
2179                );
2180            },
2181        );
2182    }
2183
2184    #[test]
2185    fn mozjs_closure_args() {
2186        check_metrics::<MozjsParser>("function (a, b) {return a + b};", "foo.js", |metric| {
2187            insta::assert_json_snapshot!(
2188                metric.nargs,
2189                @r###"
2190                    {
2191                      "total_functions": 0.0,
2192                      "total_closures": 2.0,
2193                      "average_functions": 0.0,
2194                      "average_closures": 2.0,
2195                      "total": 2.0,
2196                      "average": 2.0,
2197                      "functions_min": 0.0,
2198                      "functions_max": 0.0,
2199                      "closures_min": 0.0,
2200                      "closures_max": 2.0
2201                    }"###
2202            );
2203        });
2204    }
2205
2206    // Regression tests for issue #77: bare-identifier arrow functions
2207    // (`x => x`) use the singular `parameter` field instead of the plural
2208    // `parameters` field, and were previously counted as nargs=0.
2209    //
2210    // `nargs_total` is used so the assertion is independent of whether the
2211    // arrow function is classified as a function or a closure (this depends
2212    // on its enclosing context — e.g. a `VariableDeclarator` ancestor makes
2213    // it a function).
2214
2215    #[test]
2216    fn javascript_bare_arrow_function() {
2217        check_metrics::<JavascriptParser>("const f = x => x;", "foo.js", |metric| {
2218            assert_eq!(metric.nargs.nargs_total(), 1.0);
2219        });
2220    }
2221
2222    #[test]
2223    fn javascript_async_bare_arrow_function() {
2224        check_metrics::<JavascriptParser>("const f = async x => x;", "foo.js", |metric| {
2225            assert_eq!(metric.nargs.nargs_total(), 1.0);
2226        });
2227    }
2228
2229    #[test]
2230    fn javascript_parenthesized_arrow_function() {
2231        check_metrics::<JavascriptParser>("const f = (x) => x;", "foo.js", |metric| {
2232            assert_eq!(metric.nargs.nargs_total(), 1.0);
2233        });
2234    }
2235
2236    #[test]
2237    fn javascript_multi_parenthesized_arrow_function() {
2238        check_metrics::<JavascriptParser>("const f = (x, y) => x + y;", "foo.js", |metric| {
2239            assert_eq!(metric.nargs.nargs_total(), 2.0);
2240        });
2241    }
2242
2243    #[test]
2244    fn typescript_bare_arrow_function() {
2245        check_metrics::<TypescriptParser>("const f = x => x;", "foo.ts", |metric| {
2246            assert_eq!(metric.nargs.nargs_total(), 1.0);
2247        });
2248    }
2249
2250    #[test]
2251    fn typescript_async_bare_arrow_function() {
2252        check_metrics::<TypescriptParser>("const f = async x => x;", "foo.ts", |metric| {
2253            assert_eq!(metric.nargs.nargs_total(), 1.0);
2254        });
2255    }
2256
2257    #[test]
2258    fn typescript_parenthesized_arrow_function() {
2259        check_metrics::<TypescriptParser>("const f = (x: number) => x;", "foo.ts", |metric| {
2260            assert_eq!(metric.nargs.nargs_total(), 1.0);
2261        });
2262    }
2263
2264    #[test]
2265    fn typescript_multi_parenthesized_arrow_function() {
2266        check_metrics::<TypescriptParser>(
2267            "const f = (x: number, y: number) => x + y;",
2268            "foo.ts",
2269            |metric| {
2270                assert_eq!(metric.nargs.nargs_total(), 2.0);
2271            },
2272        );
2273    }
2274
2275    #[test]
2276    fn tsx_bare_arrow_function() {
2277        check_metrics::<TsxParser>("const f = x => x;", "foo.tsx", |metric| {
2278            assert_eq!(metric.nargs.nargs_total(), 1.0);
2279        });
2280    }
2281
2282    #[test]
2283    fn tsx_async_bare_arrow_function() {
2284        check_metrics::<TsxParser>("const f = async x => x;", "foo.tsx", |metric| {
2285            assert_eq!(metric.nargs.nargs_total(), 1.0);
2286        });
2287    }
2288
2289    #[test]
2290    fn tsx_parenthesized_arrow_function() {
2291        check_metrics::<TsxParser>("const f = (x: number) => x;", "foo.tsx", |metric| {
2292            assert_eq!(metric.nargs.nargs_total(), 1.0);
2293        });
2294    }
2295
2296    #[test]
2297    fn tsx_multi_parenthesized_arrow_function() {
2298        check_metrics::<TsxParser>(
2299            "const f = (x: number, y: number) => x + y;",
2300            "foo.tsx",
2301            |metric| {
2302                assert_eq!(metric.nargs.nargs_total(), 2.0);
2303            },
2304        );
2305    }
2306
2307    #[test]
2308    fn mozjs_bare_arrow_function() {
2309        check_metrics::<MozjsParser>("const f = x => x;", "foo.js", |metric| {
2310            assert_eq!(metric.nargs.nargs_total(), 1.0);
2311        });
2312    }
2313
2314    #[test]
2315    fn mozjs_async_bare_arrow_function() {
2316        check_metrics::<MozjsParser>("const f = async x => x;", "foo.js", |metric| {
2317            assert_eq!(metric.nargs.nargs_total(), 1.0);
2318        });
2319    }
2320
2321    #[test]
2322    fn mozjs_parenthesized_arrow_function() {
2323        check_metrics::<MozjsParser>("const f = (x) => x;", "foo.js", |metric| {
2324            assert_eq!(metric.nargs.nargs_total(), 1.0);
2325        });
2326    }
2327
2328    #[test]
2329    fn mozjs_multi_parenthesized_arrow_function() {
2330        check_metrics::<MozjsParser>("const f = (x, y) => x + y;", "foo.js", |metric| {
2331            assert_eq!(metric.nargs.nargs_total(), 2.0);
2332        });
2333    }
2334
2335    #[test]
2336    fn kotlin_nargs_functions_and_closures() {
2337        check_metrics::<KotlinParser>(
2338            "fun add(a: Int, b: Int): Int {
2339                val transform = { x: Int, y: Int -> x + y }
2340                return transform(a, b)
2341            }",
2342            "foo.kt",
2343            |metric| {
2344                insta::assert_json_snapshot!(
2345                    metric.nargs,
2346                    @r###"
2347                    {
2348                      "total_functions": 2.0,
2349                      "total_closures": 2.0,
2350                      "average_functions": 2.0,
2351                      "average_closures": 2.0,
2352                      "total": 4.0,
2353                      "average": 2.0,
2354                      "functions_min": 0.0,
2355                      "functions_max": 2.0,
2356                      "closures_min": 0.0,
2357                      "closures_max": 2.0
2358                    }
2359                    "###
2360                );
2361            },
2362        );
2363    }
2364
2365    #[test]
2366    fn lua_no_functions_and_closures() {
2367        check_metrics::<LuaParser>("local x = 1", "foo.lua", |metric| {
2368            // No functions or closures: both halves are zero.
2369            assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2370            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2371            insta::assert_json_snapshot!(metric.nargs);
2372        });
2373    }
2374
2375    #[test]
2376    fn lua_single_function() {
2377        check_metrics::<LuaParser>("function f(a, b) return a + b end", "foo.lua", |metric| {
2378            // f(a, b) → fn_args_sum 2, no closures.
2379            assert_eq!(metric.nargs.fn_args_sum(), 2.0);
2380            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2381            insta::assert_json_snapshot!(metric.nargs);
2382        });
2383    }
2384
2385    #[test]
2386    fn lua_single_closure() {
2387        check_metrics::<LuaParser>(
2388            "local f = function(a, b) return a + b end",
2389            "foo.lua",
2390            |metric| {
2391                // Anonymous `function(a, b)` bound via `local` → closure_args_sum 2.
2392                assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2393                assert_eq!(metric.nargs.closure_args_sum(), 2.0);
2394                insta::assert_json_snapshot!(metric.nargs);
2395            },
2396        );
2397    }
2398
2399    #[test]
2400    fn lua_functions() {
2401        check_metrics::<LuaParser>(
2402            "function f(a) return a end
2403function g(x, y, z) return x + y + z end",
2404            "foo.lua",
2405            |metric| {
2406                // f(a)=1 + g(x,y,z)=3 → fn_args_sum 4.
2407                assert_eq!(metric.nargs.fn_args_sum(), 4.0);
2408                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2409                insta::assert_json_snapshot!(metric.nargs);
2410            },
2411        );
2412    }
2413
2414    #[test]
2415    fn lua_vararg_function() {
2416        // `...` is a vararg_expression node and counts as one argument.
2417        check_metrics::<LuaParser>("function f(a, ...) return a end", "foo.lua", |metric| {
2418            // a + ... → fn_args_sum 2.
2419            assert_eq!(metric.nargs.fn_args_sum(), 2.0);
2420            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2421            insta::assert_json_snapshot!(metric.nargs);
2422        });
2423    }
2424
2425    #[test]
2426    fn lua_colon_method_nargs() {
2427        // Colon syntax: `self` is implicit and NOT in the `parameters` node.
2428        // Only explicit params (a, b) are counted.
2429        check_metrics::<LuaParser>(
2430            "function obj:method(a, b) return a + b end",
2431            "foo.lua",
2432            |metric| {
2433                // Only explicit a, b → fn_args_sum 2 (implicit self excluded).
2434                assert_eq!(metric.nargs.fn_args_sum(), 2.0);
2435                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2436                insta::assert_json_snapshot!(metric.nargs);
2437            },
2438        );
2439    }
2440
2441    #[test]
2442    fn tcl_no_functions() {
2443        check_metrics::<TclParser>("set x 1", "foo.tcl", |metric| {
2444            // Bare `set` command, no procs → both halves zero.
2445            assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2446            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2447            insta::assert_json_snapshot!(metric.nargs);
2448        });
2449    }
2450
2451    #[test]
2452    fn tcl_single_function() {
2453        check_metrics::<TclParser>("proc f {a b} { puts $a }", "foo.tcl", |metric| {
2454            // proc f {a b} → fn_args_sum 2.
2455            assert_eq!(metric.nargs.fn_args_sum(), 2.0);
2456            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2457            insta::assert_json_snapshot!(metric.nargs);
2458        });
2459    }
2460
2461    #[test]
2462    fn tcl_single_function_no_args() {
2463        check_metrics::<TclParser>("proc f {} { puts hello }", "foo.tcl", |metric| {
2464            // proc f {} → empty arg list, fn_args_sum 0.
2465            assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2466            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2467            insta::assert_json_snapshot!(metric.nargs);
2468        });
2469    }
2470
2471    #[test]
2472    fn tcl_functions() {
2473        check_metrics::<TclParser>(
2474            "proc f {a b} { puts $a }
2475proc g {x y z} { puts $x }",
2476            "foo.tcl",
2477            |metric| {
2478                // f(a,b)=2 + g(x,y,z)=3 → fn_args_sum 5.
2479                assert_eq!(metric.nargs.fn_args_sum(), 5.0);
2480                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2481                insta::assert_json_snapshot!(metric.nargs);
2482            },
2483        );
2484    }
2485
2486    #[test]
2487    fn tcl_nested_functions() {
2488        check_metrics::<TclParser>(
2489            "proc outer {a} {
2490    proc inner {x y} { puts $x }
2491    inner $a $a
2492}",
2493            "foo.tcl",
2494            |metric| {
2495                // outer(a)=1 + inner(x,y)=2 → fn_args_sum 3.
2496                assert_eq!(metric.nargs.fn_args_sum(), 3.0);
2497                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2498                insta::assert_json_snapshot!(metric.nargs);
2499            },
2500        );
2501    }
2502
2503    #[test]
2504    fn tcl_args_vararg() {
2505        // `args` is the Tcl variadic catch-all; it counts as one argument.
2506        check_metrics::<TclParser>("proc f {a b args} { puts $a }", "foo.tcl", |metric| {
2507            // a + b + args → fn_args_sum 3 (variadic is one slot).
2508            assert_eq!(metric.nargs.fn_args_sum(), 3.0);
2509            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2510            insta::assert_json_snapshot!(metric.nargs);
2511        });
2512    }
2513
2514    #[test]
2515    fn tcl_default_arg() {
2516        // `{name default}` is a single argument with a default value.
2517        check_metrics::<TclParser>(
2518            "proc greet {{name World} greeting} {
2519    puts \"$greeting, $name!\"
2520}",
2521            "foo.tcl",
2522            |metric| {
2523                // {name World} counts as one slot + greeting → fn_args_sum 2.
2524                assert_eq!(metric.nargs.fn_args_sum(), 2.0);
2525                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2526                insta::assert_json_snapshot!(metric.nargs);
2527            },
2528        );
2529    }
2530
2531    #[test]
2532    fn kotlin_zero_args() {
2533        check_metrics::<KotlinParser>("fun f(): Int { return 42 }", "foo.kt", |metric| {
2534            // fun f() → empty parameter list, fn_args_sum 0.
2535            assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2536            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2537            insta::assert_json_snapshot!(metric.nargs);
2538        });
2539    }
2540
2541    #[test]
2542    fn kotlin_single_arg() {
2543        check_metrics::<KotlinParser>(
2544            "fun double(x: Int): Int { return x * 2 }",
2545            "foo.kt",
2546            |metric| {
2547                // double(x) → fn_args_sum 1.
2548                assert_eq!(metric.nargs.fn_args_sum(), 1.0);
2549                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2550                insta::assert_json_snapshot!(metric.nargs);
2551            },
2552        );
2553    }
2554
2555    #[test]
2556    fn kotlin_multiple_args() {
2557        check_metrics::<KotlinParser>(
2558            "fun add(a: Int, b: Int, c: Int): Int { return a + b + c }",
2559            "foo.kt",
2560            |metric| {
2561                // add(a, b, c) → fn_args_sum 3.
2562                assert_eq!(metric.nargs.fn_args_sum(), 3.0);
2563                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2564                insta::assert_json_snapshot!(metric.nargs);
2565            },
2566        );
2567    }
2568
2569    #[test]
2570    fn kotlin_default_args() {
2571        check_metrics::<KotlinParser>(
2572            "fun greet(name: String = \"World\", greeting: String = \"Hello\"): String {
2573                 return \"$greeting, $name!\"
2574             }",
2575            "foo.kt",
2576            |metric| {
2577                // Defaults still count as parameter slots → fn_args_sum 2.
2578                assert_eq!(metric.nargs.fn_args_sum(), 2.0);
2579                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2580                insta::assert_json_snapshot!(metric.nargs);
2581            },
2582        );
2583    }
2584
2585    #[test]
2586    fn kotlin_empty_lambda() {
2587        // Two lambdas in the same function body: one with two explicit parameters
2588        // (proving the lambda path is taken and args are counted), and one with an
2589        // explicit empty parameter list `{ -> expr }` (proving
2590        // `compute_kotlin_lambda_args` returns 0 for it without crashing or
2591        // accidentally counting tokens inside the arrow expression).
2592        // If the grammar fails to parse either lambda, `total_closures` would be
2593        // lower than 2, making the snapshot unambiguous.
2594        check_metrics::<KotlinParser>(
2595            "fun f() {
2596                 val two = { x: Int, y: Int -> x + y }
2597                 val zero = { -> 42 }
2598             }",
2599            "foo.kt",
2600            |metric| {
2601                // Outer fun f() has 0 params; two lambdas counted as closures:
2602                // {x, y -> ...} contributes 2, {-> 42} contributes 0 →
2603                // closure_args_sum 2 across two closure entries.
2604                assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2605                assert_eq!(metric.nargs.closure_args_sum(), 2.0);
2606                insta::assert_json_snapshot!(metric.nargs);
2607            },
2608        );
2609    }
2610
2611    #[test]
2612    fn kotlin_anonymous_function() {
2613        // `fun(x: Int, y: Int) = x + y` — anonymous function expression.
2614        // The grammar surfaces it as an `AnonymousFunction` node, which routes
2615        // through `compute_kotlin_func_args` (not the lambda path).
2616        check_metrics::<KotlinParser>(
2617            "val add = fun(x: Int, y: Int): Int = x + y",
2618            "foo.kt",
2619            |metric| {
2620                // Anonymous fun(x, y) is classified as a closure → closure_args_sum 2.
2621                assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2622                assert_eq!(metric.nargs.closure_args_sum(), 2.0);
2623                insta::assert_json_snapshot!(metric.nargs);
2624            },
2625        );
2626    }
2627
2628    #[test]
2629    fn php_no_functions_and_closures() {
2630        check_metrics::<PhpParser>("<?php $a = 42;", "foo.php", |metric| {
2631            insta::assert_json_snapshot!(
2632                metric.nargs,
2633                @r###"
2634                {
2635                  "total_functions": 0.0,
2636                  "total_closures": 0.0,
2637                  "average_functions": 0.0,
2638                  "average_closures": 0.0,
2639                  "total": 0.0,
2640                  "average": 0.0,
2641                  "functions_min": 0.0,
2642                  "functions_max": 0.0,
2643                  "closures_min": 0.0,
2644                  "closures_max": 0.0
2645                }"###
2646            );
2647        });
2648    }
2649
2650    #[test]
2651    fn php_single_function() {
2652        // Two parameters in a regular function.
2653        check_metrics::<PhpParser>(
2654            "<?php
2655            function f(bool $a, int $b): bool {
2656                if ($a) { return $a; }
2657                return false;
2658            }",
2659            "foo.php",
2660            |metric| {
2661                insta::assert_json_snapshot!(
2662                    metric.nargs,
2663                    @r###"
2664                    {
2665                      "total_functions": 2.0,
2666                      "total_closures": 0.0,
2667                      "average_functions": 2.0,
2668                      "average_closures": 0.0,
2669                      "total": 2.0,
2670                      "average": 2.0,
2671                      "functions_min": 0.0,
2672                      "functions_max": 2.0,
2673                      "closures_min": 0.0,
2674                      "closures_max": 0.0
2675                    }"###
2676                );
2677            },
2678        );
2679    }
2680
2681    #[test]
2682    fn php_single_closure() {
2683        // Anonymous function with 2 params + arrow function with 1 param.
2684        // Each is a separate closure space.
2685        check_metrics::<PhpParser>(
2686            "<?php
2687            $f = function (int $a, int $b) { return $a + $b; };
2688            $g = fn (int $x) => $x * 2;",
2689            "foo.php",
2690            |metric| {
2691                insta::assert_json_snapshot!(
2692                    metric.nargs,
2693                    @r###"
2694                    {
2695                      "total_functions": 0.0,
2696                      "total_closures": 3.0,
2697                      "average_functions": 0.0,
2698                      "average_closures": 1.5,
2699                      "total": 3.0,
2700                      "average": 1.5,
2701                      "functions_min": 0.0,
2702                      "functions_max": 0.0,
2703                      "closures_min": 0.0,
2704                      "closures_max": 2.0
2705                    }"###
2706                );
2707            },
2708        );
2709    }
2710
2711    #[test]
2712    fn php_functions() {
2713        // Two top-level functions, 1 + 2 args.
2714        check_metrics::<PhpParser>(
2715            "<?php
2716            function a(int $x): int { return $x; }
2717            function b(int $x, int $y): int { return $x + $y; }",
2718            "foo.php",
2719            |metric| {
2720                insta::assert_json_snapshot!(
2721                    metric.nargs,
2722                    @r###"
2723                    {
2724                      "total_functions": 3.0,
2725                      "total_closures": 0.0,
2726                      "average_functions": 1.5,
2727                      "average_closures": 0.0,
2728                      "total": 3.0,
2729                      "average": 1.5,
2730                      "functions_min": 0.0,
2731                      "functions_max": 2.0,
2732                      "closures_min": 0.0,
2733                      "closures_max": 0.0
2734                    }"###
2735                );
2736            },
2737        );
2738    }
2739
2740    #[test]
2741    fn php_nested_functions() {
2742        // PHP cannot define nested named functions inside a function body
2743        // syntactically, but a class with methods exhibits the same shape:
2744        // a top-level scope plus inner function-spaces.
2745        check_metrics::<PhpParser>(
2746            "<?php
2747            class A {
2748                public function outer(int $a): int {
2749                    $f = function (int $b) use ($a) { return $a + $b; };
2750                    return $f($a);
2751                }
2752            }",
2753            "foo.php",
2754            |metric| {
2755                insta::assert_json_snapshot!(
2756                    metric.nargs,
2757                    @r###"
2758                    {
2759                      "total_functions": 1.0,
2760                      "total_closures": 1.0,
2761                      "average_functions": 1.0,
2762                      "average_closures": 1.0,
2763                      "total": 2.0,
2764                      "average": 1.0,
2765                      "functions_min": 0.0,
2766                      "functions_max": 1.0,
2767                      "closures_min": 0.0,
2768                      "closures_max": 1.0
2769                    }"###
2770                );
2771            },
2772        );
2773    }
2774
2775    #[test]
2776    fn elixir_default_nargs_is_zero() {
2777        // Documents Elixir's current default-impl behaviour. `def` calls
2778        // are not recognised as functions (`is_func` returns `false`),
2779        // and Elixir anonymous functions hold their parameters inside
2780        // a `stab_clause` (not a parameter-list field), so the default
2781        // `NArgs::compute` heuristic finds no formal parameters to
2782        // count. This anchors the limitation so a future real impl
2783        // that wires up `stab_clause`-based argument counting cannot
2784        // silently regress to zero.
2785        check_metrics::<ElixirParser>(
2786            "defmodule Foo do\n  def add(a, b), do: a + b\n  def use_anon do\n    add2 = fn x, y -> x + y end\n    add2.(1, 2)\n  end\nend\n",
2787            "foo.ex",
2788            |metric| {
2789                assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2790                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2791            },
2792        );
2793    }
2794
2795    #[test]
2796    fn ruby_no_functions_and_closures() {
2797        check_metrics::<RubyParser>("a = 42\n", "foo.rb", |metric| {
2798            assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2799            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2800        });
2801    }
2802
2803    #[test]
2804    fn ruby_single_function() {
2805        // Single method with 3 parameters.
2806        check_metrics::<RubyParser>("def foo(a, b, c)\n  a + b + c\nend\n", "foo.rb", |metric| {
2807            assert_eq!(metric.nargs.fn_args_sum(), 3.0);
2808            assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2809        });
2810    }
2811
2812    #[test]
2813    fn ruby_single_closure() {
2814        // A bare block `[1,2,3].each { |x| ... }` is the only closure
2815        // here; `each` is a method call so the method-args count is 0.
2816        check_metrics::<RubyParser>("[1, 2, 3].each { |x| puts x }\n", "foo.rb", |metric| {
2817            assert_eq!(metric.nargs.fn_args_sum(), 0.0);
2818            assert_eq!(metric.nargs.closure_args_sum(), 1.0);
2819        });
2820    }
2821
2822    #[test]
2823    fn ruby_functions() {
2824        // Two methods, args=2 and args=1; one lambda with args=2.
2825        check_metrics::<RubyParser>(
2826            "def add(a, b)\n  a + b\nend\n\ndef neg(x)\n  -x\nend\n\nf = ->(a, b) { a * b }\n",
2827            "foo.rb",
2828            |metric| {
2829                assert_eq!(metric.nargs.fn_args_sum(), 3.0);
2830                assert_eq!(metric.nargs.closure_args_sum(), 2.0);
2831            },
2832        );
2833    }
2834
2835    #[test]
2836    fn ruby_nested_functions() {
2837        // An outer method with 1 arg containing an inner method with 2.
2838        check_metrics::<RubyParser>(
2839            "def outer(a)\n  def inner(b, c)\n    b + c\n  end\n  inner(a, a)\nend\n",
2840            "foo.rb",
2841            |metric| {
2842                assert_eq!(metric.nargs.fn_args_sum(), 3.0);
2843                assert_eq!(metric.nargs.closure_args_sum(), 0.0);
2844            },
2845        );
2846    }
2847}