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::c_declarator::innermost_declarator;
23use crate::checker::Checker;
24use crate::macros::implement_metric_trait;
25use crate::*;
26
27/// The `NArgs` metric.
28///
29/// This metric counts the number of arguments
30/// of functions/closures.
31#[derive(Debug, Clone, PartialEq)]
32#[non_exhaustive]
33pub struct Stats {
34    fn_nargs: usize,
35    closure_nargs: usize,
36    fn_nargs_sum: usize,
37    closure_nargs_sum: usize,
38    fn_nargs_min: usize,
39    closure_nargs_min: usize,
40    fn_nargs_max: usize,
41    closure_nargs_max: usize,
42    total_functions: usize,
43    total_closures: usize,
44}
45
46impl Default for Stats {
47    fn default() -> Self {
48        Self {
49            fn_nargs: 0,
50            closure_nargs: 0,
51            fn_nargs_sum: 0,
52            closure_nargs_sum: 0,
53            fn_nargs_min: usize::MAX,
54            closure_nargs_min: usize::MAX,
55            fn_nargs_max: 0,
56            closure_nargs_max: 0,
57            total_functions: 0,
58            total_closures: 0,
59        }
60    }
61}
62
63impl fmt::Display for Stats {
64    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
65        write!(
66            f,
67            "function_args: {}, closure_args: {}, function_args_average: {}, closure_args_average: {}, total: {}, average: {}, function_args_min: {}, function_args_max: {}, closure_args_min: {}, closure_args_max: {}",
68            self.function_args_sum(),
69            self.closure_args_sum(),
70            self.function_args_average(),
71            self.closure_args_average(),
72            self.total(),
73            self.average(),
74            self.function_args_min(),
75            self.function_args_max(),
76            self.closure_args_min(),
77            self.closure_args_max()
78        )
79    }
80}
81
82impl Stats {
83    /// Merges a second `NArgs` metric into the first one
84    pub fn merge(&mut self, other: &Stats) {
85        self.closure_nargs_min = self.closure_nargs_min.min(other.closure_nargs_min);
86        self.closure_nargs_max = self.closure_nargs_max.max(other.closure_nargs_max);
87        self.fn_nargs_min = self.fn_nargs_min.min(other.fn_nargs_min);
88        self.fn_nargs_max = self.fn_nargs_max.max(other.fn_nargs_max);
89        self.fn_nargs_sum += other.fn_nargs_sum;
90        self.closure_nargs_sum += other.closure_nargs_sum;
91    }
92
93    /// Returns the number of function arguments in a space.
94    #[inline]
95    #[must_use]
96    pub fn function_args(&self) -> u64 {
97        self.fn_nargs as u64
98    }
99
100    /// Returns the number of closure arguments in a space.
101    #[inline]
102    #[must_use]
103    pub fn closure_args(&self) -> u64 {
104        self.closure_nargs as u64
105    }
106
107    /// Returns the number of function arguments sum in a space.
108    #[inline]
109    #[must_use]
110    pub fn function_args_sum(&self) -> u64 {
111        self.fn_nargs_sum as u64
112    }
113
114    /// Returns the number of closure arguments sum in a space.
115    #[inline]
116    #[must_use]
117    pub fn closure_args_sum(&self) -> u64 {
118        self.closure_nargs_sum as u64
119    }
120
121    /// Returns the average number of functions arguments in a space.
122    #[inline]
123    #[must_use]
124    pub fn function_args_average(&self) -> f64 {
125        crate::metrics::average(self.fn_nargs_sum as f64, self.total_functions)
126    }
127
128    /// Returns the average number of closures arguments in a space.
129    #[inline]
130    #[must_use]
131    pub fn closure_args_average(&self) -> f64 {
132        crate::metrics::average(self.closure_nargs_sum as f64, self.total_closures)
133    }
134
135    /// Returns the total number of arguments of each function and
136    /// closure in a space.
137    #[inline]
138    #[must_use]
139    pub fn total(&self) -> u64 {
140        self.function_args_sum() + self.closure_args_sum()
141    }
142
143    /// Returns this space's **own** argument count: its function
144    /// parameters plus the parameters of any lambda the language folds
145    /// into it (a lambda that opens no space of its own).
146    ///
147    /// This is the quantity `bca check --threshold nargs=N` gates on
148    /// since #1196, and the one the wire shape serializes as
149    /// `nargs.value` — as opposed to [`total`](Self::total), which sums
150    /// the whole subtree and so charges a function for every nested
151    /// callable's parameters. Named here rather than spelled out at
152    /// each call site so the CLI gate, the serialized field, and any
153    /// future front-end read one definition instead of three copies of
154    /// the same sum (#1236).
155    #[inline]
156    #[must_use]
157    pub fn own_args(&self) -> u64 {
158        self.function_args() + self.closure_args()
159    }
160
161    /// Returns the `NArgs` metric average value
162    ///
163    /// This value is computed dividing the `NArgs` value
164    /// for the total number of functions/closures in a space.
165    #[inline]
166    #[must_use]
167    pub fn average(&self) -> f64 {
168        crate::metrics::average(
169            self.total() as f64,
170            self.total_functions + self.total_closures,
171        )
172    }
173    /// Returns the minimum number of function arguments in a space.
174    ///
175    /// Collapses the `usize::MAX` sentinel that `Stats::default()` plants
176    /// into `fn_nargs_min` to `0.0`, so a never-observed space
177    /// serializes to a meaningful number rather than `1.8446744e19`.
178    #[inline]
179    #[must_use]
180    pub fn function_args_min(&self) -> u64 {
181        if self.fn_nargs_min == usize::MAX {
182            0
183        } else {
184            self.fn_nargs_min as u64
185        }
186    }
187    /// Returns the maximum number of function arguments in a space.
188    #[inline]
189    #[must_use]
190    pub fn function_args_max(&self) -> u64 {
191        self.fn_nargs_max as u64
192    }
193    /// Returns the minimum number of closure arguments in a space.
194    ///
195    /// Same `usize::MAX` sentinel collapse as `function_args_min`.
196    #[inline]
197    #[must_use]
198    pub fn closure_args_min(&self) -> u64 {
199        if self.closure_nargs_min == usize::MAX {
200            0
201        } else {
202            self.closure_nargs_min as u64
203        }
204    }
205    /// Returns the maximum number of closure arguments in a space.
206    #[inline]
207    #[must_use]
208    pub fn closure_args_max(&self) -> u64 {
209        self.closure_nargs_max as u64
210    }
211    #[inline]
212    pub(crate) fn compute_sum(&mut self) {
213        self.closure_nargs_sum += self.closure_nargs;
214        self.fn_nargs_sum += self.fn_nargs;
215    }
216    #[inline]
217    pub(crate) fn compute_minmax(&mut self) {
218        self.closure_nargs_min = self.closure_nargs_min.min(self.closure_nargs);
219        self.closure_nargs_max = self.closure_nargs_max.max(self.closure_nargs);
220        self.fn_nargs_min = self.fn_nargs_min.min(self.fn_nargs);
221        self.fn_nargs_max = self.fn_nargs_max.max(self.fn_nargs);
222        self.compute_sum();
223    }
224    pub(crate) fn finalize(&mut self, total_functions: usize, total_closures: usize) {
225        self.total_functions = total_functions;
226        self.total_closures = total_closures;
227    }
228}
229
230/// How many of `params`' children are formal parameters.
231///
232/// The one place that says what a parameter is, and the only caller of
233/// [`Checker::is_non_arg`]. No language's `is_non_arg` lists a comment —
234/// they carry punctuation plus, in a few cases, a non-parameter the
235/// grammar puts in the list anyway (Rust's `self` receivers, Python's
236/// PEP 570 `/` marker, PHP's `...`) — so the purely negative filter
237/// this replaces counted a comment sitting between two parameters —
238/// `int h(int a /* one */, int b)` reported 3 — and counted the comment
239/// that stands in for an unnamed parameter, so `void f(int /*unused*/)`
240/// reported 2 (#1201). tree-sitter attaches a comment as a direct child
241/// of the parameter list, not inside the parameter it documents, which
242/// is why no `is_non_arg` list could have caught it.
243///
244/// Excluding comments here rather than in twenty `is_non_arg` impls is
245/// what makes it one rule: the next language added inherits it. Perl,
246/// Elixir and Kotlin lambdas reach their parameter list by three routes
247/// `compute_args` cannot express, so they call this directly.
248#[inline]
249fn count_args<T: Checker>(params: &Node, code: &[u8]) -> usize {
250    params
251        .children()
252        .filter(|child| {
253            !T::is_non_arg(child) && !T::is_comment(child) && !T::is_empty_param_marker(child, code)
254        })
255        .count()
256}
257
258#[inline]
259fn compute_args<T: Checker>(node: &Node, code: &[u8], nargs: &mut usize) {
260    if let Some(params) = node.child_by_field_name("parameters") {
261        // The field can hold a lone parameter rather than a list, in
262        // which case there are no children to walk and `count_args`
263        // yields zero — see `Checker::is_bare_param` (#1185).
264        if T::is_bare_param(&params) {
265            *nargs += 1;
266            return;
267        }
268        *nargs += count_args::<T>(&params, code);
269    } else if node.child_by_field_name("parameter").is_some() {
270        // JS/TS/TSX/MozJS arrow functions with a bare identifier parameter
271        // (`x => …`) use the singular `parameter` field instead of the plural
272        // `parameters` field. The grammar guarantees this is exactly one
273        // identifier, so count it as one argument.
274        *nargs += 1;
275    }
276}
277
278#[doc(hidden)]
279/// Per-language counting of function arguments.
280pub(crate) trait NArgs
281where
282    Self: Checker,
283    Self: std::marker::Sized,
284{
285    /// Walk `node` and update `stats` with this metric for the language
286    /// implementing the trait.
287    ///
288    /// Uses the source-aware [`Checker::is_func_with_code`] rather than the
289    /// byte-less `is_func`, exactly as [`crate::nom::Nom::compute`] does.
290    /// For every grammar with a syntactic function-definition node the two
291    /// are the same predicate, so no count moves; the point is that a
292    /// language whose declarations are only recognisable from the source
293    /// text — Elixir's `def` is an ordinary `Call` (#275) — does not
294    /// silently report 0 here (#1142).
295    fn compute<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
296        if Self::is_func_with_code(node, code, ancestors) {
297            compute_args::<Self>(
298                &Self::params_owner(node, ancestors),
299                code,
300                &mut stats.fn_nargs,
301            );
302            return;
303        }
304
305        if Self::is_closure(node, ancestors) {
306            compute_args::<Self>(
307                &Self::params_owner(node, ancestors),
308                code,
309                &mut stats.closure_nargs,
310            );
311        }
312    }
313
314    /// The node whose `parameters` field holds this callable's formal
315    /// arguments. Defaults to the callable's own node.
316    ///
317    /// Exists so a language that spells its parameters somewhere other
318    /// than on the function node can say *that* and inherit everything
319    /// else. Overriding [`Self::compute`] to change this one expression
320    /// means re-stating the `is_func_with_code`-not-`is_func` rule and
321    /// the closure fallback, and a language that copied them does not
322    /// pick up a later correction — which is the drift #1142 and #1162
323    /// were both filed about.
324    ///
325    /// It answers for both channels — a closure reaches its parameters
326    /// through this too, which is what lets the C family express a C++
327    /// lambda and a pointer-returning function as one rule (#1200).
328    ///
329    /// The two are mutually exclusive: only the default `compute` calls
330    /// this, so a language that overrides `compute` (Objc, Go, Kotlin,
331    /// Lua, Tcl, iRules, Perl, Elixir, Groovy) would define a
332    /// `params_owner` that is never consulted. Override one or the
333    /// other, not both.
334    fn params_owner<'tree>(node: &Node<'tree>, _ancestors: Ancestors<'tree, '_>) -> Node<'tree> {
335        *node
336    }
337}
338
339// The C family spells a function's parameters on the
340// `function_declarator` buried under whatever the *return type*
341// contributed, so all three point `params_owner` at the innermost
342// declarator carrying a `parameters` field. Falling back to the node
343// keeps the pre-#1200 answer for a shape with no declarator at all — a
344// parameterless C++ lambda, `[]{ … }`, which has none.
345//
346// C++ and Mozcpp reach this for their lambdas as well as their
347// functions; C has no closure form at all (`CCode::is_closure` is a
348// constant `false`), so its closure channel is unreachable rather than
349// merely unused.
350impl NArgs for CppCode {
351    fn params_owner<'tree>(node: &Node<'tree>, _ancestors: Ancestors<'tree, '_>) -> Node<'tree> {
352        innermost_declarator::<Self>(node).unwrap_or(*node)
353    }
354}
355
356impl NArgs for CCode {
357    fn params_owner<'tree>(node: &Node<'tree>, _ancestors: Ancestors<'tree, '_>) -> Node<'tree> {
358        innermost_declarator::<Self>(node).unwrap_or(*node)
359    }
360}
361
362impl NArgs for MozcppCode {
363    fn params_owner<'tree>(node: &Node<'tree>, _ancestors: Ancestors<'tree, '_>) -> Node<'tree> {
364        innermost_declarator::<Self>(node).unwrap_or(*node)
365    }
366}
367
368// Objective-C carries parameters in three different shapes, so it cannot
369// share the single-`declarator`-field C/C++ impl:
370//   * free `function_definition`s use the C declarator → `parameters`
371//     field, counted exactly as in C;
372//   * a `method_definition` lists one `method_parameter` per labelled
373//     argument (`- (void)foo:(int)a bar:(int)b` → 2; the unary
374//     `- (void)foo` → 0);
375//   * a block `^(int x){ … }` holds its params in a `parameter_list`
376//     child rather than under a `parameters` field.
377impl NArgs for ObjcCode {
378    fn compute<'a>(node: &Node<'a>, code: &[u8], _ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
379        match node.kind_id().into() {
380            Objc::FunctionDefinition | Objc::FunctionDefinition2 => {
381                // The same declarator walk C and C++ use: Objective-C
382                // inherits C's declarator syntax, so `FILE *f(int a)`
383                // buries the parameter list one level down (#1200).
384                if let Some(owner) = innermost_declarator::<Self>(node) {
385                    compute_args::<Self>(&owner, code, &mut stats.fn_nargs);
386                }
387            }
388            Objc::MethodDefinition => {
389                // Both `method_parameter` aliases are accepted per the
390                // #285 / lesson-2 defensive convention: real parse trees
391                // emit `MethodParameter` (475), but the grammar also
392                // declares the `MethodParameter2` (476) alias, so list it
393                // too rather than risk a future bump emitting it.
394                node.act_on_child(&mut |n| {
395                    if matches!(
396                        n.kind_id().into(),
397                        Objc::MethodParameter | Objc::MethodParameter2
398                    ) {
399                        stats.fn_nargs += 1;
400                    }
401                });
402            }
403            Objc::BlockLiteral => {
404                // Through `count_args`, so the block channel gets the same
405                // three exclusions `compute_args` gives the function
406                // channel. Counting `ParameterDeclaration |
407                // VariadicParameter` positively could not consult
408                // `Checker::is_empty_param_marker`, so `^(void){ … }` —
409                // whose `parameter_list` holds a real
410                // `parameter_declaration` for the `void`, exactly as
411                // `int f(void)` does — reported one parameter (#1218).
412                //
413                // It inherits the shared rule's *inclusions* too, which the
414                // narrower positive match had excluded by construction: on
415                // invalid source an `ERROR` child (`^(int a,,)`) or a
416                // `compound_statement` one (`^({ int x; })`) now counts.
417                // That is the point rather than a regression — those are
418                // the numbers `int f(int a,,)` already reported through
419                // `count_args`, so the block arm stopped being the one
420                // caller that answered differently.
421                //
422                // `ParameterList2` is deliberately not matched: it is the
423                // alias for the hidden `_old_style_parameter_list`, and
424                // `block_literal` cannot produce it — even a K&R function
425                // definition emits `ParameterList`. Marked rather than
426                // silently omitted per `grammar-dispatch.md` §1/§2.
427                if let Some(params) = node.first_child(|id| Objc::ParameterList == id) {
428                    stats.closure_nargs += count_args::<Self>(&params, code);
429                }
430            }
431            _ => {}
432        }
433    }
434}
435
436// Go's `parameter_declaration` allows multiple names to share one type
437// (`func f(a, b int)` is one parameter_declaration with two `name` children
438// but two formal parameters). Count names rather than declarations so the
439// reported nargs matches Go's parameter count.
440fn compute_go_args(node: &Node, nargs: &mut usize) {
441    let Some(params) = node.child_by_field_name("parameters") else {
442        return;
443    };
444    *nargs += params
445        .children()
446        .map(|child| match child.kind_id().into() {
447            Go::ParameterDeclaration => child
448                .children()
449                .filter(|c| c.kind_id() == Go::Identifier)
450                .count()
451                .max(1),
452            Go::VariadicParameterDeclaration => 1,
453            _ => 0,
454        })
455        .sum::<usize>();
456}
457
458impl NArgs for GoCode {
459    fn compute<'a>(node: &Node<'a>, _code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
460        if Self::is_func(node, ancestors) {
461            compute_go_args(node, &mut stats.fn_nargs);
462            return;
463        }
464
465        if Self::is_closure(node, ancestors) {
466            compute_go_args(node, &mut stats.closure_nargs);
467        }
468    }
469}
470
471fn compute_kotlin_func_args(node: &Node, nargs: &mut usize) {
472    if let Some(params) = node
473        .children()
474        .find(|c| c.kind_id() == Kotlin::FunctionValueParameters)
475    {
476        params.act_on_child(&mut |n| {
477            if n.kind_id() == Kotlin::Parameter {
478                *nargs += 1;
479            }
480        });
481    }
482}
483
484fn compute_kotlin_lambda_args(node: &Node, code: &[u8], nargs: &mut usize) {
485    // Lambda parameters are plain identifiers or destructuring patterns separated
486    // by commas; there is no typed `Parameter` wrapper node (unlike function
487    // value parameters), so a negative filter is the correct predicate here — the
488    // shared one, which also drops the comment `{ a, /* one */ b -> }` puts
489    // between them (#1201). `KotlinCode::is_non_arg` adds the parens to the
490    // comma, which costs nothing: a destructuring pattern nests its own parens
491    // inside a `multi_variable_declaration`, so `lambda_parameters` never has a
492    // paren as a direct child.
493    if let Some(params) = node
494        .children()
495        .find(|c| c.kind_id() == Kotlin::LambdaParameters)
496    {
497        *nargs += count_args::<KotlinCode>(&params, code);
498    }
499}
500
501impl NArgs for KotlinCode {
502    fn compute<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
503        if Self::is_func(node, ancestors) {
504            compute_kotlin_func_args(node, &mut stats.fn_nargs);
505            return;
506        }
507
508        if Self::is_closure(node, ancestors) {
509            if node.kind_id() == Kotlin::LambdaLiteral {
510                compute_kotlin_lambda_args(node, code, &mut stats.closure_nargs);
511            } else {
512                compute_kotlin_func_args(node, &mut stats.closure_nargs);
513            }
514        }
515    }
516}
517
518fn compute_lua_args(node: &Node, nargs: &mut usize) {
519    let Some(params) = node.child_by_field_name("parameters") else {
520        return;
521    };
522    *nargs += params
523        .children()
524        .filter(|c| matches!(c.kind_id().into(), Lua::Identifier | Lua::VarargExpression))
525        .count();
526}
527
528impl NArgs for LuaCode {
529    fn compute<'a>(node: &Node<'a>, _code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
530        if Self::is_func(node, ancestors) {
531            compute_lua_args(node, &mut stats.fn_nargs);
532        } else if Self::is_closure(node, ancestors) {
533            compute_lua_args(node, &mut stats.closure_nargs);
534        }
535    }
536}
537
538fn compute_tcl_args(node: &Node, nargs: &mut usize) {
539    let Some(params) = node.child_by_field_name("arguments") else {
540        return;
541    };
542    *nargs += params
543        .children()
544        .filter(|c| c.kind_id() == Tcl::Argument)
545        .count();
546}
547
548impl NArgs for TclCode {
549    fn compute<'a>(node: &Node<'a>, _code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
550        if Self::is_func(node, ancestors) {
551            compute_tcl_args(node, &mut stats.fn_nargs);
552        }
553    }
554}
555
556// iRules counterpart of `compute_tcl_args`. Only `procedure` carries an
557// `arguments` *field*; `when_event` handlers have no formal parameters
558// (the event context is implicit), so they correctly count zero. `{a 5}`
559// default-valued parameters parse as a single `argument`, so each formal
560// parameter contributes one regardless of its default.
561fn compute_irules_args(node: &Node, nargs: &mut usize) {
562    let Some(params) = node.child_by_field_name("arguments") else {
563        return;
564    };
565    *nargs += params
566        .children()
567        .filter(|c| c.kind_id() == Irules::Argument)
568        .count();
569}
570
571impl NArgs for IrulesCode {
572    fn compute<'a>(node: &Node<'a>, _code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
573        if Self::is_func(node, ancestors) {
574            compute_irules_args(node, &mut stats.fn_nargs);
575        }
576    }
577}
578
579// tree-sitter-perl emits a subroutine signature as an unnamed
580// `function_signature` child rather than under a `parameters` field, so
581// the shared `compute_args` helper never sees it. `FunctionSignature2`
582// is the hidden `_function_signature` supertype, listed defensively per
583// the lesson-2 convention.
584//
585// A bare attribute swallows the signature — `sub f :lvalue ($z)` parses
586// as `function_attribute → function_signature` — while an attribute
587// carrying its own parens (`sub f :prototype($$) ($a, $b)`) leaves the
588// signature a direct child. Look one level into `function_attribute` so
589// both spellings count; a `:prototype($$)` argument list is a
590// `function_prototype`, a different kind, so it cannot be mistaken for a
591// signature.
592fn perl_signature<'a>(node: &Node<'a>) -> Option<Node<'a>> {
593    fn is_signature(id: u16) -> bool {
594        matches!(
595            id.into(),
596            Perl::FunctionSignature | Perl::FunctionSignature2
597        )
598    }
599    node.children().find_map(|child| {
600        if is_signature(child.kind_id()) {
601            Some(child)
602        } else if child.kind_id() == Perl::FunctionAttribute {
603            child.first_child(is_signature)
604        } else {
605            None
606        }
607    })
608}
609
610// Count every signature child `count_args` accepts: a defaulted parameter
611// (`$y = 5`) is a `binary_expression`, not a bare `scalar_variable`, so a
612// positive variant list would undercount it. The negative filter also
613// survives signature forms the grammar may add — at the price of needing
614// the comment exclusion, since a multi-line signature documents its
615// parameters with `comments` children sitting directly under
616// `function_signature`. Perl carried that exclusion inline for years
617// before #1201 found the same hole in every other language and moved it
618// into the shared predicate.
619fn compute_perl_args(node: &Node, code: &[u8], nargs: &mut usize) {
620    let Some(signature) = perl_signature(node) else {
621        return;
622    };
623    *nargs += count_args::<PerlCode>(&signature, code);
624}
625
626impl NArgs for PerlCode {
627    fn compute<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
628        if Self::is_func(node, ancestors) {
629            compute_perl_args(node, code, &mut stats.fn_nargs);
630            return;
631        }
632
633        // Every anonymous sub reads 0 today: tree-sitter-perl 1.1.2 parses
634        // its signature inside an `ERROR` node
635        // (`anonymous_function → sub → ERROR → function_signature`), so
636        // `perl_signature` finds nothing among the direct children.
637        // Deliberately not recovered — descending through `ERROR` would
638        // pin us to a parse shape upstream will change. The call stays
639        // here so a grammar fix starts counting (and fails
640        // `perl_anonymous_sub_signature_is_zero`) rather than passing
641        // silently; revisit at the next `recreate-grammars.sh` bump.
642        if Self::is_closure(node, ancestors) {
643            compute_perl_args(node, code, &mut stats.closure_nargs);
644        }
645    }
646}
647
648// Elixir has no function-definition node. `def bar(a, b, c)` is a `Call`
649// whose `arguments` holds a *second* `Call` carrying the real parameter
650// list, which is why the `parameters`-field heuristic in `compute_args`
651// finds nothing:
652//
653//   call (def)
654//   ├─ identifier  def            <- the `target` field
655//   ╰─ arguments                  <- NOT a field; tree-sitter-elixir
656//      ╰─ call     bar(a, b, c)      gives `call` only a `target` field,
657//         ├─ identifier bar          so both `arguments` levels have to
658//         ╰─ arguments (a, b, c)     be found by kind.
659//
660// A guarded head interposes a `when` `binary_operator` whose `left` is
661// that `Call` — without unwrapping it every guarded clause counts 0, and
662// guards are a large fraction of real Elixir. A head that is a bare
663// `identifier` (`def noargs, do: 1`) has no parameter list and counts 0.
664//
665// `def a + b` and `def -a` define the operator functions `+/2` and `-/1`.
666// Their head is the operator node itself, with the parameters as its
667// operands and no `arguments` container to walk, so the arity comes from
668// the operator's shape.
669fn elixir_declared_args(node: &Node, code: &[u8]) -> usize {
670    let Some(head) = elixir_arguments(node).and_then(|a| a.children().find(Node::is_named)) else {
671        return 0;
672    };
673    let head = elixir_unwrap_guard(&head, code);
674    match head.kind_id().into() {
675        Elixir::Call => elixir_arguments(&head).map_or(0, |p| count_args::<ElixirCode>(&p, code)),
676        Elixir::BinaryOperator => 2,
677        Elixir::UnaryOperator => 1,
678        _ => 0,
679    }
680}
681
682fn elixir_arguments<'a>(node: &Node<'a>) -> Option<Node<'a>> {
683    node.first_child(|id| id == Elixir::Arguments)
684}
685
686// Returns the guarded expression when `node` is a `when` guard, and
687// `node` itself otherwise. Matching `BinaryOperator` alone would also
688// unwrap an operator definition, whose operands are the parameters.
689fn elixir_unwrap_guard<'a>(node: &Node<'a>, code: &[u8]) -> Node<'a> {
690    let is_when = node.kind_id() == Elixir::BinaryOperator
691        && node
692            .child_by_field_name("operator")
693            .and_then(|op| op.utf8_text(code))
694            == Some("when");
695    if is_when {
696        node.child_by_field_name("left").unwrap_or(*node)
697    } else {
698        *node
699    }
700}
701
702impl NArgs for ElixirCode {
703    fn compute<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
704        // `is_func` is byte-less and constant `false` for Elixir, because a
705        // `def` is textually indistinguishable from any other `Call`. The
706        // code-aware predicate (#275) is the only one that identifies one,
707        // and it also excludes a `def` inside `quote do … end`, which
708        // declares nothing until the macro expands (#310).
709        if Self::is_func_with_code(node, code, ancestors) {
710            stats.fn_nargs += elixir_declared_args(node, code);
711            return;
712        }
713
714        // Every clause of one `fn` must have the same arity, so the first
715        // `stab_clause` gives the closure's argument count. Summing the
716        // clauses would report `2n` for an n-clause function — do not
717        // "fix" this into a sum.
718        //
719        // A guarded clause (`fn x when is_integer(x) -> …`) aliases its
720        // `left` to a `binary_operator`, exactly as a guarded `def` head
721        // does, so it needs the same unwrap — otherwise every guarded
722        // closure counts the fixed 3 children of the guard expression.
723        if Self::is_closure(node, ancestors)
724            && let Some(clause) = node.first_child(|id| id == Elixir::StabClause)
725            && let Some(params) = clause.child_by_field_name("left")
726        {
727            stats.closure_nargs +=
728                count_args::<ElixirCode>(&elixir_unwrap_guard(&params, code), code);
729        }
730    }
731}
732
733implement_metric_trait!(
734    [NArgs],
735    PythonCode,
736    MozjsCode,
737    JavascriptCode,
738    TypescriptCode,
739    TsxCode,
740    RustCode,
741    PreprocCode,
742    CcommentCode,
743    BashCode,
744    PhpCode,
745    CsharpCode,
746    RubyCode
747);
748
749// A record's compact constructor (`record R(int a, int b) { R { … } }`,
750// JLS 8.10.4) declares no formal parameter list of its own: the grammar
751// gives `compact_constructor_declaration` only `name` and `body` fields,
752// and hangs the parameters — the record's components — off the enclosing
753// `record_declaration`. Resolve that declaration so the two spellings of
754// one constructor agree: the canonical `R(int a, int b) { … }` reports 2,
755// and so should the compact form (#1160).
756//
757// The nesting is fixed by the grammar — the constructor is a direct child
758// of the record's `class_body` — so the record sits exactly two steps up.
759// The kind check states what that positional step is allowed to land on;
760// no kind other than `record_declaration` carries a `parameters` field
761// here, so it changes no count today, but it keeps the step from silently
762// starting to mean something else if one ever does.
763//
764// A record that declares *no* constructor still reports 0: its canonical
765// constructor is implicit, so there is no node to open a space for and
766// nothing to attribute the components to. Adding an empty `R { }` to such
767// a record therefore moves its `nargs` from 0 to the component count
768// without changing the API — the same asymmetry an explicit
769// `R(int a, int b) { }` already produces, and the price of measuring
770// declared code rather than generated code.
771fn java_compact_constructor_record<'tree>(
772    node: &Node<'tree>,
773    ancestors: Ancestors<'tree, '_>,
774) -> Option<Node<'tree>> {
775    if node.kind_id() != Java::CompactConstructorDeclaration {
776        return None;
777    }
778    let (grandparent, _) = ancestors.iter(node).nth(1)?;
779    (grandparent.kind_id() == Java::RecordDeclaration).then_some(grandparent)
780}
781
782impl NArgs for JavaCode {
783    fn params_owner<'tree>(node: &Node<'tree>, ancestors: Ancestors<'tree, '_>) -> Node<'tree> {
784        java_compact_constructor_record(node, ancestors).unwrap_or(*node)
785    }
786}
787
788// Groovy closures use `closure_parameters` as an unnamed child rather
789// than a `parameters` field, so the default NArgs walker (which looks
790// for a `parameters` field) misses them. Match the closure_parameters
791// child directly and count its `closure_parameter` grand-children.
792impl NArgs for GroovyCode {
793    fn compute<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>, stats: &mut Stats) {
794        use crate::languages::language_groovy::Groovy;
795
796        if Self::is_func(node, ancestors) {
797            compute_args::<Self>(node, code, &mut stats.fn_nargs);
798            return;
799        }
800
801        if Self::is_closure(node, ancestors)
802            && let Some(params) = node.first_child(|id| id == Groovy::ClosureParameters)
803        {
804            params.act_on_child(&mut |n| {
805                if n.kind_id() == Groovy::ClosureParameter {
806                    stats.closure_nargs += 1;
807                }
808            });
809        }
810    }
811}
812
813#[cfg(test)]
814#[allow(
815    clippy::float_cmp,
816    clippy::cast_precision_loss,
817    clippy::cast_possible_truncation,
818    clippy::cast_sign_loss,
819    clippy::similar_names,
820    clippy::doc_markdown,
821    clippy::needless_raw_string_hashes,
822    clippy::too_many_lines
823)]
824mod tests {
825    use crate::test_support::{check_func_space_only_shim, check_metrics_only_shim};
826
827    use super::*;
828
829    // Nargs pulls Nom for its per-function average divisor, which is also
830    // what this module's `metric.nom.functions_sum()` /
831    // `closures_sum()` assertions read.
832    check_metrics_only_shim!(check_metrics, Nargs);
833    // The same selection, handed the whole space tree: the per-space own
834    // count is only observable one space at a time.
835    check_func_space_only_shim!(check_spaces, Nargs);
836
837    /// Regression for #227: a `Stats::default()` that never sees an
838    /// observation must not leak the `usize::MAX` sentinel for
839    /// `fn_args_min` or `closure_args_min`. Both getters collapse
840    /// the sentinel to `0.0` so JSON never emits `1.8446744e19`.
841    #[test]
842    fn nargs_empty_file_min_is_zero() {
843        let stats = Stats::default();
844        assert_eq!(stats.function_args_min(), 0);
845        assert_eq!(stats.closure_args_min(), 0);
846    }
847
848    /// The per-space **own** argument count, at every space of a tree
849    /// where it differs from the subtree `total` at every level.
850    ///
851    /// `bca check --threshold nargs=N` has gated on `own_args()` since
852    /// #1196, and #1236 serialized it as `nargs.value` so the JSON-walking
853    /// `to_sarif` binding can reproduce that gate. Before #1236 the
854    /// binding compared `nargs.total` and reported `outer` at 7 — a
855    /// finding `bca check` never emits, because `outer` declares two
856    /// parameters and the other five belong to closures that are gated
857    /// on rows of their own.
858    ///
859    /// The fixture is the #1236 reproducer, picked because no space in it
860    /// has `own == total`: a fixture where the two agree passes whichever
861    /// one the code reads.
862    #[cfg(feature = "rust")]
863    #[test]
864    fn own_args_excludes_nested_closure_spaces() {
865        fn walk(space: &FuncSpace, rows: &mut Vec<(String, usize, u64, u64)>) {
866            rows.push((
867                space.name.clone().unwrap_or_default(),
868                space.start_line,
869                space.metrics.nargs.own_args(),
870                space.metrics.nargs.total(),
871            ));
872            for child in &space.spaces {
873                walk(child, rows);
874            }
875        }
876
877        check_spaces::<RustParser, _>(
878            "fn outer(a: i32, b: i32) {
879                 let f = |x: i32, y: i32, z: i32| x + y + z;
880                 let g = |p: i32, q: i32| p + q;
881                 f(a, b, 0);
882                 g(a, b);
883             }",
884            "closures.rs",
885            |space| {
886                let mut rows = Vec::new();
887                walk(&space, &mut rows);
888                // Hand-pinned in walk order, `(name, start_line, own, total)`.
889                // The file root owns no parameters; `outer` owns its two;
890                // each closure owns its own list. Every row's `total` is
891                // that space's own count plus its descendants'.
892                assert_eq!(
893                    rows,
894                    vec![
895                        ("closures.rs".to_owned(), 1, 0, 7),
896                        ("outer".to_owned(), 1, 2, 7),
897                        ("<anonymous>".to_owned(), 2, 3, 3),
898                        ("<anonymous>".to_owned(), 3, 2, 2),
899                    ],
900                );
901
902                // The serialized projection the Python `to_sarif` binding
903                // walks: `value` is the gate's number, `total` is not.
904                let outer = &space.spaces[0];
905                let json = serde_json::to_value(&outer.metrics.nargs)
906                    .expect("nargs serializes through its wire projection");
907                assert_eq!(json["value"], 2, "nargs.value must be `outer`'s own args");
908                assert_eq!(json["total"], 7, "nargs.total stays the subtree sum");
909            },
910        );
911    }
912
913    #[cfg(feature = "python")]
914    #[test]
915    fn python_no_functions_and_closures() {
916        check_metrics::<PythonParser>("a = 42", "foo.py", |metric| {
917            // 0 functions + 0 closures
918            insta::assert_json_snapshot!(
919                metric.nargs,
920                @r#"
921            {
922              "function_args": 0,
923              "closure_args": 0,
924              "function_args_average": 0.0,
925              "closure_args_average": 0.0,
926              "total": 0,
927              "value": 0,
928              "average": 0.0,
929              "function_args_min": 0,
930              "function_args_max": 0,
931              "closure_args_min": 0,
932              "closure_args_max": 0
933            }
934            "#
935            );
936        });
937    }
938
939    #[cfg(feature = "rust")]
940    #[test]
941    fn rust_no_functions_and_closures() {
942        check_metrics::<RustParser>("let a = 42;", "foo.rs", |metric| {
943            // 0 functions + 0 closures
944            insta::assert_json_snapshot!(
945                metric.nargs,
946                @r#"
947            {
948              "function_args": 0,
949              "closure_args": 0,
950              "function_args_average": 0.0,
951              "closure_args_average": 0.0,
952              "total": 0,
953              "value": 0,
954              "average": 0.0,
955              "function_args_min": 0,
956              "function_args_max": 0,
957              "closure_args_min": 0,
958              "closure_args_max": 0
959            }
960            "#
961            );
962        });
963    }
964
965    #[cfg(feature = "cpp")]
966    #[test]
967    fn cpp_no_functions_and_closures() {
968        check_metrics::<CppParser>("int a = 42;", "foo.cpp", |metric| {
969            // 0 functions + 0 closures
970            insta::assert_json_snapshot!(
971                metric.nargs,
972                @r#"
973            {
974              "function_args": 0,
975              "closure_args": 0,
976              "function_args_average": 0.0,
977              "closure_args_average": 0.0,
978              "total": 0,
979              "value": 0,
980              "average": 0.0,
981              "function_args_min": 0,
982              "function_args_max": 0,
983              "closure_args_min": 0,
984              "closure_args_max": 0
985            }
986            "#
987            );
988        });
989    }
990
991    #[cfg(feature = "javascript")]
992    #[test]
993    fn javascript_no_functions_and_closures() {
994        check_metrics::<JavascriptParser>("var a = 42;", "foo.js", |metric| {
995            // 0 functions + 0 closures
996            insta::assert_json_snapshot!(
997                metric.nargs,
998                @r#"
999            {
1000              "function_args": 0,
1001              "closure_args": 0,
1002              "function_args_average": 0.0,
1003              "closure_args_average": 0.0,
1004              "total": 0,
1005              "value": 0,
1006              "average": 0.0,
1007              "function_args_min": 0,
1008              "function_args_max": 0,
1009              "closure_args_min": 0,
1010              "closure_args_max": 0
1011            }
1012            "#
1013            );
1014        });
1015    }
1016
1017    #[cfg(feature = "python")]
1018    #[test]
1019    fn python_single_function() {
1020        check_metrics::<PythonParser>(
1021            "def f(a, b):
1022                 if a:
1023                     return a",
1024            "foo.py",
1025            |metric| {
1026                // 1 function
1027                insta::assert_json_snapshot!(
1028                    metric.nargs,
1029                    @r#"
1030                {
1031                  "function_args": 2,
1032                  "closure_args": 0,
1033                  "function_args_average": 2.0,
1034                  "closure_args_average": 0.0,
1035                  "total": 2,
1036                  "value": 0,
1037                  "average": 2.0,
1038                  "function_args_min": 0,
1039                  "function_args_max": 2,
1040                  "closure_args_min": 0,
1041                  "closure_args_max": 0
1042                }
1043                "#
1044                );
1045            },
1046        );
1047    }
1048
1049    #[cfg(feature = "rust")]
1050    #[test]
1051    fn rust_single_function() {
1052        check_metrics::<RustParser>(
1053            "fn f(a: bool, b: usize) {
1054                 if a {
1055                     return a;
1056                }
1057             }",
1058            "foo.rs",
1059            |metric| {
1060                // 1 function
1061                insta::assert_json_snapshot!(
1062                    metric.nargs,
1063                    @r#"
1064                {
1065                  "function_args": 2,
1066                  "closure_args": 0,
1067                  "function_args_average": 2.0,
1068                  "closure_args_average": 0.0,
1069                  "total": 2,
1070                  "value": 0,
1071                  "average": 2.0,
1072                  "function_args_min": 0,
1073                  "function_args_max": 2,
1074                  "closure_args_min": 0,
1075                  "closure_args_max": 0
1076                }
1077                "#
1078                );
1079            },
1080        );
1081    }
1082
1083    #[cfg(feature = "c")]
1084    #[test]
1085    fn c_single_function() {
1086        check_metrics::<CParser>(
1087            "int f(int a, int b) {
1088                 if (a) {
1089                     return a;
1090                }
1091             }",
1092            "foo.c",
1093            |metric| {
1094                // 1 function
1095                insta::assert_json_snapshot!(
1096                    metric.nargs,
1097                    @r#"
1098                {
1099                  "function_args": 2,
1100                  "closure_args": 0,
1101                  "function_args_average": 2.0,
1102                  "closure_args_average": 0.0,
1103                  "total": 2,
1104                  "value": 0,
1105                  "average": 2.0,
1106                  "function_args_min": 0,
1107                  "function_args_max": 2,
1108                  "closure_args_min": 0,
1109                  "closure_args_max": 0
1110                }
1111                "#
1112                );
1113            },
1114        );
1115    }
1116
1117    #[cfg(feature = "javascript")]
1118    #[test]
1119    fn javascript_single_function() {
1120        check_metrics::<JavascriptParser>(
1121            "function f(a, b) {
1122                 return a * b;
1123             }",
1124            "foo.js",
1125            |metric| {
1126                // 1 function
1127                insta::assert_json_snapshot!(
1128                    metric.nargs,
1129                    @r#"
1130                {
1131                  "function_args": 2,
1132                  "closure_args": 0,
1133                  "function_args_average": 2.0,
1134                  "closure_args_average": 0.0,
1135                  "total": 2,
1136                  "value": 0,
1137                  "average": 2.0,
1138                  "function_args_min": 0,
1139                  "function_args_max": 2,
1140                  "closure_args_min": 0,
1141                  "closure_args_max": 0
1142                }
1143                "#
1144                );
1145            },
1146        );
1147    }
1148
1149    #[cfg(feature = "python")]
1150    #[test]
1151    fn python_single_lambda() {
1152        check_metrics::<PythonParser>("bar = lambda a: True", "foo.py", |metric| {
1153            // 1 lambda
1154            insta::assert_json_snapshot!(
1155                metric.nargs,
1156                @r#"
1157            {
1158              "function_args": 0,
1159              "closure_args": 1,
1160              "function_args_average": 0.0,
1161              "closure_args_average": 1.0,
1162              "total": 1,
1163              "value": 1,
1164              "average": 1.0,
1165              "function_args_min": 0,
1166              "function_args_max": 0,
1167              "closure_args_min": 1,
1168              "closure_args_max": 1
1169            }
1170            "#
1171            );
1172        });
1173    }
1174
1175    #[cfg(feature = "rust")]
1176    #[test]
1177    fn rust_single_closure() {
1178        check_metrics::<RustParser>("let bar = |i: i32| -> i32 { i + 1 };", "foo.rs", |metric| {
1179            // 1 lambda
1180            insta::assert_json_snapshot!(
1181                metric.nargs,
1182                @r#"
1183            {
1184              "function_args": 0,
1185              "closure_args": 1,
1186              "function_args_average": 0.0,
1187              "closure_args_average": 1.0,
1188              "total": 1,
1189              "value": 0,
1190              "average": 1.0,
1191              "function_args_min": 0,
1192              "function_args_max": 0,
1193              "closure_args_min": 0,
1194              "closure_args_max": 1
1195            }
1196            "#
1197            );
1198        });
1199    }
1200
1201    #[cfg(feature = "cpp")]
1202    #[test]
1203    fn cpp_single_lambda() {
1204        check_metrics::<CppParser>(
1205            "auto bar = [](int x, int y) -> int { return x + y; };",
1206            "foo.cpp",
1207            |metric| {
1208                // 1 lambda
1209                insta::assert_json_snapshot!(
1210                    metric.nargs,
1211                    @r#"
1212                {
1213                  "function_args": 0,
1214                  "closure_args": 2,
1215                  "function_args_average": 0.0,
1216                  "closure_args_average": 2.0,
1217                  "total": 2,
1218                  "value": 2,
1219                  "average": 2.0,
1220                  "function_args_min": 0,
1221                  "function_args_max": 0,
1222                  "closure_args_min": 2,
1223                  "closure_args_max": 2
1224                }
1225                "#
1226                );
1227            },
1228        );
1229    }
1230
1231    #[cfg(feature = "javascript")]
1232    #[test]
1233    fn javascript_single_closure() {
1234        check_metrics::<JavascriptParser>("function (a, b) {return a + b};", "foo.js", |metric| {
1235            // 1 lambda
1236            insta::assert_json_snapshot!(
1237                metric.nargs,
1238                @r#"
1239            {
1240              "function_args": 0,
1241              "closure_args": 2,
1242              "function_args_average": 0.0,
1243              "closure_args_average": 2.0,
1244              "total": 2,
1245              "value": 0,
1246              "average": 2.0,
1247              "function_args_min": 0,
1248              "function_args_max": 0,
1249              "closure_args_min": 0,
1250              "closure_args_max": 2
1251            }
1252            "#
1253            );
1254        });
1255    }
1256
1257    #[cfg(feature = "python")]
1258    #[test]
1259    fn python_functions() {
1260        check_metrics::<PythonParser>(
1261            "def f(a, b):
1262                 if a:
1263                     return a
1264            def f(a, b):
1265                 if b:
1266                     return b",
1267            "foo.py",
1268            |metric| {
1269                // 2 functions
1270                insta::assert_json_snapshot!(
1271                    metric.nargs,
1272                    @r#"
1273                {
1274                  "function_args": 4,
1275                  "closure_args": 0,
1276                  "function_args_average": 2.0,
1277                  "closure_args_average": 0.0,
1278                  "total": 4,
1279                  "value": 0,
1280                  "average": 2.0,
1281                  "function_args_min": 0,
1282                  "function_args_max": 2,
1283                  "closure_args_min": 0,
1284                  "closure_args_max": 0
1285                }
1286                "#
1287                );
1288            },
1289        );
1290
1291        check_metrics::<PythonParser>(
1292            "def f(a, b):
1293                 if a:
1294                     return a
1295            def f(a, b, c):
1296                 if b:
1297                     return b",
1298            "foo.py",
1299            |metric| {
1300                // 2 functions
1301                insta::assert_json_snapshot!(
1302                    metric.nargs,
1303                    @r#"
1304                {
1305                  "function_args": 5,
1306                  "closure_args": 0,
1307                  "function_args_average": 2.5,
1308                  "closure_args_average": 0.0,
1309                  "total": 5,
1310                  "value": 0,
1311                  "average": 2.5,
1312                  "function_args_min": 0,
1313                  "function_args_max": 3,
1314                  "closure_args_min": 0,
1315                  "closure_args_max": 0
1316                }
1317                "#
1318                );
1319            },
1320        );
1321    }
1322
1323    #[cfg(feature = "rust")]
1324    #[test]
1325    fn rust_functions() {
1326        check_metrics::<RustParser>(
1327            "fn f(a: bool, b: usize) {
1328                 if a {
1329                     return a;
1330                }
1331             }
1332             fn f1(a: bool, b: usize) {
1333                 if a {
1334                     return a;
1335                }
1336             }",
1337            "foo.rs",
1338            |metric| {
1339                // 2 functions
1340                insta::assert_json_snapshot!(
1341                    metric.nargs,
1342                    @r#"
1343                {
1344                  "function_args": 4,
1345                  "closure_args": 0,
1346                  "function_args_average": 2.0,
1347                  "closure_args_average": 0.0,
1348                  "total": 4,
1349                  "value": 0,
1350                  "average": 2.0,
1351                  "function_args_min": 0,
1352                  "function_args_max": 2,
1353                  "closure_args_min": 0,
1354                  "closure_args_max": 0
1355                }
1356                "#
1357                );
1358            },
1359        );
1360
1361        check_metrics::<RustParser>(
1362            "fn f(a: bool, b: usize) {
1363                 if a {
1364                     return a;
1365                }
1366             }
1367             fn f1(a: bool, b: usize, c: usize) {
1368                 if a {
1369                     return a;
1370                }
1371             }",
1372            "foo.rs",
1373            |metric| {
1374                // 2 functions
1375                insta::assert_json_snapshot!(
1376                    metric.nargs,
1377                    @r#"
1378                {
1379                  "function_args": 5,
1380                  "closure_args": 0,
1381                  "function_args_average": 2.5,
1382                  "closure_args_average": 0.0,
1383                  "total": 5,
1384                  "value": 0,
1385                  "average": 2.5,
1386                  "function_args_min": 0,
1387                  "function_args_max": 3,
1388                  "closure_args_min": 0,
1389                  "closure_args_max": 0
1390                }
1391                "#
1392                );
1393            },
1394        );
1395    }
1396
1397    /// The `self` receiver (`self`, `&self`, `&mut self`) parses as a
1398    /// `self_parameter` node and, like Go's `receiver` field and C++'s
1399    /// implicit `this`, must not be counted as a formal parameter (#457).
1400    #[cfg(feature = "rust")]
1401    #[test]
1402    fn rust_method_excludes_self_receiver() {
1403        check_metrics::<RustParser>(
1404            "struct S;
1405             impl S {
1406                 fn a(self) {}                  // self          -> 0 args
1407                 fn b(&self, x: i32) {}         // &self     + 1 -> 1 arg
1408                 fn c(&mut self, x: i32, y: i32) {} // &mut self + 2 -> 2 args
1409             }",
1410            "foo.rs",
1411            |metric| {
1412                // 3 methods: 0 + 1 + 2 explicit params. The three receiver
1413                // forms contribute nothing. sum = 3, max = 2.
1414                let s = &metric.nargs;
1415                assert_eq!(s.function_args_sum(), 3);
1416                assert_eq!(s.function_args_max(), 2);
1417            },
1418        );
1419
1420        // A *typed* receiver (`self: Box<Self>`, `self: Rc<Self>`,
1421        // `self: Pin<&mut Self>`) parses as an ordinary `parameter` node —
1422        // not `self_parameter` — but its binding is the `self` keyword, so
1423        // it is still a receiver and must be excluded too, matching the
1424        // bare-receiver case and Go/C++ receiver parity (#457). A normal
1425        // `parameter` like `x: i32` binds an `identifier`, never `self`.
1426        check_metrics::<RustParser>(
1427            "use std::rc::Rc;
1428             use std::pin::Pin;
1429             struct S;
1430             impl S {
1431                 fn a(self: Box<Self>, x: i32, y: i32) {} // receiver + 2 -> 2
1432                 fn b(self: Rc<Self>, x: i32) {}          // receiver + 1 -> 1
1433                 fn c(self: Pin<&mut Self>) {}            // receiver     -> 0
1434             }",
1435            "foo.rs",
1436            |metric| {
1437                // Each typed receiver contributes nothing. sum = 2+1+0 = 3,
1438                // max = 2 (from method `a`).
1439                let s = &metric.nargs;
1440                assert_eq!(s.function_args_sum(), 3);
1441                assert_eq!(s.function_args_max(), 2);
1442            },
1443        );
1444    }
1445
1446    #[cfg(all(feature = "c", feature = "cpp"))]
1447    #[test]
1448    fn c_functions() {
1449        check_metrics::<CParser>(
1450            "int f(int a, int b) {
1451                 if (a) {
1452                     return a;
1453                }
1454             }
1455             int f1(int a, int b) {
1456                 if (a) {
1457                     return a;
1458                }
1459             }",
1460            "foo.c",
1461            |metric| {
1462                // 2 functions
1463                insta::assert_json_snapshot!(
1464                    metric.nargs,
1465                    @r#"
1466                {
1467                  "function_args": 4,
1468                  "closure_args": 0,
1469                  "function_args_average": 2.0,
1470                  "closure_args_average": 0.0,
1471                  "total": 4,
1472                  "value": 0,
1473                  "average": 2.0,
1474                  "function_args_min": 0,
1475                  "function_args_max": 2,
1476                  "closure_args_min": 0,
1477                  "closure_args_max": 0
1478                }
1479                "#
1480                );
1481            },
1482        );
1483
1484        check_metrics::<CppParser>(
1485            "int f(int a, int b) {
1486                 if (a) {
1487                     return a;
1488                }
1489             }
1490             int f1(int a, int b, int c) {
1491                 if (a) {
1492                     return a;
1493                }
1494             }",
1495            "foo.c",
1496            |metric| {
1497                // 2 functions
1498                insta::assert_json_snapshot!(
1499                    metric.nargs,
1500                    @r#"
1501                {
1502                  "function_args": 5,
1503                  "closure_args": 0,
1504                  "function_args_average": 2.5,
1505                  "closure_args_average": 0.0,
1506                  "total": 5,
1507                  "value": 0,
1508                  "average": 2.5,
1509                  "function_args_min": 0,
1510                  "function_args_max": 3,
1511                  "closure_args_min": 0,
1512                  "closure_args_max": 0
1513                }
1514                "#
1515                );
1516            },
1517        );
1518    }
1519
1520    #[cfg(feature = "javascript")]
1521    #[test]
1522    fn javascript_functions() {
1523        check_metrics::<JavascriptParser>(
1524            "function f(a, b) {
1525                 return a * b;
1526             }
1527             function f1(a, b) {
1528                 return a * b;
1529             }",
1530            "foo.js",
1531            |metric| {
1532                // 2 functions
1533                insta::assert_json_snapshot!(
1534                    metric.nargs,
1535                    @r#"
1536                {
1537                  "function_args": 4,
1538                  "closure_args": 0,
1539                  "function_args_average": 2.0,
1540                  "closure_args_average": 0.0,
1541                  "total": 4,
1542                  "value": 0,
1543                  "average": 2.0,
1544                  "function_args_min": 0,
1545                  "function_args_max": 2,
1546                  "closure_args_min": 0,
1547                  "closure_args_max": 0
1548                }
1549                "#
1550                );
1551            },
1552        );
1553
1554        check_metrics::<JavascriptParser>(
1555            "function f(a, b) {
1556                 return a * b;
1557             }
1558             function f1(a, b, c) {
1559                 return a * b;
1560             }",
1561            "foo.js",
1562            |metric| {
1563                // 2 functions
1564                insta::assert_json_snapshot!(
1565                    metric.nargs,
1566                    @r#"
1567                {
1568                  "function_args": 5,
1569                  "closure_args": 0,
1570                  "function_args_average": 2.5,
1571                  "closure_args_average": 0.0,
1572                  "total": 5,
1573                  "value": 0,
1574                  "average": 2.5,
1575                  "function_args_min": 0,
1576                  "function_args_max": 3,
1577                  "closure_args_min": 0,
1578                  "closure_args_max": 0
1579                }
1580                "#
1581                );
1582            },
1583        );
1584    }
1585
1586    #[cfg(feature = "python")]
1587    #[test]
1588    fn python_nested_functions() {
1589        check_metrics::<PythonParser>(
1590            "def f(a, b):
1591                 def foo(a):
1592                     if a:
1593                         return 1
1594                 bar = lambda a: lambda b: b or True or True
1595                 return bar(foo(a))(a)",
1596            "foo.py",
1597            |metric| {
1598                // 2 functions + 2 lambdas = 4
1599                insta::assert_json_snapshot!(
1600                    metric.nargs,
1601                    @r#"
1602                {
1603                  "function_args": 3,
1604                  "closure_args": 2,
1605                  "function_args_average": 1.5,
1606                  "closure_args_average": 1.0,
1607                  "total": 5,
1608                  "value": 0,
1609                  "average": 1.25,
1610                  "function_args_min": 0,
1611                  "function_args_max": 2,
1612                  "closure_args_min": 0,
1613                  "closure_args_max": 2
1614                }
1615                "#
1616                );
1617            },
1618        );
1619    }
1620
1621    #[cfg(feature = "rust")]
1622    #[test]
1623    fn rust_nested_functions() {
1624        check_metrics::<RustParser>(
1625            "fn f(a: i32, b: i32) -> i32 {
1626                 fn foo(a: i32) -> i32 {
1627                     return a;
1628                 }
1629                 let bar = |a: i32, b: i32| -> i32 { a + 1 };
1630                 let bar1 = |b: i32| -> i32 { b + 1 };
1631                 return bar(foo(a), a);
1632             }",
1633            "foo.rs",
1634            |metric| {
1635                // 2 functions + 2 lambdas = 4
1636                insta::assert_json_snapshot!(
1637                    metric.nargs,
1638                    @r#"
1639                {
1640                  "function_args": 3,
1641                  "closure_args": 3,
1642                  "function_args_average": 1.5,
1643                  "closure_args_average": 1.5,
1644                  "total": 6,
1645                  "value": 0,
1646                  "average": 1.5,
1647                  "function_args_min": 0,
1648                  "function_args_max": 2,
1649                  "closure_args_min": 0,
1650                  "closure_args_max": 2
1651                }
1652                "#
1653                );
1654            },
1655        );
1656    }
1657
1658    #[cfg(feature = "cpp")]
1659    #[test]
1660    fn cpp_nested_functions() {
1661        check_metrics::<CppParser>(
1662            "int f(int a, int b, int c) {
1663                 auto foo = [](int x) -> int { return x; };
1664                 auto bar = [](int x, int y) -> int { return x + y; };
1665                 return bar(foo(a), a);
1666             }",
1667            "foo.cpp",
1668            |metric| {
1669                // 1 functions + 2 lambdas = 3
1670                insta::assert_json_snapshot!(
1671                    metric.nargs,
1672                    @r#"
1673                {
1674                  "function_args": 3,
1675                  "closure_args": 3,
1676                  "function_args_average": 3.0,
1677                  "closure_args_average": 1.5,
1678                  "total": 6,
1679                  "value": 0,
1680                  "average": 2.0,
1681                  "function_args_min": 0,
1682                  "function_args_max": 3,
1683                  "closure_args_min": 0,
1684                  "closure_args_max": 3
1685                }
1686                "#
1687                );
1688            },
1689        );
1690    }
1691
1692    /// Default arguments still surface as separate `parameter_declaration`
1693    /// nodes — defaults are not removed from the count.  A 3-param function
1694    /// whose third parameter has a default value reports `nargs = 3`.
1695    #[cfg(feature = "cpp")]
1696    #[test]
1697    fn cpp_default_arguments() {
1698        check_metrics::<CppParser>(
1699            "int f(int a, int b, int c = 0) {
1700                 return a + b + c;
1701             }",
1702            "foo.cpp",
1703            |metric| {
1704                // 1 function, 3 parameters (defaults still count).
1705                let s = &metric.nargs;
1706                assert_eq!(s.function_args_sum(), 3);
1707                assert_eq!(s.function_args_max(), 3);
1708                insta::assert_json_snapshot!(
1709                    metric.nargs,
1710                    @r#"
1711                {
1712                  "function_args": 3,
1713                  "closure_args": 0,
1714                  "function_args_average": 3.0,
1715                  "closure_args_average": 0.0,
1716                  "total": 3,
1717                  "value": 0,
1718                  "average": 3.0,
1719                  "function_args_min": 0,
1720                  "function_args_max": 3,
1721                  "closure_args_min": 0,
1722                  "closure_args_max": 0
1723                }
1724                "#
1725                );
1726            },
1727        );
1728    }
1729
1730    /// C-style variadic `...` parameter contributes +1 (one named declarator
1731    /// plus the `...` declarator).  The grammar emits the variadic ellipsis
1732    /// as a sibling parameter node that `count_args` counts, because it is
1733    /// neither a comment nor one of the `(`, `)`, `,` tokens `CCode::is_non_arg`
1734    /// rejects.
1735    #[cfg(feature = "c")]
1736    #[test]
1737    fn c_variadic_function() {
1738        check_metrics::<CParser>(
1739            "int printf(const char* fmt, ...) {
1740                 return 0;
1741             }",
1742            "foo.c",
1743            |metric| {
1744                // 1 function, 2 nargs: `fmt` and `...`
1745                let s = &metric.nargs;
1746                assert_eq!(s.function_args_sum(), 2);
1747                assert_eq!(s.function_args_max(), 2);
1748                insta::assert_json_snapshot!(
1749                    metric.nargs,
1750                    @r#"
1751                {
1752                  "function_args": 2,
1753                  "closure_args": 0,
1754                  "function_args_average": 2.0,
1755                  "closure_args_average": 0.0,
1756                  "total": 2,
1757                  "value": 0,
1758                  "average": 2.0,
1759                  "function_args_min": 0,
1760                  "function_args_max": 2,
1761                  "closure_args_min": 0,
1762                  "closure_args_max": 0
1763                }
1764                "#
1765                );
1766            },
1767        );
1768    }
1769
1770    /// C++ template parameter packs (`Args... args`) count as one runtime
1771    /// parameter (the parameter pack itself), not as N — the template
1772    /// arguments are compile-time and live on the template-parameter list,
1773    /// not on `parameters`.  The tree-sitter-cpp grammar represents
1774    /// `Args... args` as a single `variadic_parameter_declaration` under
1775    /// `parameters`.
1776    #[cfg(feature = "cpp")]
1777    #[test]
1778    fn cpp_template_parameter_pack() {
1779        check_metrics::<CppParser>(
1780            "template<typename... Args>
1781             int sum(int seed, Args... args) {
1782                 return seed;
1783             }",
1784            "foo.cpp",
1785            |metric| {
1786                // 1 function, 2 nargs: `seed` and `Args... args`
1787                let s = &metric.nargs;
1788                assert_eq!(s.function_args_sum(), 2);
1789                assert_eq!(s.function_args_max(), 2);
1790                insta::assert_json_snapshot!(
1791                    metric.nargs,
1792                    @r#"
1793                {
1794                  "function_args": 2,
1795                  "closure_args": 0,
1796                  "function_args_average": 2.0,
1797                  "closure_args_average": 0.0,
1798                  "total": 2,
1799                  "value": 0,
1800                  "average": 2.0,
1801                  "function_args_min": 0,
1802                  "function_args_max": 2,
1803                  "closure_args_min": 0,
1804                  "closure_args_max": 0
1805                }
1806                "#
1807                );
1808            },
1809        );
1810    }
1811
1812    /// Lambda capture list (`[=, &x]`) is not part of the parameter list.
1813    /// `compute_args` reads the `declarator` field, which only contains the
1814    /// `( … )` parameter list.  Variables captured for the closure body do
1815    /// not inflate `nargs`.
1816    #[cfg(feature = "cpp")]
1817    #[test]
1818    fn cpp_lambda_capture_not_counted() {
1819        check_metrics::<CppParser>(
1820            "int f() {
1821                 int x = 1;
1822                 int y = 2;
1823                 auto g = [=, &x](int a, int b) -> int { return a + b + x + y; };
1824                 return g(1, 2);
1825             }",
1826            "foo.cpp",
1827            |metric| {
1828                // 1 function (0 args), 1 lambda (2 args: a, b — captures `=, &x` excluded).
1829                let s = &metric.nargs;
1830                assert_eq!(s.function_args_sum(), 0);
1831                assert_eq!(s.closure_args_sum(), 2);
1832                assert_eq!(s.closure_args_max(), 2);
1833                insta::assert_json_snapshot!(
1834                    metric.nargs,
1835                    @r#"
1836                {
1837                  "function_args": 0,
1838                  "closure_args": 2,
1839                  "function_args_average": 0.0,
1840                  "closure_args_average": 2.0,
1841                  "total": 2,
1842                  "value": 0,
1843                  "average": 1.0,
1844                  "function_args_min": 0,
1845                  "function_args_max": 0,
1846                  "closure_args_min": 0,
1847                  "closure_args_max": 2
1848                }
1849                "#
1850                );
1851            },
1852        );
1853    }
1854
1855    /// Implicit `this` on a member function is not part of the AST
1856    /// parameter list — it is an implicit argument at the language level
1857    /// only.  A non-static member function `void M(int a)` reports
1858    /// `nargs = 1`, not 2.
1859    #[cfg(feature = "cpp")]
1860    #[test]
1861    fn cpp_member_function_this_not_counted() {
1862        check_metrics::<CppParser>(
1863            "struct S {
1864                 int x;
1865                 int set(int a) {     // implicit `this` is NOT counted
1866                     this->x = a;
1867                     return a;
1868                 }
1869             };",
1870            "foo.cpp",
1871            |metric| {
1872                // 1 member function with 1 explicit parameter `a`.
1873                let s = &metric.nargs;
1874                assert_eq!(s.function_args_sum(), 1);
1875                assert_eq!(s.function_args_max(), 1);
1876                insta::assert_json_snapshot!(
1877                    metric.nargs,
1878                    @r#"
1879                {
1880                  "function_args": 1,
1881                  "closure_args": 0,
1882                  "function_args_average": 1.0,
1883                  "closure_args_average": 0.0,
1884                  "total": 1,
1885                  "value": 0,
1886                  "average": 1.0,
1887                  "function_args_min": 0,
1888                  "function_args_max": 1,
1889                  "closure_args_min": 0,
1890                  "closure_args_max": 0
1891                }
1892                "#
1893                );
1894            },
1895        );
1896    }
1897
1898    #[cfg(feature = "go")]
1899    #[test]
1900    fn go_zero_args() {
1901        check_metrics::<GoParser>(
1902            "package main
1903            func f() {}",
1904            "foo.go",
1905            |metric| {
1906                insta::assert_json_snapshot!(
1907                    metric.nargs,
1908                    @r#"
1909                {
1910                  "function_args": 0,
1911                  "closure_args": 0,
1912                  "function_args_average": 0.0,
1913                  "closure_args_average": 0.0,
1914                  "total": 0,
1915                  "value": 0,
1916                  "average": 0.0,
1917                  "function_args_min": 0,
1918                  "function_args_max": 0,
1919                  "closure_args_min": 0,
1920                  "closure_args_max": 0
1921                }
1922                "#
1923                );
1924            },
1925        );
1926    }
1927
1928    #[cfg(feature = "go")]
1929    #[test]
1930    fn go_multiple_args() {
1931        check_metrics::<GoParser>(
1932            "package main
1933            func f(a int, b string, c bool) {}",
1934            "foo.go",
1935            |metric| {
1936                insta::assert_json_snapshot!(
1937                    metric.nargs,
1938                    @r#"
1939                {
1940                  "function_args": 3,
1941                  "closure_args": 0,
1942                  "function_args_average": 3.0,
1943                  "closure_args_average": 0.0,
1944                  "total": 3,
1945                  "value": 0,
1946                  "average": 3.0,
1947                  "function_args_min": 0,
1948                  "function_args_max": 3,
1949                  "closure_args_min": 0,
1950                  "closure_args_max": 0
1951                }
1952                "#
1953                );
1954            },
1955        );
1956    }
1957
1958    #[cfg(feature = "go")]
1959    #[test]
1960    fn go_method_excludes_receiver() {
1961        check_metrics::<GoParser>(
1962            "package main
1963            type T struct{}
1964            func (t *T) Greet(name string) string {
1965                return name
1966            }",
1967            "foo.go",
1968            |metric| {
1969                // Receiver is in a separate `receiver` field and is not counted.
1970                insta::assert_json_snapshot!(
1971                    metric.nargs,
1972                    @r#"
1973                {
1974                  "function_args": 1,
1975                  "closure_args": 0,
1976                  "function_args_average": 1.0,
1977                  "closure_args_average": 0.0,
1978                  "total": 1,
1979                  "value": 0,
1980                  "average": 1.0,
1981                  "function_args_min": 0,
1982                  "function_args_max": 1,
1983                  "closure_args_min": 0,
1984                  "closure_args_max": 0
1985                }
1986                "#
1987                );
1988            },
1989        );
1990    }
1991
1992    #[cfg(feature = "go")]
1993    #[test]
1994    fn go_variadic() {
1995        check_metrics::<GoParser>(
1996            "package main
1997            func f(args ...int) {}",
1998            "foo.go",
1999            |metric| {
2000                insta::assert_json_snapshot!(
2001                    metric.nargs,
2002                    @r#"
2003                {
2004                  "function_args": 1,
2005                  "closure_args": 0,
2006                  "function_args_average": 1.0,
2007                  "closure_args_average": 0.0,
2008                  "total": 1,
2009                  "value": 0,
2010                  "average": 1.0,
2011                  "function_args_min": 0,
2012                  "function_args_max": 1,
2013                  "closure_args_min": 0,
2014                  "closure_args_max": 0
2015                }
2016                "#
2017                );
2018            },
2019        );
2020    }
2021
2022    #[cfg(feature = "go")]
2023    #[test]
2024    fn go_grouped_params() {
2025        check_metrics::<GoParser>(
2026            "package main
2027            func f(a, b int, c string) {}",
2028            "foo.go",
2029            |metric| {
2030                // `a, b int` is one parameter_declaration with two `name`
2031                // children — semantically two parameters.
2032                insta::assert_json_snapshot!(
2033                    metric.nargs,
2034                    @r#"
2035                {
2036                  "function_args": 3,
2037                  "closure_args": 0,
2038                  "function_args_average": 3.0,
2039                  "closure_args_average": 0.0,
2040                  "total": 3,
2041                  "value": 0,
2042                  "average": 3.0,
2043                  "function_args_min": 0,
2044                  "function_args_max": 3,
2045                  "closure_args_min": 0,
2046                  "closure_args_max": 0
2047                }
2048                "#
2049                );
2050            },
2051        );
2052    }
2053
2054    #[cfg(feature = "go")]
2055    #[test]
2056    fn go_func_literal_args() {
2057        check_metrics::<GoParser>(
2058            "package main
2059            var f = func(x, y int) int { return x + y }",
2060            "foo.go",
2061            |metric| {
2062                // Closure with grouped params: `x, y int` -> 2 closure args.
2063                insta::assert_json_snapshot!(
2064                    metric.nargs,
2065                    @r#"
2066                {
2067                  "function_args": 0,
2068                  "closure_args": 2,
2069                  "function_args_average": 0.0,
2070                  "closure_args_average": 2.0,
2071                  "total": 2,
2072                  "value": 0,
2073                  "average": 2.0,
2074                  "function_args_min": 0,
2075                  "function_args_max": 0,
2076                  "closure_args_min": 0,
2077                  "closure_args_max": 2
2078                }
2079                "#
2080                );
2081            },
2082        );
2083    }
2084
2085    #[cfg(feature = "javascript")]
2086    #[test]
2087    fn javascript_nested_functions() {
2088        check_metrics::<JavascriptParser>(
2089            "function f(a, b) {
2090                 function foo(a, c) {
2091                     return a;
2092                 }
2093                 var bar = function (a, b) {return a + b};
2094                 function (a) {return a};
2095                 return bar(foo(a), a);
2096             }",
2097            "foo.js",
2098            |metric| {
2099                // 3 functions + 1 lambdas = 4
2100                insta::assert_json_snapshot!(
2101                    metric.nargs,
2102                    @r#"
2103                {
2104                  "function_args": 6,
2105                  "closure_args": 1,
2106                  "function_args_average": 2.0,
2107                  "closure_args_average": 1.0,
2108                  "total": 7,
2109                  "value": 0,
2110                  "average": 1.75,
2111                  "function_args_min": 0,
2112                  "function_args_max": 2,
2113                  "closure_args_min": 0,
2114                  "closure_args_max": 1
2115                }
2116                "#
2117                );
2118            },
2119        );
2120    }
2121
2122    #[cfg(feature = "perl")]
2123    #[test]
2124    fn perl_no_functions_and_closures() {
2125        check_metrics::<PerlParser>(
2126            "my $x = 1;
2127             print $x;",
2128            "foo.pl",
2129            |metric| {
2130                // Cross-check via nom that no spurious sub/closure was
2131                // recognised — symmetric with the other `perl_*` nargs
2132                // tests, and would catch a regression that miscounted
2133                // `print` (or similar) as a function.
2134                assert_eq!(metric.nom.functions_sum(), 0);
2135                assert_eq!(metric.nom.closures_sum(), 0);
2136                insta::assert_json_snapshot!(
2137                    metric.nargs,
2138                    @r#"
2139                {
2140                  "function_args": 0,
2141                  "closure_args": 0,
2142                  "function_args_average": 0.0,
2143                  "closure_args_average": 0.0,
2144                  "total": 0,
2145                  "value": 0,
2146                  "average": 0.0,
2147                  "function_args_min": 0,
2148                  "function_args_max": 0,
2149                  "closure_args_min": 0,
2150                  "closure_args_max": 0
2151                }
2152                "#
2153                );
2154            },
2155        );
2156    }
2157
2158    #[cfg(feature = "perl")]
2159    #[test]
2160    fn perl_single_function() {
2161        // This sub declares no signature, so it has no formal parameters to
2162        // count and nargs is 0 — args arrive via `@_`. Signature-carrying
2163        // subs are counted; see `perl_signature_function`. To make sure the
2164        // test still discriminates "function parsed" from "function silently
2165        // dropped", also assert nom recognised exactly one function.
2166        check_metrics::<PerlParser>(
2167            "sub greet {
2168                my ($name) = @_;
2169                print \"hi $name\";
2170            }",
2171            "foo.pl",
2172            |metric| {
2173                assert_eq!(metric.nom.functions_sum(), 1);
2174                assert_eq!(metric.nom.closures_sum(), 0);
2175                insta::assert_json_snapshot!(
2176                    metric.nargs,
2177                    @r#"
2178                {
2179                  "function_args": 0,
2180                  "closure_args": 0,
2181                  "function_args_average": 0.0,
2182                  "closure_args_average": 0.0,
2183                  "total": 0,
2184                  "value": 0,
2185                  "average": 0.0,
2186                  "function_args_min": 0,
2187                  "function_args_max": 0,
2188                  "closure_args_min": 0,
2189                  "closure_args_max": 0
2190                }
2191                "#
2192                );
2193            },
2194        );
2195    }
2196
2197    #[cfg(feature = "perl")]
2198    #[test]
2199    fn perl_single_closure() {
2200        // This closure declares no signature, so nargs stays 0; it takes its
2201        // arguments through `@_`. A signature-carrying closure also reads 0,
2202        // for an unrelated upstream-grammar reason — see
2203        // `perl_anonymous_sub_signature_is_zero`. Assert via nom that the
2204        // anonymous function was actually identified as a closure.
2205        check_metrics::<PerlParser>(
2206            "my $f = sub {
2207                my ($x) = @_;
2208                return $x + 1;
2209            };",
2210            "foo.pl",
2211            |metric| {
2212                assert_eq!(metric.nom.functions_sum(), 0);
2213                assert_eq!(metric.nom.closures_sum(), 1);
2214                insta::assert_json_snapshot!(
2215                    metric.nargs,
2216                    @r#"
2217                {
2218                  "function_args": 0,
2219                  "closure_args": 0,
2220                  "function_args_average": 0.0,
2221                  "closure_args_average": 0.0,
2222                  "total": 0,
2223                  "value": 0,
2224                  "average": 0.0,
2225                  "function_args_min": 0,
2226                  "function_args_max": 0,
2227                  "closure_args_min": 0,
2228                  "closure_args_max": 0
2229                }
2230                "#
2231                );
2232            },
2233        );
2234    }
2235
2236    #[cfg(feature = "perl")]
2237    #[test]
2238    fn perl_multiple_functions() {
2239        // Neither sub declares a signature, so both count 0. Assert nom
2240        // counted both top-level subs so the test fails if either sub is
2241        // dropped.
2242        check_metrics::<PerlParser>(
2243            "sub a { return 1; }
2244             sub b {
2245                 my ($x, $y) = @_;
2246                 return $x + $y;
2247             }",
2248            "foo.pl",
2249            |metric| {
2250                assert_eq!(metric.nom.functions_sum(), 2);
2251                assert_eq!(metric.nom.closures_sum(), 0);
2252                insta::assert_json_snapshot!(
2253                    metric.nargs,
2254                    @r#"
2255                {
2256                  "function_args": 0,
2257                  "closure_args": 0,
2258                  "function_args_average": 0.0,
2259                  "closure_args_average": 0.0,
2260                  "total": 0,
2261                  "value": 0,
2262                  "average": 0.0,
2263                  "function_args_min": 0,
2264                  "function_args_max": 0,
2265                  "closure_args_min": 0,
2266                  "closure_args_max": 0
2267                }
2268                "#
2269                );
2270            },
2271        );
2272    }
2273
2274    #[cfg(feature = "perl")]
2275    #[test]
2276    fn perl_nested_closure() {
2277        // Neither the outer sub nor the nested closure declares a signature,
2278        // so both count 0. Assert nom recognised one outer sub plus one
2279        // nested closure.
2280        check_metrics::<PerlParser>(
2281            "sub outer {
2282                my $inner = sub { return 42; };
2283                return $inner->();
2284            }",
2285            "foo.pl",
2286            |metric| {
2287                assert_eq!(metric.nom.functions_sum(), 1);
2288                assert_eq!(metric.nom.closures_sum(), 1);
2289                insta::assert_json_snapshot!(
2290                    metric.nargs,
2291                    @r#"
2292                {
2293                  "function_args": 0,
2294                  "closure_args": 0,
2295                  "function_args_average": 0.0,
2296                  "closure_args_average": 0.0,
2297                  "total": 0,
2298                  "value": 0,
2299                  "average": 0.0,
2300                  "function_args_min": 0,
2301                  "function_args_max": 0,
2302                  "closure_args_min": 0,
2303                  "closure_args_max": 0
2304                }
2305                "#
2306                );
2307            },
2308        );
2309    }
2310
2311    /// Regression for #1147: a signature sub reported 0 because the
2312    /// signature is an unnamed `function_signature` child, not a
2313    /// `parameters` field.
2314    #[cfg(feature = "perl")]
2315    #[test]
2316    fn perl_signature_function() {
2317        check_metrics::<PerlParser>(
2318            "use feature 'signatures';
2319             sub add($x, $y) { return $x + $y; }",
2320            "foo.pl",
2321            |metric| {
2322                assert_eq!(metric.nom.functions_sum(), 1);
2323                let s = &metric.nargs;
2324                assert_eq!(s.function_args_sum(), 2);
2325                assert_eq!(s.function_args_max(), 2);
2326                insta::assert_json_snapshot!(
2327                    metric.nargs,
2328                    @r#"
2329                {
2330                  "function_args": 2,
2331                  "closure_args": 0,
2332                  "function_args_average": 2.0,
2333                  "closure_args_average": 0.0,
2334                  "total": 2,
2335                  "value": 0,
2336                  "average": 2.0,
2337                  "function_args_min": 0,
2338                  "function_args_max": 2,
2339                  "closure_args_min": 0,
2340                  "closure_args_max": 0
2341                }
2342                "#
2343                );
2344            },
2345        );
2346    }
2347
2348    /// A defaulted parameter is a `binary_expression`, not a bare
2349    /// `scalar_variable`, so counting only the variable kinds would report
2350    /// 2 here instead of 3. Pins the negative filter in
2351    /// `compute_perl_args` (#1147).
2352    #[cfg(feature = "perl")]
2353    #[test]
2354    fn perl_signature_defaults_and_slurpy() {
2355        check_metrics::<PerlParser>(
2356            "use feature 'signatures';
2357             sub deflt($x, $y = 5, @rest) { return $x; }",
2358            "foo.pl",
2359            |metric| {
2360                assert_eq!(metric.nom.functions_sum(), 1);
2361                let s = &metric.nargs;
2362                assert_eq!(s.function_args_sum(), 3);
2363                assert_eq!(s.function_args_max(), 3);
2364                insta::assert_json_snapshot!(
2365                    metric.nargs,
2366                    @r#"
2367                {
2368                  "function_args": 3,
2369                  "closure_args": 0,
2370                  "function_args_average": 3.0,
2371                  "closure_args_average": 0.0,
2372                  "total": 3,
2373                  "value": 0,
2374                  "average": 3.0,
2375                  "function_args_min": 0,
2376                  "function_args_max": 3,
2377                  "closure_args_min": 0,
2378                  "closure_args_max": 0
2379                }
2380                "#
2381                );
2382            },
2383        );
2384    }
2385
2386    /// A signature sub and an `@_` sub in one file: the min/max and the
2387    /// average have to keep the zero-argument sub in the divisor rather
2388    /// than folding it away.
2389    #[cfg(feature = "perl")]
2390    #[test]
2391    fn perl_signature_and_at_underscore_mixed() {
2392        check_metrics::<PerlParser>(
2393            "use feature 'signatures';
2394             sub sig($x, $y, $z) { return $x; }
2395             sub legacy { my ($a) = @_; return $a; }",
2396            "foo.pl",
2397            |metric| {
2398                assert_eq!(metric.nom.functions_sum(), 2);
2399                let s = &metric.nargs;
2400                assert_eq!(s.function_args_sum(), 3);
2401                assert_eq!(s.function_args_max(), 3);
2402                // 3 args over 2 functions: the zero-argument sub stays in
2403                // the divisor, so a fold that dropped it would read 3.0.
2404                insta::assert_json_snapshot!(
2405                    metric.nargs,
2406                    @r#"
2407                {
2408                  "function_args": 3,
2409                  "closure_args": 0,
2410                  "function_args_average": 1.5,
2411                  "closure_args_average": 0.0,
2412                  "total": 3,
2413                  "value": 0,
2414                  "average": 1.5,
2415                  "function_args_min": 0,
2416                  "function_args_max": 3,
2417                  "closure_args_min": 0,
2418                  "closure_args_max": 0
2419                }
2420                "#
2421                );
2422            },
2423        );
2424    }
2425
2426    /// Perl puts subroutine attributes before the signature
2427    /// (`sub NAME ATTRS SIG BLOCK`), and a bare attribute swallows the
2428    /// signature into its own `function_attribute` node. Pins the
2429    /// one-level descent in `perl_signature`.
2430    #[cfg(feature = "perl")]
2431    #[test]
2432    fn perl_signature_behind_attribute() {
2433        check_metrics::<PerlParser>(
2434            "use feature 'signatures';
2435             sub attrs :lvalue ($z) { return $z; }",
2436            "foo.pl",
2437            |metric| {
2438                assert_eq!(metric.nom.functions_sum(), 1);
2439                let s = &metric.nargs;
2440                assert_eq!(s.function_args_sum(), 1);
2441                assert_eq!(s.function_args_max(), 1);
2442                insta::assert_json_snapshot!(
2443                    metric.nargs,
2444                    @r#"
2445                {
2446                  "function_args": 1,
2447                  "closure_args": 0,
2448                  "function_args_average": 1.0,
2449                  "closure_args_average": 0.0,
2450                  "total": 1,
2451                  "value": 0,
2452                  "average": 1.0,
2453                  "function_args_min": 0,
2454                  "function_args_max": 1,
2455                  "closure_args_min": 0,
2456                  "closure_args_max": 0
2457                }
2458                "#
2459                );
2460            },
2461        );
2462    }
2463
2464    /// A multi-line signature documents its parameters with `comments`
2465    /// children sitting directly under `function_signature`, so the
2466    /// negative filter has to exclude them or a documented 3-parameter sub
2467    /// reads 6 and trips the default `nargs` limit of 5.
2468    #[cfg(feature = "perl")]
2469    #[test]
2470    fn perl_signature_comments_are_not_parameters() {
2471        check_metrics::<PerlParser>(
2472            "use feature 'signatures';
2473             sub documented(
2474                 $host,    # hostname to connect to
2475                 $port,    # TCP port
2476                 $timeout, # seconds
2477             ) { return $host; }",
2478            "foo.pl",
2479            |metric| {
2480                assert_eq!(metric.nom.functions_sum(), 1);
2481                let s = &metric.nargs;
2482                assert_eq!(s.function_args_sum(), 3);
2483                assert_eq!(s.function_args_max(), 3);
2484                insta::assert_json_snapshot!(
2485                    metric.nargs,
2486                    @r#"
2487                {
2488                  "function_args": 3,
2489                  "closure_args": 0,
2490                  "function_args_average": 3.0,
2491                  "closure_args_average": 0.0,
2492                  "total": 3,
2493                  "value": 0,
2494                  "average": 3.0,
2495                  "function_args_min": 0,
2496                  "function_args_max": 3,
2497                  "closure_args_min": 0,
2498                  "closure_args_max": 0
2499                }
2500                "#
2501                );
2502            },
2503        );
2504    }
2505
2506    /// The two shapes that must stay at 0 for reasons the counting rule
2507    /// depends on: an empty signature has no children but the parens, and
2508    /// a prototype (`($$)`) is a `function_prototype`, a different kind
2509    /// that `perl_signature` deliberately does not match.
2510    #[cfg(feature = "perl")]
2511    #[test]
2512    fn perl_empty_signature_and_prototype_are_zero() {
2513        check_metrics::<PerlParser>(
2514            "use feature 'signatures';
2515             sub empty() { return 1; }
2516             sub proto($$) { return 1; }",
2517            "foo.pl",
2518            |metric| {
2519                assert_eq!(metric.nom.functions_sum(), 2);
2520                let s = &metric.nargs;
2521                assert_eq!(s.function_args_sum(), 0);
2522                assert_eq!(s.function_args_max(), 0);
2523            },
2524        );
2525    }
2526
2527    /// Perl 5.38's `method` is a second `is_func` kind
2528    /// (`function_definition_without_sub`) reaching the same helper, so it
2529    /// gets its own fixture rather than riding on the `sub` tests.
2530    #[cfg(feature = "perl")]
2531    #[test]
2532    fn perl_method_signature_function() {
2533        check_metrics::<PerlParser>(
2534            "use v5.38;
2535             class Point {
2536                 method shift_by($dx, $dy) { return $dx; }
2537             }",
2538            "foo.pl",
2539            |metric| {
2540                let s = &metric.nargs;
2541                assert_eq!(s.function_args_sum(), 2);
2542                assert_eq!(s.function_args_max(), 2);
2543            },
2544        );
2545    }
2546
2547    /// `FunctionSignature2` is the hidden `_function_signature` supertype;
2548    /// `perl_signature` lists it defensively. Pin that the grammar never
2549    /// emits it, so a bump that promotes the rule fails loudly instead of
2550    /// changing behaviour invisibly (lesson 34).
2551    #[cfg(feature = "perl")]
2552    #[test]
2553    fn perl_hidden_function_signature_is_unreachable() {
2554        let mut hidden = false;
2555        let mut emitted = false;
2556        crate::test_support::for_each_node_with_chain::<PerlCode>(
2557            b"use feature 'signatures';\nsub add($x, $y) { return $x + $y; }\n",
2558            |node, _| {
2559                hidden |= node.kind_id() == Perl::FunctionSignature2 as u16;
2560                emitted |= node.kind_id() == Perl::FunctionSignature as u16;
2561            },
2562        );
2563        assert!(
2564            emitted,
2565            "fixture must reach a real `function_signature`, else the \
2566             hidden-rule check below is vacuous"
2567        );
2568        assert!(
2569            !hidden,
2570            "grammar now emits the hidden `_function_signature`; re-check \
2571             the defensive arm in `perl_signature`"
2572        );
2573    }
2574
2575    /// Upstream-grammar limitation, deliberately pinned: tree-sitter-perl
2576    /// 1.1.2 parses an anonymous sub's signature inside an `ERROR` node,
2577    /// so a signature-carrying closure counts 0. A grammar bump that fixes
2578    /// the parse should fail this test rather than shift metrics silently.
2579    #[cfg(feature = "perl")]
2580    #[test]
2581    fn perl_anonymous_sub_signature_is_zero() {
2582        check_metrics::<PerlParser>(
2583            "use feature 'signatures';
2584             my $mul = sub ($p, $q) { return $p * $q; };",
2585            "foo.pl",
2586            |metric| {
2587                assert_eq!(metric.nom.functions_sum(), 0);
2588                assert_eq!(metric.nom.closures_sum(), 1);
2589                let s = &metric.nargs;
2590                assert_eq!(s.closure_args_sum(), 0);
2591                assert_eq!(s.closure_args_max(), 0);
2592                insta::assert_json_snapshot!(
2593                    metric.nargs,
2594                    @r#"
2595                {
2596                  "function_args": 0,
2597                  "closure_args": 0,
2598                  "function_args_average": 0.0,
2599                  "closure_args_average": 0.0,
2600                  "total": 0,
2601                  "value": 0,
2602                  "average": 0.0,
2603                  "function_args_min": 0,
2604                  "function_args_max": 0,
2605                  "closure_args_min": 0,
2606                  "closure_args_max": 0
2607                }
2608                "#
2609                );
2610            },
2611        );
2612    }
2613
2614    #[cfg(feature = "java")]
2615    #[test]
2616    fn java_no_functions() {
2617        check_metrics::<JavaParser>(
2618            "class Foo {
2619                 int x = 42;
2620                 String name = \"hello\";
2621             }",
2622            "foo.java",
2623            |metric| {
2624                insta::assert_json_snapshot!(
2625                    metric.nargs,
2626                    @r#"
2627                {
2628                  "function_args": 0,
2629                  "closure_args": 0,
2630                  "function_args_average": 0.0,
2631                  "closure_args_average": 0.0,
2632                  "total": 0,
2633                  "value": 0,
2634                  "average": 0.0,
2635                  "function_args_min": 0,
2636                  "function_args_max": 0,
2637                  "closure_args_min": 0,
2638                  "closure_args_max": 0
2639                }
2640                "#
2641                );
2642            },
2643        );
2644    }
2645
2646    #[cfg(feature = "java")]
2647    #[test]
2648    fn java_single_method() {
2649        check_metrics::<JavaParser>(
2650            "class Foo {
2651                 void greet(String name, int count) {
2652                     return;
2653                 }
2654             }",
2655            "foo.java",
2656            |metric| {
2657                insta::assert_json_snapshot!(
2658                    metric.nargs,
2659                    @r#"
2660                {
2661                  "function_args": 2,
2662                  "closure_args": 0,
2663                  "function_args_average": 2.0,
2664                  "closure_args_average": 0.0,
2665                  "total": 2,
2666                  "value": 0,
2667                  "average": 2.0,
2668                  "function_args_min": 0,
2669                  "function_args_max": 2,
2670                  "closure_args_min": 0,
2671                  "closure_args_max": 0
2672                }
2673                "#
2674                );
2675            },
2676        );
2677    }
2678
2679    #[cfg(feature = "java")]
2680    #[test]
2681    fn java_multiple_methods() {
2682        check_metrics::<JavaParser>(
2683            "class Foo {
2684                 void a(int x) {
2685                     return;
2686                 }
2687                 void b(int x, int y, int z) {
2688                     return;
2689                 }
2690             }",
2691            "foo.java",
2692            |metric| {
2693                insta::assert_json_snapshot!(
2694                    metric.nargs,
2695                    @r#"
2696                {
2697                  "function_args": 4,
2698                  "closure_args": 0,
2699                  "function_args_average": 2.0,
2700                  "closure_args_average": 0.0,
2701                  "total": 4,
2702                  "value": 0,
2703                  "average": 2.0,
2704                  "function_args_min": 0,
2705                  "function_args_max": 3,
2706                  "closure_args_min": 0,
2707                  "closure_args_max": 0
2708                }
2709                "#
2710                );
2711            },
2712        );
2713    }
2714
2715    #[cfg(feature = "java")]
2716    #[test]
2717    fn java_constructor_args() {
2718        check_metrics::<JavaParser>(
2719            "class Foo {
2720                 Foo(String name, int age) {
2721                     return;
2722                 }
2723             }",
2724            "foo.java",
2725            |metric| {
2726                insta::assert_json_snapshot!(
2727                    metric.nargs,
2728                    @r#"
2729                {
2730                  "function_args": 2,
2731                  "closure_args": 0,
2732                  "function_args_average": 2.0,
2733                  "closure_args_average": 0.0,
2734                  "total": 2,
2735                  "value": 0,
2736                  "average": 2.0,
2737                  "function_args_min": 0,
2738                  "function_args_max": 2,
2739                  "closure_args_min": 0,
2740                  "closure_args_max": 0
2741                }
2742                "#
2743                );
2744            },
2745        );
2746    }
2747
2748    /// A record's compact constructor (`R { … }`, JLS 8.10.4) writes no
2749    /// formal parameter list: its parameters are the record's components,
2750    /// which the grammar hangs off the enclosing `record_declaration`.
2751    /// `nargs` reports the component count so the two spellings of one
2752    /// constructor agree — the canonical `R(int a, int b) { … }` reports
2753    /// 2, and so does the compact form (#1160).
2754    ///
2755    /// The records are nested, and carry *different* component counts, so
2756    /// the fixture pins which record each constructor resolved to. The
2757    /// lookup steps two ancestors up from the constructor; a version that
2758    /// walked to the outermost record instead would give `Single` 2 and
2759    /// total 4, and one that read the constructor node itself — which has
2760    /// no `parameters` field — would give 0.
2761    #[cfg(feature = "java")]
2762    #[test]
2763    fn java_record_compact_constructor_counts_record_components() {
2764        check_metrics::<JavaParser>(
2765            "record Pair(int a, int b) {
2766                 Pair { }
2767                 record Single(int c) {
2768                     Single { }
2769                 }
2770             }",
2771            "foo.java",
2772            |metric| {
2773                let s = &metric.nargs;
2774                // Two constructors carrying 2 and 1 arguments. Pre-fix,
2775                // `compact_constructor_declaration` was not a function at
2776                // all, so both the count and the sum were 0.
2777                assert_eq!(metric.nom.functions_sum(), 2);
2778                assert_eq!(s.function_args_sum(), 3);
2779                // A sum of 3 across two functions whose largest is 2 can
2780                // only be 2 + 1.
2781                assert_eq!(s.function_args_max(), 2);
2782                assert_eq!(s.closure_args_sum(), 0);
2783            },
2784        );
2785    }
2786
2787    /// Java's explicit receiver parameter (`void m(S this, int a)`, JLS
2788    /// 8.4.1) parses as a `receiver_parameter` node — distinct from a real
2789    /// `formal_parameter` — and binds `this`, not a value. Like Rust's
2790    /// `self_parameter` (#457), Go's `receiver` field, and C++'s implicit
2791    /// `this`, it must not be counted as a formal parameter (#470).
2792    #[cfg(feature = "java")]
2793    #[test]
2794    fn java_method_excludes_explicit_receiver() {
2795        check_metrics::<JavaParser>(
2796            "class S {
2797                 void m(S this, int a) {}   // receiver + 1 -> 1 arg
2798                 void n(int a, int b) {}     // control: 2 real params
2799                 void r(S this) {}           // receiver only  -> 0 args
2800             }",
2801            "foo.java",
2802            |metric| {
2803                // m:1 + n:2 + r:0. The two receiver parameters contribute
2804                // nothing. Pre-fix, the receivers inflated this to sum = 5
2805                // (m:2 + n:2 + r:1), max = 2. After #470: sum = 3, max = 2.
2806                let s = &metric.nargs;
2807                assert_eq!(s.function_args_sum(), 3);
2808                assert_eq!(s.function_args_max(), 2);
2809            },
2810        );
2811    }
2812
2813    #[cfg(feature = "java")]
2814    #[test]
2815    fn java_lambda_args() {
2816        check_metrics::<JavaParser>(
2817            "class Foo {
2818                 void run() {
2819                     Runnable r = (int a, int b) -> a + b;
2820                 }
2821             }",
2822            "foo.java",
2823            |metric| {
2824                insta::assert_json_snapshot!(
2825                    metric.nargs,
2826                    @r#"
2827                {
2828                  "function_args": 0,
2829                  "closure_args": 2,
2830                  "function_args_average": 0.0,
2831                  "closure_args_average": 2.0,
2832                  "total": 2,
2833                  "value": 0,
2834                  "average": 1.0,
2835                  "function_args_min": 0,
2836                  "function_args_max": 0,
2837                  "closure_args_min": 0,
2838                  "closure_args_max": 2
2839                }
2840                "#
2841                );
2842            },
2843        );
2844    }
2845
2846    #[cfg(feature = "groovy")]
2847    #[test]
2848    fn groovy_no_functions_and_closures() {
2849        check_metrics::<GroovyParser>("int x = 1", "foo.groovy", |metric| {
2850            assert_eq!(metric.nargs.total(), 0);
2851        });
2852    }
2853
2854    #[cfg(feature = "groovy")]
2855    #[test]
2856    fn groovy_single_method() {
2857        check_metrics::<GroovyParser>(
2858            "class A {
2859                void greet(String name, int times) {
2860                    println(name)
2861                }
2862            }",
2863            "foo.groovy",
2864            |metric| {
2865                assert_eq!(metric.nargs.function_args_sum(), 2);
2866                assert_eq!(metric.nargs.closure_args_sum(), 0);
2867            },
2868        );
2869    }
2870
2871    #[cfg(feature = "groovy")]
2872    #[test]
2873    fn groovy_multiple_methods() {
2874        check_metrics::<GroovyParser>(
2875            "class A {
2876                int add(int x, int y) { x + y }
2877                int sub(int x, int y, int z) { x - y - z }
2878            }",
2879            "foo.groovy",
2880            |metric| {
2881                assert_eq!(metric.nargs.function_args_sum(), 5);
2882            },
2883        );
2884    }
2885
2886    #[cfg(feature = "groovy")]
2887    #[test]
2888    fn groovy_lambda_args() {
2889        // Two-parameter Groovy closure inside a method body. Groovy's
2890        // primary lambda-shaped construct is the closure
2891        // (`{ params -> body }`); the dekobon grammar does not model
2892        // Java's `(params) -> body` arrow form because real-world
2893        // Groovy code rarely uses it.
2894        check_metrics::<GroovyParser>(
2895            "class Foo {
2896                void run() {
2897                    def f = { int a, int b -> a + b }
2898                }
2899            }",
2900            "foo.groovy",
2901            |metric| {
2902                assert_eq!(metric.nargs.closure_args_sum(), 2);
2903            },
2904        );
2905    }
2906
2907    #[cfg(feature = "groovy")]
2908    #[test]
2909    fn groovy_implicit_it_not_counted() {
2910        // The `it` implicit closure parameter is just an identifier in
2911        // the grammar — no `formal_parameters` node. `nargs` counts
2912        // declared parameters only, so this closure has 0.
2913        check_metrics::<GroovyParser>(
2914            "class A {
2915                void apply() {
2916                    [1, 2, 3].each { println(it) }
2917                }
2918            }",
2919            "foo.groovy",
2920            |metric| {
2921                assert_eq!(metric.nargs.closure_args_sum(), 0);
2922            },
2923        );
2924    }
2925
2926    #[cfg(feature = "csharp")]
2927    #[test]
2928    fn csharp_no_functions() {
2929        check_metrics::<CsharpParser>(
2930            "class Foo {
2931                 int x = 42;
2932                 string Name = \"hello\";
2933             }",
2934            "foo.cs",
2935            |metric| {
2936                insta::assert_json_snapshot!(
2937                    metric.nargs,
2938                    @r#"
2939                {
2940                  "function_args": 0,
2941                  "closure_args": 0,
2942                  "function_args_average": 0.0,
2943                  "closure_args_average": 0.0,
2944                  "total": 0,
2945                  "value": 0,
2946                  "average": 0.0,
2947                  "function_args_min": 0,
2948                  "function_args_max": 0,
2949                  "closure_args_min": 0,
2950                  "closure_args_max": 0
2951                }
2952                "#
2953                );
2954            },
2955        );
2956    }
2957
2958    #[cfg(feature = "csharp")]
2959    #[test]
2960    fn csharp_single_method() {
2961        check_metrics::<CsharpParser>(
2962            "class Foo {
2963                 void Greet(string name, int count) {
2964                     return;
2965                 }
2966             }",
2967            "foo.cs",
2968            |metric| {
2969                insta::assert_json_snapshot!(
2970                    metric.nargs,
2971                    @r#"
2972                {
2973                  "function_args": 2,
2974                  "closure_args": 0,
2975                  "function_args_average": 2.0,
2976                  "closure_args_average": 0.0,
2977                  "total": 2,
2978                  "value": 0,
2979                  "average": 2.0,
2980                  "function_args_min": 0,
2981                  "function_args_max": 2,
2982                  "closure_args_min": 0,
2983                  "closure_args_max": 0
2984                }
2985                "#
2986                );
2987            },
2988        );
2989    }
2990
2991    #[cfg(feature = "csharp")]
2992    #[test]
2993    fn csharp_multiple_methods() {
2994        check_metrics::<CsharpParser>(
2995            "class Foo {
2996                 void A(int x) {
2997                     return;
2998                 }
2999                 void B(int x, int y, int z) {
3000                     return;
3001                 }
3002             }",
3003            "foo.cs",
3004            |metric| {
3005                insta::assert_json_snapshot!(
3006                    metric.nargs,
3007                    @r#"
3008                {
3009                  "function_args": 4,
3010                  "closure_args": 0,
3011                  "function_args_average": 2.0,
3012                  "closure_args_average": 0.0,
3013                  "total": 4,
3014                  "value": 0,
3015                  "average": 2.0,
3016                  "function_args_min": 0,
3017                  "function_args_max": 3,
3018                  "closure_args_min": 0,
3019                  "closure_args_max": 0
3020                }
3021                "#
3022                );
3023            },
3024        );
3025    }
3026
3027    #[cfg(feature = "csharp")]
3028    #[test]
3029    fn csharp_constructor_args() {
3030        check_metrics::<CsharpParser>(
3031            "class Foo {
3032                 public Foo(string name, int age) {
3033                     return;
3034                 }
3035             }",
3036            "foo.cs",
3037            |metric| {
3038                insta::assert_json_snapshot!(
3039                    metric.nargs,
3040                    @r#"
3041                {
3042                  "function_args": 2,
3043                  "closure_args": 0,
3044                  "function_args_average": 2.0,
3045                  "closure_args_average": 0.0,
3046                  "total": 2,
3047                  "value": 0,
3048                  "average": 2.0,
3049                  "function_args_min": 0,
3050                  "function_args_max": 2,
3051                  "closure_args_min": 0,
3052                  "closure_args_max": 0
3053                }
3054                "#
3055                );
3056            },
3057        );
3058    }
3059
3060    #[cfg(feature = "csharp")]
3061    #[test]
3062    fn csharp_lambda_args() {
3063        check_metrics::<CsharpParser>(
3064            "class Foo {
3065                 void Run() {
3066                     System.Func<int, int, int> f = (int a, int b) => a + b;
3067                 }
3068             }",
3069            "foo.cs",
3070            |metric| {
3071                insta::assert_json_snapshot!(
3072                    metric.nargs,
3073                    @r#"
3074                {
3075                  "function_args": 0,
3076                  "closure_args": 2,
3077                  "function_args_average": 0.0,
3078                  "closure_args_average": 2.0,
3079                  "total": 2,
3080                  "value": 0,
3081                  "average": 1.0,
3082                  "function_args_min": 0,
3083                  "function_args_max": 0,
3084                  "closure_args_min": 0,
3085                  "closure_args_max": 2
3086                }
3087                "#
3088                );
3089            },
3090        );
3091    }
3092
3093    #[cfg(feature = "typescript")]
3094    #[test]
3095    fn tsx_function_and_arrow() {
3096        check_metrics::<TsxParser>(
3097            "function add(a: number, b: number): number {
3098                 return a + b;
3099             }
3100             const multiply = (x: number, y: number) => x * y;",
3101            "foo.tsx",
3102            |metric| {
3103                insta::assert_json_snapshot!(
3104                    metric.nargs,
3105                    @r#"
3106                {
3107                  "function_args": 4,
3108                  "closure_args": 0,
3109                  "function_args_average": 2.0,
3110                  "closure_args_average": 0.0,
3111                  "total": 4,
3112                  "value": 0,
3113                  "average": 2.0,
3114                  "function_args_min": 0,
3115                  "function_args_max": 2,
3116                  "closure_args_min": 0,
3117                  "closure_args_max": 0
3118                }
3119                "#
3120                );
3121            },
3122        );
3123    }
3124
3125    #[cfg(feature = "typescript")]
3126    #[test]
3127    fn typescript_typed_and_optional_params() {
3128        check_metrics::<TypescriptParser>(
3129            "function format(value: number, prefix?: string, suffix?: string): string {
3130                 return (prefix ?? '') + value.toString() + (suffix ?? '');
3131             }
3132             const identity = (x: number): number => x;",
3133            "foo.ts",
3134            |metric| {
3135                insta::assert_json_snapshot!(
3136                    metric.nargs,
3137                    @r#"
3138                {
3139                  "function_args": 4,
3140                  "closure_args": 0,
3141                  "function_args_average": 2.0,
3142                  "closure_args_average": 0.0,
3143                  "total": 4,
3144                  "value": 0,
3145                  "average": 2.0,
3146                  "function_args_min": 0,
3147                  "function_args_max": 3,
3148                  "closure_args_min": 0,
3149                  "closure_args_max": 0
3150                }
3151                "#
3152                );
3153            },
3154        );
3155    }
3156
3157    #[cfg(feature = "mozjs")]
3158    #[test]
3159    fn mozjs_single_function() {
3160        check_metrics::<MozjsParser>(
3161            "function f(a, b) {
3162                 return a * b;
3163             }",
3164            "foo.js",
3165            |metric| {
3166                insta::assert_json_snapshot!(
3167                    metric.nargs,
3168                    @r#"
3169                {
3170                  "function_args": 2,
3171                  "closure_args": 0,
3172                  "function_args_average": 2.0,
3173                  "closure_args_average": 0.0,
3174                  "total": 2,
3175                  "value": 0,
3176                  "average": 2.0,
3177                  "function_args_min": 0,
3178                  "function_args_max": 2,
3179                  "closure_args_min": 0,
3180                  "closure_args_max": 0
3181                }
3182                "#
3183                );
3184            },
3185        );
3186    }
3187
3188    #[cfg(feature = "mozjs")]
3189    #[test]
3190    fn mozjs_closure_args() {
3191        check_metrics::<MozjsParser>("function (a, b) {return a + b};", "foo.js", |metric| {
3192            insta::assert_json_snapshot!(
3193                metric.nargs,
3194                @r#"
3195            {
3196              "function_args": 0,
3197              "closure_args": 2,
3198              "function_args_average": 0.0,
3199              "closure_args_average": 2.0,
3200              "total": 2,
3201              "value": 0,
3202              "average": 2.0,
3203              "function_args_min": 0,
3204              "function_args_max": 0,
3205              "closure_args_min": 0,
3206              "closure_args_max": 2
3207            }
3208            "#
3209            );
3210        });
3211    }
3212
3213    // Regression tests for issue #77: bare-identifier arrow functions
3214    // (`x => x`) use the singular `parameter` field instead of the plural
3215    // `parameters` field, and were previously counted as nargs=0.
3216    //
3217    // `total` is used so the assertion is independent of whether the
3218    // arrow function is classified as a function or a closure (this depends
3219    // on its enclosing context — e.g. a `VariableDeclarator` ancestor makes
3220    // it a function).
3221
3222    #[cfg(feature = "javascript")]
3223    #[test]
3224    fn javascript_bare_arrow_function() {
3225        check_metrics::<JavascriptParser>("const f = x => x;", "foo.js", |metric| {
3226            assert_eq!(metric.nargs.total(), 1);
3227        });
3228    }
3229
3230    #[cfg(feature = "javascript")]
3231    #[test]
3232    fn javascript_async_bare_arrow_function() {
3233        check_metrics::<JavascriptParser>("const f = async x => x;", "foo.js", |metric| {
3234            assert_eq!(metric.nargs.total(), 1);
3235        });
3236    }
3237
3238    #[cfg(feature = "javascript")]
3239    #[test]
3240    fn javascript_parenthesized_arrow_function() {
3241        check_metrics::<JavascriptParser>("const f = (x) => x;", "foo.js", |metric| {
3242            assert_eq!(metric.nargs.total(), 1);
3243        });
3244    }
3245
3246    #[cfg(feature = "javascript")]
3247    #[test]
3248    fn javascript_multi_parenthesized_arrow_function() {
3249        check_metrics::<JavascriptParser>("const f = (x, y) => x + y;", "foo.js", |metric| {
3250            assert_eq!(metric.nargs.total(), 2);
3251        });
3252    }
3253
3254    #[cfg(feature = "typescript")]
3255    #[test]
3256    fn typescript_bare_arrow_function() {
3257        check_metrics::<TypescriptParser>("const f = x => x;", "foo.ts", |metric| {
3258            assert_eq!(metric.nargs.total(), 1);
3259        });
3260    }
3261
3262    #[cfg(feature = "typescript")]
3263    #[test]
3264    fn typescript_async_bare_arrow_function() {
3265        check_metrics::<TypescriptParser>("const f = async x => x;", "foo.ts", |metric| {
3266            assert_eq!(metric.nargs.total(), 1);
3267        });
3268    }
3269
3270    #[cfg(feature = "typescript")]
3271    #[test]
3272    fn typescript_parenthesized_arrow_function() {
3273        check_metrics::<TypescriptParser>("const f = (x: number) => x;", "foo.ts", |metric| {
3274            assert_eq!(metric.nargs.total(), 1);
3275        });
3276    }
3277
3278    #[cfg(feature = "typescript")]
3279    #[test]
3280    fn typescript_multi_parenthesized_arrow_function() {
3281        check_metrics::<TypescriptParser>(
3282            "const f = (x: number, y: number) => x + y;",
3283            "foo.ts",
3284            |metric| {
3285                assert_eq!(metric.nargs.total(), 2);
3286            },
3287        );
3288    }
3289
3290    #[cfg(feature = "typescript")]
3291    #[test]
3292    fn tsx_bare_arrow_function() {
3293        check_metrics::<TsxParser>("const f = x => x;", "foo.tsx", |metric| {
3294            assert_eq!(metric.nargs.total(), 1);
3295        });
3296    }
3297
3298    #[cfg(feature = "typescript")]
3299    #[test]
3300    fn tsx_async_bare_arrow_function() {
3301        check_metrics::<TsxParser>("const f = async x => x;", "foo.tsx", |metric| {
3302            assert_eq!(metric.nargs.total(), 1);
3303        });
3304    }
3305
3306    #[cfg(feature = "typescript")]
3307    #[test]
3308    fn tsx_parenthesized_arrow_function() {
3309        check_metrics::<TsxParser>("const f = (x: number) => x;", "foo.tsx", |metric| {
3310            assert_eq!(metric.nargs.total(), 1);
3311        });
3312    }
3313
3314    #[cfg(feature = "typescript")]
3315    #[test]
3316    fn tsx_multi_parenthesized_arrow_function() {
3317        check_metrics::<TsxParser>(
3318            "const f = (x: number, y: number) => x + y;",
3319            "foo.tsx",
3320            |metric| {
3321                assert_eq!(metric.nargs.total(), 2);
3322            },
3323        );
3324    }
3325
3326    #[cfg(feature = "mozjs")]
3327    #[test]
3328    fn mozjs_bare_arrow_function() {
3329        check_metrics::<MozjsParser>("const f = x => x;", "foo.js", |metric| {
3330            assert_eq!(metric.nargs.total(), 1);
3331        });
3332    }
3333
3334    #[cfg(feature = "mozjs")]
3335    #[test]
3336    fn mozjs_async_bare_arrow_function() {
3337        check_metrics::<MozjsParser>("const f = async x => x;", "foo.js", |metric| {
3338            assert_eq!(metric.nargs.total(), 1);
3339        });
3340    }
3341
3342    #[cfg(feature = "mozjs")]
3343    #[test]
3344    fn mozjs_parenthesized_arrow_function() {
3345        check_metrics::<MozjsParser>("const f = (x) => x;", "foo.js", |metric| {
3346            assert_eq!(metric.nargs.total(), 1);
3347        });
3348    }
3349
3350    #[cfg(feature = "mozjs")]
3351    #[test]
3352    fn mozjs_multi_parenthesized_arrow_function() {
3353        check_metrics::<MozjsParser>("const f = (x, y) => x + y;", "foo.js", |metric| {
3354            assert_eq!(metric.nargs.total(), 2);
3355        });
3356    }
3357
3358    #[cfg(feature = "kotlin")]
3359    #[test]
3360    fn kotlin_nargs_functions_and_closures() {
3361        check_metrics::<KotlinParser>(
3362            "fun add(a: Int, b: Int): Int {
3363                val transform = { x: Int, y: Int -> x + y }
3364                return transform(a, b)
3365            }",
3366            "foo.kt",
3367            |metric| {
3368                insta::assert_json_snapshot!(
3369                    metric.nargs,
3370                    @r#"
3371                {
3372                  "function_args": 2,
3373                  "closure_args": 2,
3374                  "function_args_average": 2.0,
3375                  "closure_args_average": 2.0,
3376                  "total": 4,
3377                  "value": 0,
3378                  "average": 2.0,
3379                  "function_args_min": 0,
3380                  "function_args_max": 2,
3381                  "closure_args_min": 0,
3382                  "closure_args_max": 2
3383                }
3384                "#
3385                );
3386            },
3387        );
3388    }
3389
3390    #[cfg(feature = "lua")]
3391    #[test]
3392    fn lua_no_functions_and_closures() {
3393        check_metrics::<LuaParser>("local x = 1", "foo.lua", |metric| {
3394            // No functions or closures: both halves are zero.
3395            assert_eq!(metric.nargs.function_args_sum(), 0);
3396            assert_eq!(metric.nargs.closure_args_sum(), 0);
3397            insta::assert_json_snapshot!(metric.nargs);
3398        });
3399    }
3400
3401    #[cfg(feature = "lua")]
3402    #[test]
3403    fn lua_single_function() {
3404        check_metrics::<LuaParser>("function f(a, b) return a + b end", "foo.lua", |metric| {
3405            // f(a, b) → fn_args_sum 2, no closures.
3406            assert_eq!(metric.nargs.function_args_sum(), 2);
3407            assert_eq!(metric.nargs.closure_args_sum(), 0);
3408            insta::assert_json_snapshot!(metric.nargs);
3409        });
3410    }
3411
3412    #[cfg(feature = "lua")]
3413    #[test]
3414    fn lua_single_closure() {
3415        check_metrics::<LuaParser>(
3416            "local f = function(a, b) return a + b end",
3417            "foo.lua",
3418            |metric| {
3419                // Anonymous `function(a, b)` bound via `local` → closure_args_sum 2.
3420                assert_eq!(metric.nargs.function_args_sum(), 0);
3421                assert_eq!(metric.nargs.closure_args_sum(), 2);
3422                insta::assert_json_snapshot!(metric.nargs);
3423            },
3424        );
3425    }
3426
3427    #[cfg(feature = "lua")]
3428    #[test]
3429    fn lua_functions() {
3430        check_metrics::<LuaParser>(
3431            "function f(a) return a end
3432function g(x, y, z) return x + y + z end",
3433            "foo.lua",
3434            |metric| {
3435                // f(a)=1 + g(x,y,z)=3 → fn_args_sum 4.
3436                assert_eq!(metric.nargs.function_args_sum(), 4);
3437                assert_eq!(metric.nargs.closure_args_sum(), 0);
3438                insta::assert_json_snapshot!(metric.nargs);
3439            },
3440        );
3441    }
3442
3443    #[cfg(feature = "lua")]
3444    #[test]
3445    fn lua_vararg_function() {
3446        // `...` is a vararg_expression node and counts as one argument.
3447        check_metrics::<LuaParser>("function f(a, ...) return a end", "foo.lua", |metric| {
3448            // a + ... → fn_args_sum 2.
3449            assert_eq!(metric.nargs.function_args_sum(), 2);
3450            assert_eq!(metric.nargs.closure_args_sum(), 0);
3451            insta::assert_json_snapshot!(metric.nargs);
3452        });
3453    }
3454
3455    #[cfg(feature = "lua")]
3456    #[test]
3457    fn lua_colon_method_nargs() {
3458        // Colon syntax: `self` is implicit and NOT in the `parameters` node.
3459        // Only explicit params (a, b) are counted.
3460        check_metrics::<LuaParser>(
3461            "function obj:method(a, b) return a + b end",
3462            "foo.lua",
3463            |metric| {
3464                // Only explicit a, b → fn_args_sum 2 (implicit self excluded).
3465                assert_eq!(metric.nargs.function_args_sum(), 2);
3466                assert_eq!(metric.nargs.closure_args_sum(), 0);
3467                insta::assert_json_snapshot!(metric.nargs);
3468            },
3469        );
3470    }
3471
3472    #[cfg(feature = "tcl")]
3473    #[test]
3474    fn tcl_no_functions() {
3475        check_metrics::<TclParser>("set x 1", "foo.tcl", |metric| {
3476            // Bare `set` command, no procs → both halves zero.
3477            assert_eq!(metric.nargs.function_args_sum(), 0);
3478            assert_eq!(metric.nargs.closure_args_sum(), 0);
3479            insta::assert_json_snapshot!(metric.nargs);
3480        });
3481    }
3482
3483    #[cfg(feature = "tcl")]
3484    #[test]
3485    fn tcl_single_function() {
3486        check_metrics::<TclParser>("proc f {a b} { puts $a }", "foo.tcl", |metric| {
3487            // proc f {a b} → fn_args_sum 2.
3488            assert_eq!(metric.nargs.function_args_sum(), 2);
3489            assert_eq!(metric.nargs.closure_args_sum(), 0);
3490            insta::assert_json_snapshot!(metric.nargs);
3491        });
3492    }
3493
3494    #[cfg(feature = "tcl")]
3495    #[test]
3496    fn tcl_single_function_no_args() {
3497        check_metrics::<TclParser>("proc f {} { puts hello }", "foo.tcl", |metric| {
3498            // proc f {} → empty arg list, fn_args_sum 0.
3499            assert_eq!(metric.nargs.function_args_sum(), 0);
3500            assert_eq!(metric.nargs.closure_args_sum(), 0);
3501            insta::assert_json_snapshot!(metric.nargs);
3502        });
3503    }
3504
3505    #[cfg(feature = "tcl")]
3506    #[test]
3507    fn tcl_functions() {
3508        check_metrics::<TclParser>(
3509            "proc f {a b} { puts $a }
3510proc g {x y z} { puts $x }",
3511            "foo.tcl",
3512            |metric| {
3513                // f(a,b)=2 + g(x,y,z)=3 → fn_args_sum 5.
3514                assert_eq!(metric.nargs.function_args_sum(), 5);
3515                assert_eq!(metric.nargs.closure_args_sum(), 0);
3516                insta::assert_json_snapshot!(metric.nargs);
3517            },
3518        );
3519    }
3520
3521    #[cfg(feature = "tcl")]
3522    #[test]
3523    fn tcl_nested_functions() {
3524        check_metrics::<TclParser>(
3525            "proc outer {a} {
3526    proc inner {x y} { puts $x }
3527    inner $a $a
3528}",
3529            "foo.tcl",
3530            |metric| {
3531                // outer(a)=1 + inner(x,y)=2 → fn_args_sum 3.
3532                assert_eq!(metric.nargs.function_args_sum(), 3);
3533                assert_eq!(metric.nargs.closure_args_sum(), 0);
3534                insta::assert_json_snapshot!(metric.nargs);
3535            },
3536        );
3537    }
3538
3539    #[cfg(feature = "tcl")]
3540    #[test]
3541    fn tcl_args_vararg() {
3542        // `args` is the Tcl variadic catch-all; it counts as one argument.
3543        check_metrics::<TclParser>("proc f {a b args} { puts $a }", "foo.tcl", |metric| {
3544            // a + b + args → fn_args_sum 3 (variadic is one slot).
3545            assert_eq!(metric.nargs.function_args_sum(), 3);
3546            assert_eq!(metric.nargs.closure_args_sum(), 0);
3547            insta::assert_json_snapshot!(metric.nargs);
3548        });
3549    }
3550
3551    #[cfg(feature = "tcl")]
3552    #[test]
3553    fn tcl_default_arg() {
3554        // `{name default}` is a single argument with a default value.
3555        check_metrics::<TclParser>(
3556            "proc greet {{name World} greeting} {
3557    puts \"$greeting, $name!\"
3558}",
3559            "foo.tcl",
3560            |metric| {
3561                // {name World} counts as one slot + greeting → fn_args_sum 2.
3562                assert_eq!(metric.nargs.function_args_sum(), 2);
3563                assert_eq!(metric.nargs.closure_args_sum(), 0);
3564                insta::assert_json_snapshot!(metric.nargs);
3565            },
3566        );
3567    }
3568
3569    #[cfg(feature = "kotlin")]
3570    #[test]
3571    fn kotlin_zero_args() {
3572        check_metrics::<KotlinParser>("fun f(): Int { return 42 }", "foo.kt", |metric| {
3573            // fun f() → empty parameter list, fn_args_sum 0.
3574            assert_eq!(metric.nargs.function_args_sum(), 0);
3575            assert_eq!(metric.nargs.closure_args_sum(), 0);
3576            insta::assert_json_snapshot!(metric.nargs);
3577        });
3578    }
3579
3580    #[cfg(feature = "kotlin")]
3581    #[test]
3582    fn kotlin_single_arg() {
3583        check_metrics::<KotlinParser>(
3584            "fun double(x: Int): Int { return x * 2 }",
3585            "foo.kt",
3586            |metric| {
3587                // double(x) → fn_args_sum 1.
3588                assert_eq!(metric.nargs.function_args_sum(), 1);
3589                assert_eq!(metric.nargs.closure_args_sum(), 0);
3590                insta::assert_json_snapshot!(metric.nargs);
3591            },
3592        );
3593    }
3594
3595    #[cfg(feature = "kotlin")]
3596    #[test]
3597    fn kotlin_multiple_args() {
3598        check_metrics::<KotlinParser>(
3599            "fun add(a: Int, b: Int, c: Int): Int { return a + b + c }",
3600            "foo.kt",
3601            |metric| {
3602                // add(a, b, c) → fn_args_sum 3.
3603                assert_eq!(metric.nargs.function_args_sum(), 3);
3604                assert_eq!(metric.nargs.closure_args_sum(), 0);
3605                insta::assert_json_snapshot!(metric.nargs);
3606            },
3607        );
3608    }
3609
3610    #[cfg(feature = "kotlin")]
3611    #[test]
3612    fn kotlin_default_args() {
3613        check_metrics::<KotlinParser>(
3614            "fun greet(name: String = \"World\", greeting: String = \"Hello\"): String {
3615                 return \"$greeting, $name!\"
3616             }",
3617            "foo.kt",
3618            |metric| {
3619                // Defaults still count as parameter slots → fn_args_sum 2.
3620                assert_eq!(metric.nargs.function_args_sum(), 2);
3621                assert_eq!(metric.nargs.closure_args_sum(), 0);
3622                insta::assert_json_snapshot!(metric.nargs);
3623            },
3624        );
3625    }
3626
3627    #[cfg(feature = "kotlin")]
3628    #[test]
3629    fn kotlin_empty_lambda() {
3630        // Two lambdas in the same function body: one with two explicit parameters
3631        // (proving the lambda path is taken and args are counted), and one with an
3632        // explicit empty parameter list `{ -> expr }` (proving
3633        // `compute_kotlin_lambda_args` returns 0 for it without crashing or
3634        // accidentally counting tokens inside the arrow expression).
3635        // If the grammar fails to parse either lambda, `total_closures` would be
3636        // lower than 2, making the snapshot unambiguous.
3637        check_metrics::<KotlinParser>(
3638            "fun f() {
3639                 val two = { x: Int, y: Int -> x + y }
3640                 val zero = { -> 42 }
3641             }",
3642            "foo.kt",
3643            |metric| {
3644                // Outer fun f() has 0 params; two lambdas counted as closures:
3645                // {x, y -> ...} contributes 2, {-> 42} contributes 0 →
3646                // closure_args_sum 2 across two closure entries.
3647                assert_eq!(metric.nargs.function_args_sum(), 0);
3648                assert_eq!(metric.nargs.closure_args_sum(), 2);
3649                insta::assert_json_snapshot!(metric.nargs);
3650            },
3651        );
3652    }
3653
3654    #[cfg(feature = "kotlin")]
3655    #[test]
3656    fn kotlin_anonymous_function() {
3657        // `fun(x: Int, y: Int) = x + y` — anonymous function expression.
3658        // The grammar surfaces it as an `AnonymousFunction` node, which routes
3659        // through `compute_kotlin_func_args` (not the lambda path).
3660        check_metrics::<KotlinParser>(
3661            "val add = fun(x: Int, y: Int): Int = x + y",
3662            "foo.kt",
3663            |metric| {
3664                // Anonymous fun(x, y) is classified as a closure → closure_args_sum 2.
3665                assert_eq!(metric.nargs.function_args_sum(), 0);
3666                assert_eq!(metric.nargs.closure_args_sum(), 2);
3667                insta::assert_json_snapshot!(metric.nargs);
3668            },
3669        );
3670    }
3671
3672    #[cfg(feature = "php")]
3673    #[test]
3674    fn php_no_functions_and_closures() {
3675        check_metrics::<PhpParser>("<?php $a = 42;", "foo.php", |metric| {
3676            insta::assert_json_snapshot!(
3677                metric.nargs,
3678                @r#"
3679            {
3680              "function_args": 0,
3681              "closure_args": 0,
3682              "function_args_average": 0.0,
3683              "closure_args_average": 0.0,
3684              "total": 0,
3685              "value": 0,
3686              "average": 0.0,
3687              "function_args_min": 0,
3688              "function_args_max": 0,
3689              "closure_args_min": 0,
3690              "closure_args_max": 0
3691            }
3692            "#
3693            );
3694        });
3695    }
3696
3697    #[cfg(feature = "php")]
3698    #[test]
3699    fn php_single_function() {
3700        // Two parameters in a regular function.
3701        check_metrics::<PhpParser>(
3702            "<?php
3703            function f(bool $a, int $b): bool {
3704                if ($a) { return $a; }
3705                return false;
3706            }",
3707            "foo.php",
3708            |metric| {
3709                insta::assert_json_snapshot!(
3710                    metric.nargs,
3711                    @r#"
3712                {
3713                  "function_args": 2,
3714                  "closure_args": 0,
3715                  "function_args_average": 2.0,
3716                  "closure_args_average": 0.0,
3717                  "total": 2,
3718                  "value": 0,
3719                  "average": 2.0,
3720                  "function_args_min": 0,
3721                  "function_args_max": 2,
3722                  "closure_args_min": 0,
3723                  "closure_args_max": 0
3724                }
3725                "#
3726                );
3727            },
3728        );
3729    }
3730
3731    #[cfg(feature = "php")]
3732    #[test]
3733    fn php_single_closure() {
3734        // Anonymous function with 2 params + arrow function with 1 param.
3735        // Each is a separate closure space.
3736        check_metrics::<PhpParser>(
3737            "<?php
3738            $f = function (int $a, int $b) { return $a + $b; };
3739            $g = fn (int $x) => $x * 2;",
3740            "foo.php",
3741            |metric| {
3742                insta::assert_json_snapshot!(
3743                    metric.nargs,
3744                    @r#"
3745                {
3746                  "function_args": 0,
3747                  "closure_args": 3,
3748                  "function_args_average": 0.0,
3749                  "closure_args_average": 1.5,
3750                  "total": 3,
3751                  "value": 0,
3752                  "average": 1.5,
3753                  "function_args_min": 0,
3754                  "function_args_max": 0,
3755                  "closure_args_min": 0,
3756                  "closure_args_max": 2
3757                }
3758                "#
3759                );
3760            },
3761        );
3762    }
3763
3764    #[cfg(feature = "php")]
3765    #[test]
3766    fn php_functions() {
3767        // Two top-level functions, 1 + 2 args.
3768        check_metrics::<PhpParser>(
3769            "<?php
3770            function a(int $x): int { return $x; }
3771            function b(int $x, int $y): int { return $x + $y; }",
3772            "foo.php",
3773            |metric| {
3774                insta::assert_json_snapshot!(
3775                    metric.nargs,
3776                    @r#"
3777                {
3778                  "function_args": 3,
3779                  "closure_args": 0,
3780                  "function_args_average": 1.5,
3781                  "closure_args_average": 0.0,
3782                  "total": 3,
3783                  "value": 0,
3784                  "average": 1.5,
3785                  "function_args_min": 0,
3786                  "function_args_max": 2,
3787                  "closure_args_min": 0,
3788                  "closure_args_max": 0
3789                }
3790                "#
3791                );
3792            },
3793        );
3794    }
3795
3796    #[cfg(feature = "php")]
3797    #[test]
3798    fn php_nested_functions() {
3799        // PHP cannot define nested named functions inside a function body
3800        // syntactically, but a class with methods exhibits the same shape:
3801        // a top-level scope plus inner function-spaces.
3802        check_metrics::<PhpParser>(
3803            "<?php
3804            class A {
3805                public function outer(int $a): int {
3806                    $f = function (int $b) use ($a) { return $a + $b; };
3807                    return $f($a);
3808                }
3809            }",
3810            "foo.php",
3811            |metric| {
3812                insta::assert_json_snapshot!(
3813                    metric.nargs,
3814                    @r#"
3815                {
3816                  "function_args": 1,
3817                  "closure_args": 1,
3818                  "function_args_average": 1.0,
3819                  "closure_args_average": 1.0,
3820                  "total": 2,
3821                  "value": 0,
3822                  "average": 1.0,
3823                  "function_args_min": 0,
3824                  "function_args_max": 1,
3825                  "closure_args_min": 0,
3826                  "closure_args_max": 1
3827                }
3828                "#
3829                );
3830            },
3831        );
3832    }
3833
3834    /// Regression for #1142: the parameter list sits two `arguments`
3835    /// levels down, so the `parameters`-field heuristic found nothing and
3836    /// every Elixir function reported 0.
3837    #[cfg(feature = "elixir")]
3838    #[test]
3839    fn elixir_named_function_args() {
3840        check_metrics::<ElixirParser>(
3841            "defmodule Foo do\n  def bar(a, b, c) do\n    a + b + c\n  end\nend\n",
3842            "foo.ex",
3843            |metric| {
3844                assert_eq!(metric.nom.functions_sum(), 1);
3845                let s = &metric.nargs;
3846                assert_eq!(s.function_args_sum(), 3);
3847                assert_eq!(s.function_args_max(), 3);
3848            },
3849        );
3850    }
3851
3852    /// A guard interposes a `when` `binary_operator` between the macro's
3853    /// `arguments` and the head `Call`. Without unwrapping it every
3854    /// guarded clause — a large fraction of real Elixir — counts 0.
3855    #[cfg(feature = "elixir")]
3856    #[test]
3857    fn elixir_guarded_clause_args() {
3858        check_metrics::<ElixirParser>(
3859            "defmodule Foo do\n  defp baz(x) when is_integer(x), do: x\nend\n",
3860            "foo.ex",
3861            |metric| {
3862                assert_eq!(metric.nom.functions_sum(), 1);
3863                let s = &metric.nargs;
3864                assert_eq!(s.function_args_sum(), 1);
3865                assert_eq!(s.function_args_max(), 1);
3866            },
3867        );
3868    }
3869
3870    /// `def noargs, do: 1` puts a bare `identifier` where the head `Call`
3871    /// would be. It has no parameter list, and the walk must stop there
3872    /// rather than fall through to the enclosing `arguments` — which
3873    /// holds the `do:` keyword pair and would count 1.
3874    #[cfg(feature = "elixir")]
3875    #[test]
3876    fn elixir_zero_arg_function_has_no_parameter_list() {
3877        check_metrics::<ElixirParser>(
3878            "defmodule Foo do\n  def noargs, do: 1\nend\n",
3879            "foo.ex",
3880            |metric| {
3881                assert_eq!(metric.nom.functions_sum(), 1);
3882                assert_eq!(metric.nargs.function_args_sum(), 0);
3883            },
3884        );
3885    }
3886
3887    /// Pattern and defaulted parameters are `map` and `binary_operator`
3888    /// nodes rather than plain identifiers, so the punctuation-negative
3889    /// filter is what keeps them counted.
3890    #[cfg(feature = "elixir")]
3891    #[test]
3892    fn elixir_pattern_and_default_args() {
3893        check_metrics::<ElixirParser>(
3894            "defmodule Foo do\n  def f(%{a: x}, b \\\\ 1), do: {x, b}\nend\n",
3895            "foo.ex",
3896            |metric| {
3897                assert_eq!(metric.nom.functions_sum(), 1);
3898                let s = &metric.nargs;
3899                assert_eq!(s.function_args_sum(), 2);
3900                assert_eq!(s.function_args_max(), 2);
3901            },
3902        );
3903    }
3904
3905    /// Every clause of one `fn` has the same arity, so a two-clause
3906    /// two-argument closure is 2 — summing the clauses would report 4.
3907    #[cfg(feature = "elixir")]
3908    #[test]
3909    fn elixir_multi_clause_closure_counts_one_clause() {
3910        check_metrics::<ElixirParser>(
3911            "defmodule Foo do\n  def run do\n    fn\n      a, b -> a + b\n      a, _ -> a\n    end\n  end\nend\n",
3912            "foo.ex",
3913            |metric| {
3914                assert_eq!(metric.nom.closures_sum(), 1);
3915                let s = &metric.nargs;
3916                assert_eq!(s.closure_args_sum(), 2);
3917                assert_eq!(s.closure_args_max(), 2);
3918            },
3919        );
3920    }
3921
3922    /// A guarded `fn` clause aliases its `left` to the same `when`
3923    /// `binary_operator` a guarded `def` head uses, so it needs the same
3924    /// unwrap. Without it the count is the guard expression's fixed three
3925    /// children — 3 for any arity, which is why the four-parameter form is
3926    /// the fixture here.
3927    #[cfg(feature = "elixir")]
3928    #[test]
3929    fn elixir_guarded_closure_args() {
3930        check_metrics::<ElixirParser>(
3931            "defmodule Foo do\n  def run do\n    fn a, b, c, d when is_integer(a) -> a + b + c + d end\n  end\nend\n",
3932            "foo.ex",
3933            |metric| {
3934                assert_eq!(metric.nom.closures_sum(), 1);
3935                let s = &metric.nargs;
3936                assert_eq!(s.closure_args_sum(), 4);
3937                assert_eq!(s.closure_args_max(), 4);
3938            },
3939        );
3940    }
3941
3942    /// `def a + b` and `def -a` define the operator functions `+/2` and
3943    /// `-/1`. Their head is the operator node itself, with no `arguments`
3944    /// container to walk, so the arity comes from the operator's shape.
3945    #[cfg(feature = "elixir")]
3946    #[test]
3947    fn elixir_operator_definition_args() {
3948        check_metrics::<ElixirParser>(
3949            "defmodule Foo do\n  def a + b, do: {a, b}\n  def -a, do: a\nend\n",
3950            "foo.ex",
3951            |metric| {
3952                assert_eq!(metric.nom.functions_sum(), 2);
3953                let s = &metric.nargs;
3954                assert_eq!(s.function_args_sum(), 3);
3955                assert_eq!(s.function_args_max(), 2);
3956            },
3957        );
3958    }
3959
3960    /// A `def` inside `quote do … end` is a code template, not a
3961    /// declaration, and must not contribute arguments (#310). The quoted
3962    /// head carries three parameters, so dropping the rule reads 3.
3963    #[cfg(feature = "elixir")]
3964    #[test]
3965    fn elixir_quoted_def_contributes_no_args() {
3966        check_metrics::<ElixirParser>(
3967            "defmodule Foo do\n  defmacro mac do\n    quote do\n      def generated(p, q, r), do: p + q + r\n    end\n  end\nend\n",
3968            "foo.ex",
3969            |metric| {
3970                assert_eq!(metric.nargs.function_args_sum(), 0);
3971                // Anchor the zero: if Elixir def-recognition broke
3972                // entirely, `function_args_sum` would also read 0. The
3973                // enclosing `defmacro mac` still counting proves the
3974                // recognizer ran and the quote-block gate did the
3975                // excluding.
3976                assert_eq!(metric.nom.functions_sum(), 1);
3977            },
3978        );
3979    }
3980
3981    /// Only `def` / `defp` / `defmacro` / `defmacrop` declare a function.
3982    /// `defmodule` and `defdelegate` are ordinary `Call`s of the same
3983    /// shape — `defdelegate log(msg), to: Logger` has a head `Call` with
3984    /// one parameter, so a gate that matched any macro would read 1.
3985    #[cfg(feature = "elixir")]
3986    #[test]
3987    fn elixir_non_method_macros_count_zero() {
3988        check_metrics::<ElixirParser>(
3989            "defmodule Foo do\n  defdelegate log(msg), to: Logger\nend\n",
3990            "foo.ex",
3991            |metric| {
3992                assert_eq!(metric.nargs.function_args_sum(), 0);
3993                assert_eq!(metric.nargs.closure_args_sum(), 0);
3994            },
3995        );
3996    }
3997
3998    #[cfg(feature = "ruby")]
3999    #[test]
4000    fn ruby_no_functions_and_closures() {
4001        check_metrics::<RubyParser>("a = 42\n", "foo.rb", |metric| {
4002            assert_eq!(metric.nargs.function_args_sum(), 0);
4003            assert_eq!(metric.nargs.closure_args_sum(), 0);
4004        });
4005    }
4006
4007    #[cfg(feature = "ruby")]
4008    #[test]
4009    fn ruby_single_function() {
4010        // Single method with 3 parameters.
4011        check_metrics::<RubyParser>("def foo(a, b, c)\n  a + b + c\nend\n", "foo.rb", |metric| {
4012            assert_eq!(metric.nargs.function_args_sum(), 3);
4013            assert_eq!(metric.nargs.closure_args_sum(), 0);
4014        });
4015    }
4016
4017    #[cfg(feature = "ruby")]
4018    #[test]
4019    fn ruby_single_closure() {
4020        // A bare block `[1,2,3].each { |x| ... }` is the only closure
4021        // here; `each` is a method call so the method-args count is 0.
4022        check_metrics::<RubyParser>("[1, 2, 3].each { |x| puts x }\n", "foo.rb", |metric| {
4023            assert_eq!(metric.nargs.function_args_sum(), 0);
4024            assert_eq!(metric.nargs.closure_args_sum(), 1);
4025        });
4026    }
4027
4028    #[cfg(feature = "ruby")]
4029    #[test]
4030    fn ruby_functions() {
4031        // Two methods, args=2 and args=1; one lambda with args=2.
4032        check_metrics::<RubyParser>(
4033            "def add(a, b)\n  a + b\nend\n\ndef neg(x)\n  -x\nend\n\nf = ->(a, b) { a * b }\n",
4034            "foo.rb",
4035            |metric| {
4036                assert_eq!(metric.nargs.function_args_sum(), 3);
4037                assert_eq!(metric.nargs.closure_args_sum(), 2);
4038            },
4039        );
4040    }
4041
4042    #[cfg(feature = "ruby")]
4043    #[test]
4044    fn ruby_nested_functions() {
4045        // An outer method with 1 arg containing an inner method with 2.
4046        check_metrics::<RubyParser>(
4047            "def outer(a)\n  def inner(b, c)\n    b + c\n  end\n  inner(a, a)\nend\n",
4048            "foo.rb",
4049            |metric| {
4050                assert_eq!(metric.nargs.function_args_sum(), 3);
4051                assert_eq!(metric.nargs.closure_args_sum(), 0);
4052            },
4053        );
4054    }
4055
4056    /// PEP 570 positional-only `/` and PEP 3102 keyword-only `*` markers are
4057    /// punctuation, not parameters. The grammar emits them as
4058    /// `positional_separator` / `keyword_separator` siblings of the real
4059    /// parameter nodes; both must be excluded from nargs (issue #414).
4060    #[cfg(feature = "python")]
4061    #[test]
4062    fn python_both_parameter_separators() {
4063        // 1 function, 3 real parameters: pos_only, normal, kw_only.
4064        check_metrics::<PythonParser>(
4065            "def f(pos_only, /, normal, *, kw_only): pass",
4066            "foo.py",
4067            |metric| {
4068                assert_eq!(metric.nargs.function_args_sum(), 3);
4069                assert_eq!(metric.nargs.closure_args_sum(), 0);
4070            },
4071        );
4072    }
4073
4074    /// Trailing positional-only `/` (no following parameter) is still excluded.
4075    #[cfg(feature = "python")]
4076    #[test]
4077    fn python_positional_separator_only() {
4078        // 1 function, 2 real parameters: a, b (`/` excluded).
4079        check_metrics::<PythonParser>("def f(a, b, /): pass", "foo.py", |metric| {
4080            assert_eq!(metric.nargs.function_args_sum(), 2);
4081            assert_eq!(metric.nargs.closure_args_sum(), 0);
4082        });
4083    }
4084
4085    /// Leading keyword-only `*` (forcing all following parameters to be
4086    /// keyword-only) is excluded.
4087    #[cfg(feature = "python")]
4088    #[test]
4089    fn python_keyword_separator_only() {
4090        // 1 function, 2 real parameters: a, b (`*` excluded).
4091        check_metrics::<PythonParser>("def f(*, a, b): pass", "foo.py", |metric| {
4092            assert_eq!(metric.nargs.function_args_sum(), 2);
4093            assert_eq!(metric.nargs.closure_args_sum(), 0);
4094        });
4095    }
4096
4097    /// Lambdas accept the same keyword-only `*` separator; it is excluded
4098    /// from the closure arg count.
4099    #[cfg(feature = "python")]
4100    #[test]
4101    fn python_lambda_keyword_separator() {
4102        // 1 lambda, 2 real parameters: a, b (`*` excluded).
4103        check_metrics::<PythonParser>("g = lambda a, *, b: a", "foo.py", |metric| {
4104            assert_eq!(metric.nargs.function_args_sum(), 0);
4105            assert_eq!(metric.nargs.closure_args_sum(), 2);
4106        });
4107    }
4108
4109    /// Regression guard: `*args` / `**kwargs` are real parameter nodes
4110    /// (`list_splat_pattern` / `dictionary_splat_pattern`), not separators,
4111    /// and must keep contributing to the count after the #414 fix.
4112    #[cfg(feature = "python")]
4113    #[test]
4114    fn python_args_kwargs_still_counted() {
4115        // 1 function, 3 parameters: a, *args, **kwargs.
4116        check_metrics::<PythonParser>("def f(a, *args, **kwargs): pass", "foo.py", |metric| {
4117            assert_eq!(metric.nargs.function_args_sum(), 3);
4118            assert_eq!(metric.nargs.closure_args_sum(), 0);
4119        });
4120    }
4121
4122    /// A file of bare top-level commands has no function spaces, so the
4123    /// argument count is zero.
4124    #[cfg(feature = "irules")]
4125    #[test]
4126    fn irules_no_functions_and_closures() {
4127        check_metrics::<IrulesParser>("set x 1\nlog local0. $x\n", "foo.irule", |metric| {
4128            assert_eq!(metric.nargs.function_args_sum(), 0);
4129            assert_eq!(metric.nargs.closure_args_sum(), 0);
4130        });
4131    }
4132
4133    /// A `when` handler is a function space but has no formal parameters
4134    /// (the event context is implicit), so its argument count is zero —
4135    /// `when_event` carries no `arguments` field. Guards edge case #10.
4136    #[cfg(feature = "irules")]
4137    #[test]
4138    fn irules_handler_zero_args() {
4139        check_metrics::<IrulesParser>(
4140            "when HTTP_REQUEST { log local0. \"hit\" }\n",
4141            "foo.irule",
4142            |metric| {
4143                assert_eq!(metric.nargs.function_args_sum(), 0);
4144                assert_eq!(metric.nargs.closure_args_sum(), 0);
4145                // The handler is still counted as a function space.
4146                assert_eq!(metric.nom.functions_sum(), 1);
4147            },
4148        );
4149    }
4150
4151    /// A `proc` with two formal parameters contributes two arguments.
4152    #[cfg(feature = "irules")]
4153    #[test]
4154    fn irules_single_proc() {
4155        check_metrics::<IrulesParser>("proc f { a b } { return $a }\n", "foo.irule", |metric| {
4156            assert_eq!(metric.nargs.function_args_sum(), 2);
4157            assert_eq!(metric.nargs.closure_args_sum(), 0);
4158        });
4159    }
4160
4161    /// A `proc` with an empty argument list contributes zero arguments.
4162    #[cfg(feature = "irules")]
4163    #[test]
4164    fn irules_proc_no_args() {
4165        check_metrics::<IrulesParser>("proc f { } { return 1 }\n", "foo.irule", |metric| {
4166            assert_eq!(metric.nargs.function_args_sum(), 0);
4167        });
4168    }
4169
4170    /// A default-valued parameter (`{b 5}`) is a single `argument`, so each
4171    /// formal parameter counts once regardless of its default: `{a {b 5} c}`
4172    /// is three arguments.
4173    #[cfg(feature = "irules")]
4174    #[test]
4175    fn irules_proc_arg_defaults() {
4176        check_metrics::<IrulesParser>(
4177            "proc f { a {b 5} c } { return $a }\n",
4178            "foo.irule",
4179            |metric| {
4180                assert_eq!(metric.nargs.function_args_sum(), 3);
4181            },
4182        );
4183    }
4184
4185    /// A `proc` and a `when` handler in one file: only the proc's two
4186    /// parameters count; the handler contributes zero.
4187    #[cfg(feature = "irules")]
4188    #[test]
4189    fn irules_multiple_functions() {
4190        check_metrics::<IrulesParser>(
4191            "proc add { a b } { return [expr { $a + $b }] }
4192when HTTP_REQUEST { log local0. \"hit\" }
4193",
4194            "foo.irule",
4195            |metric| {
4196                assert_eq!(metric.nargs.function_args_sum(), 2);
4197                assert_eq!(metric.nom.functions_sum(), 2);
4198            },
4199        );
4200    }
4201
4202    /// Objective-C unary method `- (void)foo` declares zero arguments.
4203    #[cfg(feature = "objc")]
4204    #[test]
4205    fn objc_no_args() {
4206        check_metrics::<ObjcParser>(
4207            "@implementation Foo
4208- (void)foo {
4209    [self doWork];
4210}
4211@end
4212",
4213            "foo.m",
4214            |metric| {
4215                assert_eq!(metric.nargs.total(), 0);
4216                insta::assert_json_snapshot!(metric.nargs, @r#"
4217                {
4218                  "function_args": 0,
4219                  "closure_args": 0,
4220                  "function_args_average": 0.0,
4221                  "closure_args_average": 0.0,
4222                  "total": 0,
4223                  "value": 0,
4224                  "average": 0.0,
4225                  "function_args_min": 0,
4226                  "function_args_max": 0,
4227                  "closure_args_min": 0,
4228                  "closure_args_max": 0
4229                }
4230                "#);
4231            },
4232        );
4233    }
4234
4235    /// Objective-C keyword method `- (void)foo:(int)a bar:(int)b` has two
4236    /// `method_parameter` children, so `function_args` is 2.
4237    #[cfg(feature = "objc")]
4238    #[test]
4239    fn objc_method_two_args() {
4240        check_metrics::<ObjcParser>(
4241            "@implementation Foo
4242- (void)foo:(int)a bar:(int)b {
4243    [self use:a];
4244}
4245@end
4246",
4247            "foo.m",
4248            |metric| {
4249                assert_eq!(metric.nargs.function_args_sum(), 2);
4250                insta::assert_json_snapshot!(metric.nargs, @r#"
4251                {
4252                  "function_args": 2,
4253                  "closure_args": 0,
4254                  "function_args_average": 2.0,
4255                  "closure_args_average": 0.0,
4256                  "total": 2,
4257                  "value": 0,
4258                  "average": 2.0,
4259                  "function_args_min": 0,
4260                  "function_args_max": 2,
4261                  "closure_args_min": 0,
4262                  "closure_args_max": 0
4263                }
4264                "#);
4265            },
4266        );
4267    }
4268
4269    /// Free C `function_definition` inside an ObjC translation unit counts
4270    /// its declarator parameters: `void f(int a, int b, int c)` has 3.
4271    #[cfg(feature = "objc")]
4272    #[test]
4273    fn objc_function_args() {
4274        check_metrics::<ObjcParser>(
4275            "void f(int a, int b, int c) {
4276    return;
4277}
4278",
4279            "foo.m",
4280            |metric| {
4281                assert_eq!(metric.nargs.function_args_sum(), 3);
4282                insta::assert_json_snapshot!(metric.nargs, @r#"
4283                {
4284                  "function_args": 3,
4285                  "closure_args": 0,
4286                  "function_args_average": 3.0,
4287                  "closure_args_average": 0.0,
4288                  "total": 3,
4289                  "value": 0,
4290                  "average": 3.0,
4291                  "function_args_min": 0,
4292                  "function_args_max": 3,
4293                  "closure_args_min": 0,
4294                  "closure_args_max": 0
4295                }
4296                "#);
4297            },
4298        );
4299    }
4300
4301    /// Objective-C block literal `^(int x, int y){ … }` is a closure
4302    /// whose `parameter_list` holds two `parameter_declaration`s, so
4303    /// `closure_args` is 2.
4304    #[cfg(feature = "objc")]
4305    #[test]
4306    fn objc_block_args() {
4307        check_metrics::<ObjcParser>(
4308            "@implementation Foo
4309- (void)bar {
4310    void (^blk)(int, int) = ^(int x, int y){
4311        [self use:x];
4312    };
4313    blk(1, 2);
4314}
4315@end
4316",
4317            "foo.m",
4318            |metric| {
4319                assert_eq!(metric.nargs.closure_args_sum(), 2);
4320                insta::assert_json_snapshot!(metric.nargs, @r#"
4321                {
4322                  "function_args": 0,
4323                  "closure_args": 2,
4324                  "function_args_average": 0.0,
4325                  "closure_args_average": 2.0,
4326                  "total": 2,
4327                  "value": 0,
4328                  "average": 1.0,
4329                  "function_args_min": 0,
4330                  "function_args_max": 0,
4331                  "closure_args_min": 0,
4332                  "closure_args_max": 2
4333                }
4334                "#);
4335            },
4336        );
4337    }
4338
4339    /// A block's `(void)` marker declares nothing, so `^(void){ … }` is a
4340    /// closure of zero parameters (#1218).
4341    ///
4342    /// The objc grammar reuses C's `parameter_list` rule, so `^(void)`
4343    /// emits a real `parameter_declaration` for the `void` — the same
4344    /// shape `int f(void)` produces, and the reason
4345    /// `Checker::is_empty_param_marker` reads the source bytes rather
4346    /// than the tree. The block arm counted it until it began routing
4347    /// through `count_args`, while the function channel beside it was
4348    /// already correct: `host` below reports 0 either way, which is what
4349    /// makes this a test of the block channel specifically.
4350    #[cfg(feature = "objc")]
4351    #[test]
4352    fn objc_block_void_marker_is_not_a_parameter() {
4353        check_metrics::<ObjcParser>(
4354            "void host(void) {
4355    void (^empty)(void) = ^(void){ };
4356    empty();
4357}
4358",
4359            "foo.m",
4360            |metric| {
4361                assert_eq!(metric.nargs.closure_args_sum(), 0);
4362                assert_eq!(metric.nargs.function_args_sum(), 0);
4363                insta::assert_json_snapshot!(metric.nargs, @r#"
4364                {
4365                  "function_args": 0,
4366                  "closure_args": 0,
4367                  "function_args_average": 0.0,
4368                  "closure_args_average": 0.0,
4369                  "total": 0,
4370                  "value": 0,
4371                  "average": 0.0,
4372                  "function_args_min": 0,
4373                  "function_args_max": 0,
4374                  "closure_args_min": 0,
4375                  "closure_args_max": 0
4376                }
4377                "#);
4378            },
4379        );
4380    }
4381
4382    /// A comment inside a block's parameter list is not a parameter, so
4383    /// `^(int a /* c */, int b){ … }` is 2 (#1201, #1218).
4384    ///
4385    /// **This fixture cannot fail by reverting the block arm.** The
4386    /// positive `matches!(ParameterDeclaration | VariadicParameter)` the
4387    /// arm used before #1218 already ignored a `comment` child, so the
4388    /// count was correct for the wrong reason — nothing asserted it, and
4389    /// the #1201 changelog claimed Objective-C blocks were swept when
4390    /// only the method fixture existed. It became load-bearing when the
4391    /// arm switched to `count_args`, whose *negative* filtering is what
4392    /// now makes `Checker::is_comment` live on this path. Perturb it by
4393    /// dropping `is_comment` from `count_args`, not by reverting the arm.
4394    #[cfg(feature = "objc")]
4395    #[test]
4396    fn objc_block_comment_is_not_a_parameter() {
4397        check_metrics::<ObjcParser>(
4398            "void host(void) {
4399    void (^two)(int, int) = ^(int a /* c */, int b){ };
4400    two(1, 2);
4401}
4402",
4403            "foo.m",
4404            |metric| {
4405                assert_eq!(metric.nargs.closure_args_sum(), 2);
4406                assert_eq!(metric.nargs.function_args_sum(), 0);
4407            },
4408        );
4409    }
4410
4411    /// A variadic block keeps its `...` counted: `^(int a, ...){ … }` is 2.
4412    ///
4413    /// The guard against #1218's fix, not against #1218. Swapping the
4414    /// arm's positive `matches!` for `count_args`' negative filters is
4415    /// what could silently drop `variadic_parameter` — it is named in the
4416    /// old match and in none of the new filters, so only a fixture says
4417    /// whether it survived. `ObjcCode::is_non_arg` covers the list's
4418    /// punctuation (`(`, `,`, `)`) and nothing else, so it does.
4419    #[cfg(feature = "objc")]
4420    #[test]
4421    fn objc_block_variadic_parameter_still_counts() {
4422        check_metrics::<ObjcParser>(
4423            "void host(void) {
4424    void (^var)(int, ...) = ^(int a, ...){ };
4425    var(1);
4426}
4427",
4428            "foo.m",
4429            |metric| {
4430                assert_eq!(metric.nargs.closure_args_sum(), 2);
4431                assert_eq!(metric.nargs.function_args_sum(), 0);
4432            },
4433        );
4434    }
4435
4436    /// A block written without a parameter list at all is 0.
4437    ///
4438    /// `^{ }` has no `parameter_list` child, so the arm's
4439    /// `first_child(ParameterList)` guard short-circuits before
4440    /// `count_args` is reached. Pinned beside the `^(void)` case because
4441    /// the two spellings mean the same thing and only one of them ever
4442    /// went through the counting path.
4443    #[cfg(feature = "objc")]
4444    #[test]
4445    fn objc_block_without_a_parameter_list_is_zero() {
4446        check_metrics::<ObjcParser>(
4447            "void host(void) {
4448    void (^none)(void) = ^{ };
4449    none();
4450}
4451",
4452            "foo.m",
4453            |metric| {
4454                assert_eq!(metric.nargs.closure_args_sum(), 0);
4455                assert_eq!(metric.nargs.function_args_sum(), 0);
4456            },
4457        );
4458    }
4459
4460    /// Regression for #782: the textual `Display` headline must report
4461    /// the cross-space *sum* (`function_args_sum`/`closure_args_sum`),
4462    /// matching the JSON/YAML/TOML/CBOR serializers, not the per-space
4463    /// direct accumulator. At a parent space that rolls up child
4464    /// function-spaces (the file/unit space of `python_nested_functions`)
4465    /// the accumulator under-counts: it reflects only the direct
4466    /// function `f` (2 args) and no merged closures (0), while the sum
4467    /// is 3 function args (f=2, foo=1) and 2 closure args. Before the
4468    /// fix Display printed `function_args: 2, closure_args: 0`.
4469    #[cfg(feature = "python")]
4470    #[test]
4471    fn display_headline_matches_sum_for_nested_functions() {
4472        check_metrics::<PythonParser>(
4473            "def f(a, b):
4474                 def foo(a):
4475                     if a:
4476                         return 1
4477                 bar = lambda a: lambda b: b or True or True
4478                 return bar(foo(a))(a)",
4479            "foo.py",
4480            |metric| {
4481                let stats = &metric.nargs;
4482                // The summed accessors are the cross-format source of truth.
4483                assert_eq!(stats.function_args_sum(), 3);
4484                assert_eq!(stats.closure_args_sum(), 2);
4485
4486                // The Display headline must echo those sums verbatim.
4487                let rendered = stats.to_string();
4488                assert!(
4489                    rendered.starts_with(&format!(
4490                        "function_args: {}, closure_args: {},",
4491                        stats.function_args_sum(),
4492                        stats.closure_args_sum()
4493                    )),
4494                    "Display headline diverged from the summed accessors: {rendered}"
4495                );
4496            },
4497        );
4498    }
4499}
4500
4501/// A lambda's parameter count must not depend on optional parentheses
4502/// (#1185).
4503///
4504/// `x -> x + 1` and `(x) -> x + 1` are the same lambda — the parens are
4505/// optional in the grammar and carry no meaning — so they must score
4506/// alike, the same "byte-equivalent constructs score identically"
4507/// contract the book states for cognitive.
4508///
4509/// The cause is shared: the `parameters` field holds a lone, childless
4510/// parameter node rather than a list, and `compute_args` walks the
4511/// field's children. The issue names Java; the sweep found **C#** has
4512/// the identical defect via `implicit_parameter`. Kotlin and Groovy
4513/// were checked and are correct — each overrides `compute` with its own
4514/// closure-parameter shape — and the JS family reaches the right answer
4515/// through the singular `parameter` field.
4516// Gated on the union of the languages `cases()` has a row for, so a
4517// build enabling none of them drops the module rather than tripping its
4518// `checked > 0` guard (`.claude/rules/testing.md`, #1286). The
4519// closure-channel test below reads a strict subset of that table and so
4520// carries its own narrower gate.
4521#[cfg(test)]
4522#[cfg(any(
4523    feature = "csharp",
4524    feature = "java",
4525    feature = "javascript",
4526    feature = "mozjs",
4527    feature = "typescript"
4528))]
4529mod lambda_parenthesisation_parity {
4530    use crate::test_support::metrics_verbatim;
4531    use crate::{LANG, MetricsOptions};
4532
4533    /// `(closure_args, function_args)` — the split matters as much as
4534    /// the count: a lambda must stay in the closure channel.
4535    fn args(lang: LANG, source: &str) -> (u64, u64) {
4536        let m = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default());
4537        (m.nargs.closure_args_sum(), m.nargs.function_args_sum())
4538    }
4539
4540    /// `(bare, parenthesised, two_params, zero_params)`.
4541    fn cases(lang: LANG) -> Option<[&'static str; 4]> {
4542        Some(match lang {
4543            LANG::Java => [
4544                "class K{ void f(){ Function<Integer,Integer> a = x -> x + 1; } }",
4545                "class K{ void f(){ Function<Integer,Integer> a = (x) -> x + 1; } }",
4546                "class K{ void f(){ BiFunction<Integer,Integer,Integer> c = (x, y) -> x + y; } }",
4547                "class K{ void f(){ Supplier<Integer> d = () -> 1; } }",
4548            ],
4549            LANG::Csharp => [
4550                "class K{ void f(){ Func<int,int> a = x => x + 1; } }",
4551                "class K{ void f(){ Func<int,int> a = (x) => x + 1; } }",
4552                "class K{ void f(){ Func<int,int,int> c = (x, y) => x + y; } }",
4553                "class K{ void f(){ Func<int> d = () => 1; } }",
4554            ],
4555            LANG::Javascript | LANG::Typescript | LANG::Tsx | LANG::Mozjs => [
4556                "function f(){ var a = x => x + 1; }",
4557                "function f(){ var a = (x) => x + 1; }",
4558                "function f(){ var c = (x, y) => x + y; }",
4559                "function f(){ var d = () => 1; }",
4560            ],
4561            _ => return None,
4562        })
4563    }
4564
4565    #[test]
4566    fn optional_parentheses_do_not_change_the_count() {
4567        let mut checked = 0;
4568        for lang in LANG::into_enum_iter() {
4569            if !lang.is_enabled() {
4570                continue;
4571            }
4572            let Some([bare, paren, two, zero]) = cases(lang) else {
4573                continue;
4574            };
4575            checked += 1;
4576
4577            let (bare_args, paren_args) = (args(lang, bare), args(lang, paren));
4578            assert_eq!(
4579                bare_args, paren_args,
4580                "{lang:?}: the parentheses changed the argument count\n  bare:  {bare}\n  paren: {paren}"
4581            );
4582            // The absolute value, so a regression that zeroed *both*
4583            // spellings would still fail.
4584            assert_eq!(
4585                bare_args.0 + bare_args.1,
4586                1,
4587                "{lang:?}: a one-parameter lambda must report one argument"
4588            );
4589            // A zero-parameter lambda must stay 0: the bare-parameter
4590            // branch must not mistake an empty list for a parameter.
4591            assert_eq!(
4592                args(lang, zero),
4593                (0, 0),
4594                "{lang:?}: `() -> …` has no arguments"
4595            );
4596            // And the plural path must be undisturbed.
4597            let two_args = args(lang, two);
4598            assert_eq!(
4599                two_args.0 + two_args.1,
4600                2,
4601                "{lang:?}: a two-parameter lambda must report two arguments"
4602            );
4603        }
4604        assert!(
4605            checked > 0,
4606            "no lambda language enabled; this test asserted nothing"
4607        );
4608    }
4609
4610    /// The lambda stays in the *closure* channel, not the function one.
4611    ///
4612    /// Java and C# route it through `is_closure`; the JS family's arrow
4613    /// is classified by `check_if_arrow_func!` and lands in `fn_args`
4614    /// when bound to a variable, which is a separate question (#1188).
4615    /// Asserting the channel per language rather than globally keeps
4616    /// this test from encoding that as a bug.
4617    #[test]
4618    #[cfg(any(feature = "csharp", feature = "java"))]
4619    fn a_bare_lambda_stays_in_the_closure_channel() {
4620        let mut checked = 0;
4621        for lang in [LANG::Java, LANG::Csharp]
4622            .into_iter()
4623            .filter(LANG::is_enabled)
4624        {
4625            checked += 1;
4626            let [bare, ..] = cases(lang).expect("both languages have cases");
4627            assert_eq!(
4628                args(lang, bare),
4629                (1, 0),
4630                "{lang:?}: the lambda's argument must be billed to closure_args"
4631            );
4632        }
4633        // Two rows against the module's five-feature gate, so this test
4634        // needs the narrower `cfg` above; the guard then covers the
4635        // residual `is_enabled` disagreement (#1286).
4636        assert!(
4637            checked > 0,
4638            "neither java nor csharp is enabled; this test asserted nothing"
4639        );
4640    }
4641}
4642
4643/// #1201 — a comment inside a parameter list counted as a parameter.
4644///
4645/// tree-sitter attaches a comment written between two parameters as a
4646/// direct child of the parameter-*list* node, not inside the parameter
4647/// it documents. Every negative filter in this module lists punctuation
4648/// only, so each comment scored one: `int h(int a /* one */, int b)`
4649/// reported 3, and the C++ idiom for a deliberately unused parameter,
4650/// `void f(int /*unused*/)`, reported 2.
4651///
4652/// Four independent loops could carry the defect and each has its own
4653/// row below, so reverting any single one of them fails on its own:
4654/// `compute_args` (the C family through Groovy), `elixir_declared_args`,
4655/// `compute_kotlin_lambda_args`, and `compute_perl_args` — the last of
4656/// which already excluded comments and is here as a no-change guard on
4657/// its collapse onto the shared `count_args`.
4658///
4659/// Go, Lua, Objective-C *methods*, Kotlin *functions* and Groovy
4660/// *closures* were already correct — each filters positively for its
4661/// parameter kind — and are swept anyway, because "this one is a
4662/// positive filter" is the reasoning that has to hold for a grammar
4663/// bump, not just for today.
4664///
4665/// Objective-C *blocks* were on that list until #1218 and are not any
4666/// more: their arm now routes through the shared `count_args`, so what
4667/// keeps a comment out of a block's count is the same negative filter
4668/// the repaired languages rely on, not a positive parameter-kind match.
4669/// Their fixture is `objc_block_comment_is_not_a_parameter`, which sits
4670/// in the module above beside the `^(void)` case that motivated the
4671/// move rather than in the table below.
4672#[cfg(test)]
4673mod comments_in_parameter_lists {
4674    use crate::test_support::metrics_verbatim;
4675    use crate::{LANG, MetricsOptions};
4676
4677    /// `(closure_args, function_args)`. Asserting the pair rather than
4678    /// the sum keeps a fix that merely moved a count between channels
4679    /// from reading as a pass.
4680    fn args(lang: LANG, source: &str) -> (u64, u64) {
4681        let m = metrics_verbatim(lang, source.as_bytes(), MetricsOptions::default());
4682        (m.nargs.closure_args_sum(), m.nargs.function_args_sum())
4683    }
4684
4685    /// Fixtures for the languages #1201 repaired. Every one of these
4686    /// reported an inflated count before the fix.
4687    ///
4688    /// Each fixture declares exactly two parameters — bar the C-family
4689    /// unnamed-parameter shape, which declares one — so the expected
4690    /// value is a 2 in one channel or the other. Where a language spells
4691    /// both a block and a line comment, both appear: they are separate
4692    /// `is_comment` arms in Rust, Java, C#, Kotlin, Groovy, PHP and the
4693    /// JS family, and a block-only sweep leaves the other arm untested.
4694    fn repaired_cases(lang: LANG) -> Option<&'static [(&'static str, (u64, u64))]> {
4695        Some(match lang {
4696            LANG::C => &[
4697                (
4698                    "int h(int a /* one */, int b /* two */) { return a; }",
4699                    (0, 2),
4700                ),
4701                ("int h(int a, // one\n      int b) { return a; }", (0, 2)),
4702                // The unnamed-parameter idiom: the comment is the only
4703                // thing standing where the name would be, so counting it
4704                // doubled the arity rather than adding to it.
4705                ("void f(int /*unused*/) { }", (0, 1)),
4706            ],
4707            // Split from `C` only for the lambda, which C has no form of.
4708            LANG::Cpp | LANG::Mozcpp => &[
4709                (
4710                    "int h(int a /* one */, int b /* two */) { return a; }",
4711                    (0, 2),
4712                ),
4713                ("int h(int a, // one\n      int b) { return a; }", (0, 2)),
4714                ("void f(int /*unused*/) { }", (0, 1)),
4715                // The closure channel, which reaches `compute_args`
4716                // through the same `declarator` field as the function
4717                // one but bills a different counter.
4718                (
4719                    "int g() { auto f = [](int a, /* one */ int b){ return a + b; }; return f(1,2); }",
4720                    (2, 0),
4721                ),
4722            ],
4723            LANG::Objc => &[("int h(int a /* one */, int b) { return a; }", (0, 2))],
4724            LANG::Javascript | LANG::Mozjs => &[
4725                ("function h(a, /* one */ b) { return a; }", (0, 2)),
4726                ("function h(a, // one\n           b) { return a; }", (0, 2)),
4727            ],
4728            LANG::Typescript | LANG::Tsx => &[
4729                (
4730                    "function h(a: number, /* one */ b: number) { return a; }",
4731                    (0, 2),
4732                ),
4733                (
4734                    "function h(a: number, // one\n           b: number) { return a; }",
4735                    (0, 2),
4736                ),
4737            ],
4738            // Python has no block comment, so the `#` form is the whole
4739            // of its exposure.
4740            LANG::Python => &[("def h(a,  # one\n      b):\n    return a\n", (0, 2))],
4741            LANG::Rust => &[
4742                ("fn h(a: i32, /* one */ b: i32) -> i32 { a }", (0, 2)),
4743                ("fn h(a: i32, // one\n     b: i32) -> i32 { a }", (0, 2)),
4744                // `compute_args` is reached a second time for closures,
4745                // with `closure_nargs` as the target. Same walk, but a
4746                // fixture that only ever asserts the function channel
4747                // cannot tell a regression at that call site apart from
4748                // a pass.
4749                (
4750                    "fn g() { let f = |a: i32, /* one */ b: i32| a + b; }",
4751                    (2, 0),
4752                ),
4753            ],
4754            // One source parses as both, so they share an arm rather
4755            // than tripping `clippy::match_same_arms` on two copies.
4756            LANG::Java | LANG::Csharp => &[
4757                (
4758                    "class K { int h(int a, /* one */ int b) { return a; } }",
4759                    (0, 2),
4760                ),
4761                (
4762                    "class K { int h(int a, // one\n                int b) { return a; } }",
4763                    (0, 2),
4764                ),
4765            ],
4766            LANG::Php => &[
4767                ("<?php function h($a, /* one */ $b) { return $a; }", (0, 2)),
4768                (
4769                    "<?php function h($a, // one\n                 $b) { return $a; }",
4770                    (0, 2),
4771                ),
4772            ],
4773            LANG::Ruby => &[("def h(a, # one\n      b)\n  a\nend", (0, 2))],
4774            LANG::Groovy => &[
4775                ("def h(a, /* one */ b) { return a }", (0, 2)),
4776                ("def h(a, // one\n      b) { return a }", (0, 2)),
4777            ],
4778            // Elixir's parameter list is a second `Call`'s `arguments`,
4779            // reached without ever entering `compute_args`, so nothing
4780            // above proves anything about it.
4781            LANG::Elixir => &[(
4782                "defmodule M do\n  def h(a, # one\n        b) do\n    a\n  end\nend",
4783                (0, 2),
4784            )],
4785            // `compute_kotlin_lambda_args` is a separate loop with its own
4786            // negative filter, which the Kotlin *function* guard below
4787            // never reaches.
4788            LANG::Kotlin => &[
4789                (
4790                    "fun g() {\n  val f = { a: Int, /* one */ b: Int -> a }\n  println(f)\n}",
4791                    (2, 0),
4792                ),
4793                (
4794                    "fun g() {\n  val f = { a: Int, // one\n            b: Int -> a }\n  println(f)\n}",
4795                    (2, 0),
4796                ),
4797            ],
4798            // Bash functions take no formal parameters, and Tcl/iRules
4799            // have no comment in this position at all — see
4800            // `tcl_has_no_comment_inside_a_parameter_list`.
4801            _ => return None,
4802        })
4803    }
4804
4805    /// Fixtures for the counting loops that were already correct before
4806    /// #1201, each because it filters *positively* for its parameter
4807    /// kind and so never saw a comment to miscount.
4808    ///
4809    /// They are swept anyway: "this one is a positive filter" is a claim
4810    /// that has to keep holding across a grammar bump, not just today,
4811    /// and Perl's is the guard on collapsing its private comment
4812    /// exclusion onto the shared `count_args`.
4813    fn already_correct_cases(lang: LANG) -> Option<&'static [(&'static str, (u64, u64))]> {
4814        Some(match lang {
4815            // One `method_parameter` per labelled argument.
4816            LANG::Objc => &[(
4817                "@implementation K\n- (void)foo:(int)a /* one */ bar:(int)b { }\n@end",
4818                (0, 2),
4819            )],
4820            // A positive `ClosureParameter` filter.
4821            LANG::Groovy => &[("def c = { x, /* one */ y -> x }", (2, 0))],
4822            // A positive `Parameter` filter.
4823            LANG::Kotlin => &[("fun h(a: Int, /* one */ b: Int): Int { return a }", (0, 2))],
4824            // Names are counted inside each `parameter_declaration`, so a
4825            // sibling comment is invisible.
4826            LANG::Go => &[
4827                (
4828                    "package m\nfunc h(a int, /* one */ b int) int { return a }",
4829                    (0, 2),
4830                ),
4831                (
4832                    "package m\nvar f = func(x int, /* one */ y int) int { return x }",
4833                    (2, 0),
4834                ),
4835            ],
4836            // A positive `Identifier | VarargExpression` filter.
4837            LANG::Lua => &[("local f = function(a, --[[one]] b) return a end", (2, 0))],
4838            // Perl carried the comment exclusion inline before #1201
4839            // moved it into `count_args`; this pins that the move changed
4840            // nothing.
4841            LANG::Perl => &[(
4842                "use feature 'signatures';\nsub h(\n  $a, # one\n  $b\n) { return $a; }",
4843                (0, 2),
4844            )],
4845            _ => return None,
4846        })
4847    }
4848
4849    /// Sweeps one fixture table, reporting every row that miscounted and
4850    /// failing differently when the table ran empty.
4851    ///
4852    /// The two populations are swept by two tests rather than one
4853    /// because they are **disjoint**, and a single test could not be
4854    /// gated (#1286): its guard was the conjunction `repaired > 0 &&
4855    /// guards > 0`, which no union of the two tables satisfies —
4856    /// `--features rust` supplies a repaired row and no guard row,
4857    /// `--features go` the reverse. Split, each test's `cfg` is exactly
4858    /// its own table's languages and each guard stays loud. The shared
4859    /// collect-then-report shape lives here so the split costs no
4860    /// duplication.
4861    fn sweep(table: impl Fn(LANG) -> Option<&'static [(&'static str, (u64, u64))]>, what: &str) {
4862        let mut checked = 0;
4863        let mut failures = Vec::new();
4864        for lang in LANG::into_enum_iter().filter(LANG::is_enabled) {
4865            for (source, expected) in table(lang).unwrap_or_default() {
4866                checked += 1;
4867                let got = args(lang, source);
4868                // Collected rather than asserted inline so a revert of
4869                // any one of the four loops shows every language it
4870                // broke, not just the alphabetically first. The branch
4871                // carries no formatting — a line that runs only on
4872                // failure can never be covered, so the report is built
4873                // once, below, from the raw tuples.
4874                if got != *expected {
4875                    failures.push((lang, source, *expected, got));
4876                }
4877            }
4878        }
4879        // Bound eagerly and interpolated by name: an `assert!` argument is
4880        // evaluated only when the assertion fires, so spelling these out
4881        // as arguments would leave two more never-executed lines behind.
4882        let failed = failures.len();
4883        assert!(
4884            failures.is_empty(),
4885            "{failed}/{checked} {what} fixtures counted a comment as a parameter: {failures:#?}"
4886        );
4887        // The tally, so a table that stopped being reached — a renamed
4888        // `LANG` variant, a feature that stopped being enabled — fails
4889        // here rather than passing vacuously.
4890        assert!(
4891            checked > 0,
4892            "no {what} fixture ran; this test asserted nothing"
4893        );
4894    }
4895
4896    #[test]
4897    #[cfg(any(
4898        feature = "c",
4899        feature = "cpp",
4900        feature = "csharp",
4901        feature = "elixir",
4902        feature = "groovy",
4903        feature = "java",
4904        feature = "javascript",
4905        feature = "kotlin",
4906        feature = "mozcpp",
4907        feature = "mozjs",
4908        feature = "objc",
4909        feature = "php",
4910        feature = "python",
4911        feature = "ruby",
4912        feature = "rust",
4913        feature = "typescript"
4914    ))]
4915    fn a_comment_in_a_repaired_parameter_list_is_not_a_parameter() {
4916        sweep(repaired_cases, "repaired");
4917    }
4918
4919    #[test]
4920    #[cfg(any(
4921        feature = "go",
4922        feature = "groovy",
4923        feature = "kotlin",
4924        feature = "lua",
4925        feature = "objc",
4926        feature = "perl"
4927    ))]
4928    fn a_positive_parameter_filter_still_ignores_a_comment() {
4929        sweep(already_correct_cases, "already-correct");
4930    }
4931
4932    /// Tcl — and iRules, which shares the shape — reports **4** for a
4933    /// `#` line inside a `proc` argument list, and that is correct.
4934    ///
4935    /// Tcl recognises a comment only where a command is expected, so
4936    /// `proc h {a\n# c\n b}` really does declare four arguments named
4937    /// `a`, `#`, `c` and `b`. The grammar agrees: it emits four
4938    /// `argument` nodes and no comment node, which is why
4939    /// `compute_tcl_args` never needed the exclusion the other
4940    /// languages did. Issue #1201 cited Tcl as the language that had
4941    /// already solved this; it had not — it has no problem to solve.
4942    ///
4943    /// **Both rows assert parity over a non-problem, not a defence.**
4944    /// Neither language has an exclusion here that a regression could
4945    /// remove; what these pin is that the *shape* stays the one described
4946    /// above, so a grammar bump that started emitting a comment node
4947    /// would surface as a count change rather than silently. The iRules
4948    /// row is the second half of a claim this doc and the #1201 changelog
4949    /// entry both made while only Tcl was exercised (#1218). It is a
4950    /// separate dialect grammar, and dialect grammars do diverge on leaf
4951    /// naming — its `argument` is kind 137 against Tcl's 93 — so it was
4952    /// dumped rather than assumed: `proc h {a\n# c\n b}` yields four
4953    /// `argument` nodes and no comment node under both.
4954    // Gated on the fixtures' own features for the reason #1220 names: the
4955    // case list is two languages wide and the loop below asserts it ran, so
4956    // a feature set enabling neither — `--no-default-features --features
4957    // rust` — would fail here and read as a defect in whatever was being
4958    // changed. The gate makes the test absent rather than vacuous; the
4959    // `ran > 0` assertion then covers the narrower case where `is_enabled`
4960    // stops agreeing with the feature it is compiled under.
4961    #[test]
4962    #[cfg(any(feature = "tcl", feature = "irules"))]
4963    fn tcl_has_no_comment_inside_a_parameter_list() {
4964        // Guarded per language rather than once: the two features are
4965        // independent, so a build with only one enabled must still run
4966        // that one's row.
4967        let mut ran = 0;
4968        for lang in [LANG::Tcl, LANG::Irules]
4969            .into_iter()
4970            .filter(LANG::is_enabled)
4971        {
4972            ran += 1;
4973            assert_eq!(
4974                args(lang, "proc h {a\n  # c\n  b} { return $a }"),
4975                (0, 4),
4976                "{lang:?}: a `#` in an argument list is an argument named `#`, not a comment"
4977            );
4978            // The uncommented control, so a regression that zeroed the
4979            // whole count would not read as this rule holding.
4980            assert_eq!(args(lang, "proc h {a b} { return $a }"), (0, 2), "{lang:?}");
4981        }
4982        // A feature set enabling neither leaves a loop of zero iterations
4983        // and a test that passes having asserted nothing — the shape
4984        // `assert_fixtures_present` exists to make loud (#1220).
4985        assert!(
4986            ran > 0,
4987            "neither tcl nor irules is enabled; this test asserted nothing"
4988        );
4989    }
4990
4991    /// The one path `count_args` never runs on: `Checker::is_bare_param`
4992    /// short-circuits before the child walk, so a comment on an
4993    /// un-parenthesised lambda parameter is safe only because of where
4994    /// the *grammar* puts it. Confirmed rather than reasoned, per
4995    /// `.claude/rules/grammar-dispatch.md`: dumping
4996    /// `x /* c */ -> x` shows `block_comment` as a **sibling** of the
4997    /// bare `identifier`, and the `parameters` field points at that
4998    /// childless identifier.
4999    ///
5000    /// So the comment is not a discriminating input here, and this test
5001    /// is deliberately written as a *parity* assertion rather than a
5002    /// count one. No perturbation distinguishes the commented spelling
5003    /// from the bare one — both fail together under every perturbation of
5004    /// the bare-parameter branch, which
5005    /// `lambda_parenthesisation_parity` already covers. What this adds
5006    /// is the guarantee that the two spellings cannot diverge, which is
5007    /// what would break if a grammar bump moved the comment inside the
5008    /// field.
5009    #[test]
5010    #[cfg(any(feature = "csharp", feature = "java"))]
5011    fn a_comment_on_a_bare_lambda_parameter_changes_nothing() {
5012        let mut checked = 0;
5013        for (lang, commented, bare) in [
5014            (
5015                LANG::Java,
5016                "class K { java.util.function.Function<Integer,Integer> f = x /* c */ -> x; }",
5017                "class K { java.util.function.Function<Integer,Integer> f = x -> x; }",
5018            ),
5019            (
5020                LANG::Csharp,
5021                "class K { System.Func<int,int> f = x /* c */ => x + 1; }",
5022                "class K { System.Func<int,int> f = x => x + 1; }",
5023            ),
5024        ]
5025        .into_iter()
5026        .filter(|(lang, ..)| lang.is_enabled())
5027        {
5028            checked += 1;
5029            let got = args(lang, commented);
5030            assert_eq!(
5031                got,
5032                args(lang, bare),
5033                "{lang:?}: the comment moved the count off the bare spelling's answer"
5034            );
5035            // The absolute value too, so a regression that zeroed both
5036            // spellings would not read as parity holding.
5037            assert_eq!(
5038                got,
5039                (1, 0),
5040                "{lang:?}: a bare lambda parameter is one closure argument"
5041            );
5042        }
5043        // The non-vacuity half the `cfg` above does not cover: a
5044        // zero-iteration loop here means `is_enabled` stopped agreeing
5045        // with the feature this compiled under (#1286).
5046        assert!(
5047            checked > 0,
5048            "neither java nor csharp is enabled; this test asserted nothing"
5049        );
5050    }
5051}
5052
5053// The guard below is a *conjunction* over three tables, so the gate is
5054// their intersection rather than their union: `cpp_only_shapes` is the
5055// narrowest at `Cpp | Mozcpp`, and both other tables include those two,
5056// so `any(cpp, mozcpp)` is exactly the set of feature configurations in
5057// which all three tallies can be non-zero (#1286). A plain union would
5058// re-admit the spurious failure — `--features c` supplies shared and
5059// attributed rows but no `cpp_only` one.
5060#[cfg(test)]
5061#[cfg(any(feature = "cpp", feature = "mozcpp"))]
5062mod c_family_return_type_declarators {
5063    use crate::test_support::space_verbatim;
5064    use crate::{LANG, MetricsOptions};
5065
5066    /// `(closure_args, function_args)` read from the fixture's sole
5067    /// nested space.
5068    ///
5069    /// The space rather than the file roll-up, because since #1196 the
5070    /// `nargs` gate reads a callable's *own* parameter count: a fix that
5071    /// repaired only the roll-up would leave the gate exactly as blind
5072    /// as #1200 found it. The pair rather than the total, so a
5073    /// regression that merely moved a count between the function and
5074    /// closure channels cannot read as a pass.
5075    #[track_caller]
5076    fn sole_space_args(lang: LANG, source: &str) -> (u64, u64) {
5077        let root = space_verbatim(lang, source.as_bytes(), MetricsOptions::default());
5078        // Descend to the *innermost* sole space, not the first one.
5079        // A free function is one level down, but a conversion operator
5080        // is two — its `struct` opens a container space in between,
5081        // whose own counters stay at zero however badly the operator is
5082        // counted. Stopping at the first level made both `operator_cast`
5083        // fixtures assert about the struct and pass with the defect
5084        // reinstated.
5085        let mut space = &root;
5086        let mut depth = 0;
5087        while let [only] = space.spaces.as_slice() {
5088            space = only;
5089            depth += 1;
5090        }
5091        assert!(depth > 0, "{lang:?}: fixture opened no space at all");
5092        (
5093            space.metrics.nargs.closure_args(),
5094            space.metrics.nargs.function_args(),
5095        )
5096    }
5097
5098    /// The return shapes every C-derived grammar here shares, plus the
5099    /// unwrapped control that keeps the fix honest.
5100    ///
5101    /// Every pointer row reported **0** before #1200 and the nested rows
5102    /// reported the *return type's* arity; the plain row already passed
5103    /// and is here so a helper that returned nothing at all could not
5104    /// look like a fix.
5105    ///
5106    /// Each nested row deliberately gives the inner and outer parameter
5107    /// lists **different** lengths. An earlier draft of the `__cdecl`
5108    /// row spelled both as one argument, which made it agree with the
5109    /// answer it was written to reject.
5110    fn c_declarator_shapes(lang: LANG) -> Option<&'static [(&'static str, (u64, u64))]> {
5111        if !matches!(lang, LANG::C | LANG::Cpp | LANG::Mozcpp | LANG::Objc) {
5112            return None;
5113        }
5114        // C declarator syntax, shared by all four grammars. `Foo` is
5115        // deliberately an undeclared type: tree-sitter resolves the
5116        // shape syntactically, and a fixture that leaned on a typedef
5117        // would be testing the fixture.
5118        Some(&[
5119            // A pointer return: the reported symptom in #1200.
5120            ("FILE *f(int a, int b, int c) { return 0; }", (0, 3)),
5121            // Two levels of `pointer_declarator`, so a walk that steps
5122            // exactly once still fails.
5123            ("int **g(int a, int b) { return 0; }", (0, 2)),
5124            // A storage-class specifier ahead of the pointer, which
5125            // sits outside the declarator entirely.
5126            ("static int *h(int a) { return 0; }", (0, 1)),
5127            // A function returning a pointer to a one-argument
5128            // function. The *outer* `function_declarator` owns
5129            // `(int c)` — the return type's list — so taking the first
5130            // `parameters` found reports 1. `fp` takes two.
5131            ("int (*fp(int a, int b))(int c) { return 0; }", (0, 2)),
5132            // The same shape with an MSVC calling convention, which
5133            // parses as a real `ms_call_modifier` node *preceding* the
5134            // declarator inside the `parenthesized_declarator`. This is
5135            // what makes the fallback take the last named child rather
5136            // than the first.
5137            (
5138                "int (__cdecl *w(int a, int b))(int c) { return 0; }",
5139                (0, 2),
5140            ),
5141            // The GNU attribute spelling, which all four grammars
5142            // absorb *into* the `function_declarator` rather than
5143            // wrapping it — so it never builds an
5144            // `attributed_declarator` and was never miscounted. It is
5145            // the control for the C++11 spelling below, a different
5146            // tree for the same source-level idea.
5147            (
5148                "int gdef(int a, int b) __attribute__((deprecated)) { return a; }",
5149                (0, 2),
5150            ),
5151            // C's `(void)` marker declares *no* parameters, but the
5152            // grammar emits a real `parameter_declaration` for it, so
5153            // every negative filter counted it as one.
5154            ("int none(void) { return 0; }", (0, 0)),
5155            // The two shapes `(void)` must not be confused with. An
5156            // unnamed parameter is structurally identical — a bare type
5157            // with no declarator — and really is one argument, so only
5158            // the bytes separate them. `void *` carries a declarator
5159            // and is likewise a real parameter.
5160            ("int unnamed(int) { return 0; }", (0, 1)),
5161            ("int ptr(void *p, int a) { return 0; }", (0, 2)),
5162            // The unwrapped control: its `declarator` field already is
5163            // the `function_declarator`, so it passed before the fix
5164            // and must keep passing after it.
5165            ("int plain(int a, int b) { return a; }", (0, 2)),
5166            // An unexpanded function-like macro in declarator position,
5167            // the shape every JNI shim takes (#1213). The macro's
5168            // `(name)` is the innermost list, so #1200's walk read 1
5169            // where the function declares 2.
5170            ("void MACRO(name)(int a, int b) { }", (0, 2)),
5171            // The multi-argument spelling. A gate keyed on the inner
5172            // list holding a single bare `type_identifier` — the
5173            // heuristic form the report proposed — passes the row above
5174            // and misses this one, which read the macro's 2 rather than
5175            // the function's 1. The two lists are deliberately
5176            // different lengths and in the opposite direction to the
5177            // row above, so neither row can agree with the other's
5178            // wrong answer.
5179            ("void MACRO(a, b)(int x) { }", (0, 1)),
5180            // The two mechanisms composed: a return type to step
5181            // through *and* a macro to stop at, so this is the only row
5182            // where the gate fires at a link the chain reached rather
5183            // than at the one it started from. It is also why the gate
5184            // tests `current`'s kind and not just the inner link's — a
5185            // `pointer_declarator`'s `declarator` field is a
5186            // `function_declarator` too, and stopping there lands on a
5187            // node with no `parameters`. That half is not exclusively
5188            // this row's to guard, though: dropping it also regresses
5189            // #1200's own `FILE *f(…)` and `int **g(…)` rows to 0.
5190            ("char *MACRO(n)(int a, int b) { return 0; }", (0, 2)),
5191            // Two nested invocations, so a gate that stopped one link
5192            // in still reads a macro's list. Three distinct lengths
5193            // because the single-nesting spelling `A(b)(c)(int x)`
5194            // reads 1 both before the fix and after it, and would
5195            // prove nothing.
5196            ("void A(b, c)(d)(int x, int y, int z) { }", (0, 3)),
5197            // A return type that nests without being a
5198            // `function_declarator` at all: the outer link is an
5199            // `array_declarator`, so the macro gate has nothing to fire
5200            // on and the chain must still reach `arr`'s own list. Two
5201            // arguments rather than the `(void)` this row first
5202            // carried — `(0, 0)` is what a walk returning `None` for
5203            // *everything* reports, so the row passed with the whole
5204            // chain dead while 28 others failed
5205            // (`.claude/rules/testing.md`, "Seed the state you claim to
5206            // assert on").
5207            ("int (*arr(int a, int b))[4] { return 0; }", (0, 2)),
5208        ])
5209    }
5210
5211    /// The C++11 `[[…]]` attribute, which is the only spelling that
5212    /// builds an `attributed_declarator` — the one fieldless rule
5213    /// putting its declarator *first*, so the last-named-child fallback
5214    /// lands on the attribute unless `attribute_declaration` is
5215    /// excluded. Reported 0 both before #1200 and after its first cut.
5216    ///
5217    /// Objective-C is absent because its grammar parses `[[…]]` on a
5218    /// *definition* as a `declaration` — no `function_definition`, so no
5219    /// space and nothing to count. That is upstream, not a miscount of
5220    /// ours; the GNU spelling above covers Objective-C's attribute path.
5221    fn cpp11_attribute_shapes(lang: LANG) -> Option<&'static [(&'static str, (u64, u64))]> {
5222        matches!(lang, LANG::C | LANG::Cpp | LANG::Mozcpp).then_some(&[(
5223            "int attr(int a, int b) [[deprecated]] { return a; }",
5224            (0, 2),
5225        )])
5226    }
5227
5228    /// The shapes only C++ has: `reference_declarator`, which — unlike
5229    /// `pointer_declarator` — exposes no `declarator` field at all, and
5230    /// the lambda, which reaches `params_owner` through the closure
5231    /// channel rather than the function one.
5232    fn cpp_only_shapes(lang: LANG) -> Option<&'static [(&'static str, (u64, u64))]> {
5233        Some(match lang {
5234            LANG::Cpp | LANG::Mozcpp => &[
5235                ("int &r(int a, int b) { static int x; return x; }", (0, 2)),
5236                // The rule's other spelling: `&&` is a distinct token
5237                // in the same `reference_declarator`, so a fix keyed on
5238                // the `&` token alone would pass the row above.
5239                (
5240                    "Foo &&m(int a) { static Foo f; return static_cast<Foo &&>(f); }",
5241                    (0, 1),
5242                ),
5243                // A member function returning a reference: the chain
5244                // ends at a `qualified_identifier` rather than a bare
5245                // one, which has named children of its own.
5246                ("Foo &Bar::get(int a) { static Foo f; return f; }", (0, 1)),
5247                // `operator()` is the one construct whose *source text*
5248                // looks like the macro nesting #1213 gates on, and it is
5249                // not rare: 1,546 function spaces across `DeepSpeech`,
5250                // against 46 direct nestings not one of which is an
5251                // operator. The grammar emits a single `operator_name`
5252                // with the parameter list as its sibling, so the gate is
5253                // unreachable from here rather than merely inactive.
5254                //
5255                // Which is what this row guards, and it is worth being
5256                // precise: no widening of the gate can fail it, because
5257                // the chain already stops at this declarator. It fails
5258                // if a future `tree-sitter-cpp` starts spelling
5259                // `operator()` as a nested `function_declarator` — at
5260                // which point the gate would silently halve the reported
5261                // arity of that whole population.
5262                (
5263                    "struct S { int operator()(int a, int b) const { return a; } };",
5264                    (0, 2),
5265                ),
5266                // An explicit template argument spelling a function
5267                // type. `template_function` is another name form with
5268                // named children, and its last one is the argument
5269                // list — so the last-named-child fallback walks off the
5270                // name side into `int (*)(int x, int y)` and bills that
5271                // type's two parameters to a one-argument function
5272                // unless `template_argument_list` is excluded. The two
5273                // lists are deliberately different lengths, so the row
5274                // cannot agree with the answer it rejects.
5275                (
5276                    "template <> void tspec<int (*)(int x, int y)>(int a) { }",
5277                    (0, 1),
5278                ),
5279                // A conversion operator takes no arguments, however
5280                // many its target *type* has. `operator_cast` is the
5281                // one link whose `declarator` field leaves the name
5282                // side, and following it billed the converted-to
5283                // function-pointer type's `(int x)` to the operator —
5284                // a regression the first cut of #1200 introduced and
5285                // no fixture then covered.
5286                (
5287                    "struct S { operator int (*)(int x) { return nullptr; } };",
5288                    (0, 0),
5289                ),
5290                // The same shape through a reference, so the fix cannot
5291                // be keyed on the pointer spelling alone.
5292                (
5293                    "struct S { operator int (&)(int x, int y) { static int *p; return *reinterpret_cast<int (*)(int, int)>(p); } };",
5294                    (0, 0),
5295                ),
5296                (
5297                    "int g() { auto f = [](int a, int b){ return a + b; }; return f(1, 2); }",
5298                    (2, 0),
5299                ),
5300                // The one shape with no declarator to walk: a
5301                // parameterless lambda has no
5302                // `abstract_function_declarator` at all, so the walk
5303                // returns `None` on its first step and `params_owner`
5304                // falls back to the node. `g` carries parameters of its
5305                // own so the expected pair is not all-zero — an
5306                // all-default expectation would hold however badly the
5307                // fallback behaved.
5308                (
5309                    "int g(int a, int b) { auto f = []{ return 1; }; return f(); }",
5310                    (0, 2),
5311                ),
5312                // …and the shape that makes that first step *matter*.
5313                // The row above executes the early return but cannot
5314                // discriminate it — perturbing the walk to begin at the
5315                // last named child instead of the `declarator` field
5316                // fails none of the suite, because a parameterless
5317                // lambda's body has nothing carrying `parameters` down
5318                // its last-child spine.
5319                //
5320                // A local *function declaration* does. `declaration`
5321                // has a `declarator` field of its own, so a walk that
5322                // starts outside the declarator chain lands on `q`'s
5323                // `function_declarator` and bills its two parameters to
5324                // the enclosing lambda, which declares none.
5325                (
5326                    "int g(int a, int b) { auto f = []{ int q(int x, int y); }; f(); return a + b; }",
5327                    (0, 2),
5328                ),
5329                // The guard for the walk's stop condition. A lambda's
5330                // `abstract_function_declarator` carries `parameters`
5331                // but its `declarator` field is *optional* and absent
5332                // here, so a walk that falls through to the last named
5333                // child descends into the parameter list and reports
5334                // `cb`'s own `(int x)` — one argument instead of two.
5335                (
5336                    "int g() { auto f = [](int a, int (*cb)(int x)){ return cb(a); }; return 0; }",
5337                    (2, 0),
5338                ),
5339            ],
5340            _ => return None,
5341        })
5342    }
5343
5344    #[test]
5345    fn a_wrapped_return_type_does_not_hide_the_parameter_list() {
5346        let (mut shared, mut cpp_only, mut attributed) = (0, 0, 0);
5347        let mut failures = Vec::new();
5348        for lang in LANG::into_enum_iter().filter(LANG::is_enabled) {
5349            for (table, counter) in [
5350                (c_declarator_shapes(lang), &mut shared),
5351                (cpp_only_shapes(lang), &mut cpp_only),
5352                (cpp11_attribute_shapes(lang), &mut attributed),
5353            ] {
5354                for (source, expected) in table.unwrap_or_default() {
5355                    *counter += 1;
5356                    let got = sole_space_args(lang, source);
5357                    // Collected rather than asserted inline so a
5358                    // regression shows every language and shape it
5359                    // broke, not just the first. The branch carries no
5360                    // formatting — a line that runs only on failure can
5361                    // never be covered.
5362                    if got != *expected {
5363                        failures.push((lang, source, *expected, got));
5364                    }
5365                }
5366            }
5367        }
5368        let (failed, checked) = (failures.len(), shared + cpp_only + attributed);
5369        assert!(
5370            failures.is_empty(),
5371            "{failed}/{checked} return-type shapes lost their parameter list: {failures:#?}"
5372        );
5373        // Every tally, so a renamed `LANG` variant or a feature that
5374        // stopped being enabled fails here rather than passing
5375        // vacuously.
5376        assert!(
5377            shared > 0 && cpp_only > 0 && attributed > 0,
5378            "no fixture ran (shared={shared}, cpp_only={cpp_only}, \
5379             attributed={attributed}); this test asserted nothing"
5380        );
5381    }
5382
5383    // The #1208 bug-lock that stood here — asserting these shapes lost
5384    // their space *name* while keeping their arity — was retired when
5385    // #1208 landed. The name half now lives beside the walk it shares
5386    // with this module, in `crate::c_declarator`.
5387}