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