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