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