Skip to main content

big_code_analysis_ast/
checker.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
9//! The [`Checker`] classification predicates and their per-language impls.
10
11use std::sync::OnceLock;
12
13use aho_corasick::AhoCorasick;
14use regex::bytes::Regex;
15
16use crate::cfg_predicate::attribute_marks_test as rust_attribute_marks_test;
17use crate::macros::csharp_invocation_expr_kinds;
18use crate::*;
19
20static AHO_CORASICK: OnceLock<AhoCorasick> = OnceLock::new();
21static RE: OnceLock<Regex> = OnceLock::new();
22
23// Shared ancestor-walk scaffold behind `check_if_func!` /
24// `check_if_arrow_func!`. Both decide whether a JS `function_expression`
25// / `arrow_function` is a *named function* (vs. an anonymous closure) by
26// counting ancestors that bind the expression to a name (`$up` kinds:
27// `var x = …`, `x = …`, `label:`, object `pair`) while stopping the walk
28// at frames that prove the expression is used positionally (`$stop`
29// kinds: a block, return, `new`, call/arguments). A positive count, or a
30// per-call `$extra` adjacency check, marks it a named function.
31//
32// This must stay a `macro_rules!` (not a `fn`): the `$up` / `$stop`
33// variant lists are matched against each JS-family language's own
34// `kind_id` enum, brought into scope by a per-language `use $language::*`
35// glob at the call site. A function could not name variants that only
36// exist after that glob import.
37//
38// Kept token-identical to the two hand-written predicates it replaced:
39// the bracketed variant lists expand straight into the `matches!`
40// patterns and `$extra` into the trailing `|| …` disjunct.
41macro_rules! js_ancestor_walk {
42    (
43        $node:ident,
44        $ancestors:ident,
45        [$($up:ident)|+],
46        [$($stop:ident)|+],
47        $extra:expr $(,)?
48    ) => {
49        // `Self` is the enclosing `impl Checker for <Lang>Code` type, so
50        // the ancestor walk's `is_else_if` filter is bound directly to the
51        // language's own `Checker` (no `ParserTrait` round-trip).
52        //
53        // `$ancestors` is the chain the caller descended through, so each
54        // step is a slice index. The walk stops at the first frame that
55        // proves positional use — an enclosing block, `return`, `new`, or
56        // call — so on ordinary JS it takes one or two steps. That is
57        // what made the pre-#1088 cost surprising: `Node::parent` is
58        // `O(depth)` per step whatever the count, so even a two-step walk
59        // was quadratic over a deeply nested file, which is what the
60        // `nom/nested-arrow` probe measures.
61        //
62        // A chain of *expression*-bodied arrows (`a => b => c => …`) is
63        // the one shape no chain can flatten: no `$stop` kind intervenes,
64        // so the walk itself is `O(depth)` long. The chain takes it from
65        // `O(depth^2)` per node to `O(depth)`; it stays quadratic overall
66        // and would need a different rule, not a cheaper lookup, to fix.
67        $node.count_specific_ancestors::<Self>(
68            $ancestors,
69            |node| matches!(node.kind_id().into(), $($up)|+),
70            |node| matches!(node.kind_id().into(), $($stop)|+),
71        ) > 0
72            || $extra
73    };
74}
75
76// Is this expression *bound* to a name, or used positionally?
77//
78// `check_if_func!` and `check_if_arrow_func!` answer that question for
79// the two anonymous-function forms, and until #1188 they answered it
80// with two different ancestor walks. Four divergences, each measured:
81//
82// 1. `$stop` was `Arguments` for one and `CallExpression` for the other
83//    — different tree levels, since `call_expression > arguments >
84//    function_expression`. An IIFE's chain is `parenthesized_expression
85//    -> call_expression` with no `arguments` on it at all, so the func
86//    walk ran past the call and reached the binding: `(function(c){})(1)`
87//    was a closure while `const v = (function(c){})(1)` was a function.
88//    The same construct, classified by what happened one level up past
89//    the call.
90// 2. `Pair` was in the func `$up` only, so `({ "k": function(){} })` was
91//    a function and `({ "k": () => {} })` a closure. The arrow reached
92//    the same verdict for a *bare* key only, and by a different
93//    mechanism — `$extra`'s `property_identifier` sibling — which a
94//    string, number or computed key does not provide.
95// 3. `has_sibling(PropertyIdentifier)` was in the arrow `$extra` only,
96//    the exact mirror: `class C { p = () => {}; }` was a function and
97//    `class C { p = function(){}; }` a closure.
98// 4. `is_child(Identifier)` is in the func `$extra` only, and **must
99//    stay there**. For a function expression the only possible
100//    `identifier` *child* is its optional name, so it means "carries its
101//    own name" and `run(function g(){})` is rightly a function. An arrow
102//    stores its un-parenthesised single parameter in exactly that
103//    position, so unifying it would make `run(x => x)` — the commonest
104//    callback shape in any JS corpus — a function.
105//
106// The first three are unified below, which makes an IIFE a closure in
107// both spellings: that is what a reader sees, and it matches the
108// behaviour the arrow form already had. `$up` and `$stop` are
109// token-identical and only `$extra` differs, for the reason in (4).
110//
111// The field-definition kind is threaded in per language rather than
112// named here because JS and TS spell it differently (`field_definition`
113// vs `public_field_definition`). Both macros take it: `$extra`'s
114// `property_identifier` sibling covers a class field only when its name
115// *is* an identifier, so without the `$up` entry
116// `class C { ["k"] = () => 1 }`, `{ "s" = … }` and `{ #p = … }` stayed
117// closures while their `function` spellings were already functions —
118// divergence (3) surviving in the shapes the first fix did not reach.
119macro_rules! check_if_func {
120    ($node: ident, $ancestors: ident, $field_definition: ident) => {
121        js_ancestor_walk!(
122            $node,
123            $ancestors,
124            [VariableDeclarator
125                | AssignmentExpression
126                | LabeledStatement
127                | Pair
128                | $field_definition],
129            [StatementBlock | ReturnStatement | NewExpression | CallExpression | Arguments],
130            $node.is_child(Identifier as u16),
131        )
132    };
133}
134
135macro_rules! check_if_arrow_func {
136    ($node: ident, $ancestors: ident, $field_definition: ident) => {
137        js_ancestor_walk!(
138            $node,
139            $ancestors,
140            [VariableDeclarator
141                | AssignmentExpression
142                | LabeledStatement
143                | Pair
144                | $field_definition],
145            [StatementBlock | ReturnStatement | NewExpression | CallExpression | Arguments],
146            $node.has_sibling($ancestors, PropertyIdentifier as u16),
147        )
148    };
149}
150
151// A generator is a function, not a closure (#1186). `function* g() {}`
152// is lexically a function *declaration* — the `*` is a modifier on the
153// declaration, not a different binding form — and `get_space_kind`
154// already calls both generator kinds `SpaceKind::Function`. The Checker
155// disagreeing with the Getter is what made `nom` report a generator as a
156// closure, bill its parameters to `closure_args`, and (reaching no
157// cognitive boundary arm) let it inherit the enclosing nesting.
158//
159// The two kinds are **not** symmetrical, which the issue's "move both"
160// framing misses. `generator_function_declaration` has a required `name`
161// field and mirrors `function_declaration`, so it is unconditionally a
162// function. `generator_function` has an *optional* one and mirrors
163// `function_expression`, so it routes through the same
164// binding-site walk: making it unconditional would classify
165// `run(function*(){})` as a function while `run(function(){})` stays a
166// closure.
167macro_rules! is_js_func {
168    ($node: ident, $ancestors: ident, $field_definition: ident) => {
169        match $node.kind_id().into() {
170            FunctionDeclaration | GeneratorFunctionDeclaration | MethodDefinition => true,
171            FunctionExpression | GeneratorFunction => {
172                check_if_func!($node, $ancestors, $field_definition)
173            }
174            ArrowFunction => check_if_arrow_func!($node, $ancestors, $field_definition),
175            _ => false,
176        }
177    };
178}
179
180macro_rules! is_js_closure {
181    ($node: ident, $ancestors: ident, $field_definition: ident) => {
182        match $node.kind_id().into() {
183            FunctionExpression | GeneratorFunction => {
184                !check_if_func!($node, $ancestors, $field_definition)
185            }
186            ArrowFunction => !check_if_arrow_func!($node, $ancestors, $field_definition),
187            _ => false,
188        }
189    };
190}
191
192macro_rules! is_js_func_and_closure_checker {
193    ($language: ident, $field_definition: ident) => {
194        #[inline]
195        fn is_func<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
196            use $language::*;
197            is_js_func!(node, ancestors, $field_definition)
198        }
199
200        #[inline]
201        fn is_closure<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
202            use $language::*;
203            is_js_closure!(node, ancestors, $field_definition)
204        }
205    };
206}
207
208// Generate an `is_string` impl for a JS-family `Checker` block: the
209// named `String` literal, `TemplateString`, plus per-language
210// anonymous *literal* aliases passed as extras. The alterator
211// flattens the same kinds; the generic `string` filter must agree
212// (issue #283). Type-keyword aliases (`: string`, which parses as an
213// anonymous `"string"` token under a `predefined_type` wrapper) are
214// not literals and stay out (#1261); per-call rationale sits above
215// each invocation.
216macro_rules! impl_js_family_is_string {
217    ($lang:ident $(, $extra:ident)* $(,)?) => {
218        fn is_string(node: &Node) -> bool {
219            matches!(
220                node.kind_id().into(),
221                $lang::String | $lang::TemplateString
222                    $(| $lang::$extra)*
223            )
224        }
225    };
226}
227
228// Generate an `is_string` impl for languages whose `is_string`
229// predicate is a flat `matches!` against one or more variant
230// kinds. Reduces drift risk for new alias additions and gives a
231// single table that answers "which kinds count as a string for
232// `find string` / `count string`?" per language (issue #301).
233//
234// Languages whose `is_string` needs anything beyond a flat variant
235// list (e.g. JS family's `String` + `String2` + `TemplateString`
236// pattern) keep their own dedicated macros or impls.
237macro_rules! impl_simple_is_string {
238    ($lang:ident, $first:ident $(, $rest:ident)* $(,)?) => {
239        fn is_string(node: &Node) -> bool {
240            matches!(
241                node.kind_id().into(),
242                $lang::$first $(| $lang::$rest)*
243            )
244        }
245    };
246}
247
248// Generate an `is_else_if` impl for the grammars that model `else if` as
249// an `$if_kind` node nested directly inside an `else_clause` wrapper
250// (`else_clause → if`). Covers C++/JS/TS/TSX (`IfStatement`) and Rust
251// (`IfExpression`). Per-call rationale (e.g. the `IfExpression` choice)
252// is hoisted above each invocation per `.claude/rules/macro-comments.md`.
253macro_rules! impl_is_else_if_parent_clause {
254    ($lang:ident, $if_kind:ident, $else_clause:ident) => {
255        #[inline]
256        fn is_else_if<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
257            node.kind_id() == $lang::$if_kind
258                && ancestors
259                    .parent(node)
260                    .is_some_and(|parent| parent.kind_id() == $lang::$else_clause)
261        }
262    };
263}
264
265// Generate an `is_else_if` impl for the grammars that emit a bare `else`
266// keyword token followed by a nested `$if_kind` sibling, with no wrapping
267// clause node. Covers Java/C#/Groovy (`IfStatement`) and Kotlin
268// (`IfExpression`).
269macro_rules! impl_is_else_if_prev_sibling {
270    ($lang:ident, $if_kind:ident, $else_kw:ident) => {
271        #[inline]
272        fn is_else_if<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
273            node.kind_id() == $lang::$if_kind
274                && ancestors
275                    .previous_sibling(node)
276                    .is_some_and(|prev| prev.kind_id() == $lang::$else_kw)
277        }
278    };
279}
280
281// Generate an `is_else_if` impl for the grammars that expose a dedicated
282// elseif/elsif clause node, so the clause node itself is the else-if
283// (no `if`-nesting or sibling-token shape to inspect). Flat `matches!`
284// against one or more variant kinds, analogous to `impl_simple_is_string!`;
285// the multi-variant form covers PHP's `ElseIfClause | ElseIfClause2`.
286macro_rules! impl_is_else_if_clause {
287    ($lang:ident, $first:ident $(, $rest:ident)* $(,)?) => {
288        #[inline]
289        fn is_else_if(node: &Node, _ancestors: Ancestors<'_, '_>) -> bool {
290            matches!(
291                node.kind_id().into(),
292                $lang::$first $(| $lang::$rest)*
293            )
294        }
295    };
296}
297
298#[inline]
299fn get_aho_corasick_match(code: &[u8]) -> bool {
300    AHO_CORASICK
301        .get_or_init(|| {
302            AhoCorasick::new(vec![b"<div rustbindgen"])
303                .expect("constant single-needle AhoCorasick automaton always compiles")
304        })
305        .is_match(code)
306}
307
308/// A [`Checker::should_skip_subtree`] verdict, plus the following
309/// nodes it also answers for.
310///
311/// Most verdicts are about one node and carry no reach. The exception
312/// is a verdict a classifier can only reach by reading a *run* of
313/// siblings: Rust's `#[…]` rows all decorate the same item, so they all
314/// get the same answer, and re-deriving it per row is `O(run)` work
315/// done `run` times. A run of `3 * depth` attributes under a parent
316/// wide enough to stay inside the forward attribute-scan budget made
317/// that quadratic — 5.2 s at depth 2 000, against 0.05 s with
318/// `exclude_tests` off (#1446). Reporting the reach lets the walker ask
319/// the classifier once per run.
320///
321/// # Soundness
322///
323/// The reach is a byte offset, and the walk visits nodes in
324/// non-decreasing `start_byte` order, so "starts before it" is a state
325/// the walker can test in `O(1)` and can never re-enter. A claim is
326/// sound when every node the walk *visits* inside the reach genuinely
327/// has the reported verdict.
328///
329/// "Visits" is the load-bearing word, and it cuts the obligation in
330/// two. A `SKIP` reach need only hold for the nodes it skips, since a
331/// skipped node's descendants are never pushed. A `RETAIN` reach
332/// carries the heavier claim: the members' descendants *are* walked, so
333/// it must be right for them too.
334#[derive(Clone, Copy, PartialEq, Eq, Debug)]
335pub struct SubtreeSkip {
336    skipped: bool,
337    covers_through_byte: Option<usize>,
338}
339
340impl SubtreeSkip {
341    /// Keep this node. Answers for nothing else.
342    pub const RETAIN: Self = Self {
343        skipped: false,
344        covers_through_byte: None,
345    };
346
347    /// Elide this node and its descendants. Answers for nothing else.
348    pub const SKIP: Self = Self {
349        skipped: true,
350        covers_through_byte: None,
351    };
352
353    /// [`Self::SKIP`] or [`Self::RETAIN`], from a predicate's answer.
354    #[inline]
355    #[must_use]
356    pub const fn of(skipped: bool) -> Self {
357        if skipped { Self::SKIP } else { Self::RETAIN }
358    }
359
360    /// Extend this verdict to every node that starts before `byte`.
361    ///
362    /// The caller is asserting that it read far enough to answer for
363    /// all of them — see the soundness note on the type.
364    #[inline]
365    #[must_use]
366    pub const fn covering_through(self, byte: usize) -> Self {
367        Self {
368            covers_through_byte: Some(byte),
369            ..self
370        }
371    }
372
373    /// Whether this node and its descendants are elided.
374    #[inline]
375    #[must_use]
376    pub const fn is_skipped(self) -> bool {
377        self.skipped
378    }
379
380    /// Whether this verdict already answers for a node starting at
381    /// `start_byte`, so the classifier need not be asked again.
382    #[inline]
383    #[must_use]
384    pub const fn answers_for(self, start_byte: usize) -> bool {
385        match self.covers_through_byte {
386            Some(through) => start_byte < through,
387            None => false,
388        }
389    }
390}
391
392/// Per-language AST classification predicates the metric walkers use to
393/// recognize comments, function spaces, calls, strings, branches, and so
394/// on by node kind.
395///
396/// Every predicate defaults to `false` — "this node is never of that
397/// category." A language implements only the predicates its grammar
398/// actually expresses; categories it lacks fall through to the default,
399/// so adding a language no longer requires copy-pasting `-> false` stubs.
400/// The flip side is that the compiler cannot flag a forgotten override, so
401/// each language's per-metric tests are the safety net for completeness.
402///
403/// `#[doc(hidden)]` for the reason the crate root gives: this is
404/// plumbing, and the surface the README points a reader at is `LANG` /
405/// `Node` / `SpaceKind`, not a 200-method classifier trait.
406#[doc(hidden)]
407pub trait Checker {
408    /// Whether `node` is a comment.
409    #[inline]
410    #[must_use]
411    fn is_comment(_: &Node) -> bool {
412        false
413    }
414    /// Whether this comment must survive `strip_comments`.
415    ///
416    /// `ancestors` is the chain the comment-removal walk descended
417    /// through. Rust's impl reads the parent to keep a comment that is
418    /// a macro token; reaching it with [`Node::parent`] costs
419    /// `O(depth)` per comment (#1096).
420    #[inline]
421    #[must_use]
422    fn is_useful_comment<'a>(_: &Node<'a>, _: &[u8], _: Ancestors<'a, '_>) -> bool {
423        false
424    }
425    /// Whether `node` opens a space: a function, closure, or one of the
426    /// containers (class, struct, trait, impl, namespace, interface).
427    #[inline]
428    #[must_use]
429    fn is_func_space(_: &Node) -> bool {
430        false
431    }
432    /// Whether `node` is a *named* function definition.
433    ///
434    /// `ancestors` is the chain the caller descended through. The
435    /// JS-family grammars have no distinct production for a named
436    /// function: `const f = () => …` and `g(() => …)` share one
437    /// `arrow_function` node, and only what encloses it says which is
438    /// which. Those steps come off the chain, because `Node::parent`
439    /// costs `O(depth)` each however few of them a walk takes (#1088).
440    /// Every other grammar answers `is_func` from the node's own kind
441    /// and ignores the parameter.
442    #[inline]
443    #[must_use]
444    fn is_func<'a>(_: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
445        false
446    }
447    /// Whether `node` is an anonymous function — the complement of
448    /// [`is_func`](Self::is_func) on the grammars that express both. Takes `ancestors`
449    /// for the same reason, plus one of its own: Ruby's `{ … }` block is
450    /// a closure only when its parent is not the `Lambda` that already
451    /// counted it.
452    #[inline]
453    #[must_use]
454    fn is_closure<'a>(_: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> bool {
455        false
456    }
457    /// Whether `node` is a *call site* — a function, method or command
458    /// invocation a reader would navigate to.
459    ///
460    /// This is the predicate behind the `call` filter that
461    /// [`ParserTrait::filters`](crate::traits::ParserTrait::filters)
462    /// builds, so it decides what `bca find --type call` and
463    /// `bca count --type call` report.
464    ///
465    /// It is deliberately narrower than the ABC `branches` axis, which
466    /// applies Fitzpatrick's *"function invocation or object creation"*
467    /// rule. Object construction (`new T(…)`) and constructor
468    /// delegation — C#'s `: base(x)` / `: this(a)` and a C# 12 primary
469    /// constructor's `: Base(x)`, Java's `super(…)` / `this(…)`,
470    /// Kotlin's `constructor_invocation` — are ABC branches and are
471    /// **not** call sites here: a constructor is not a function the
472    /// `find` result should point a reader at. The languages whose
473    /// `branches` axis consequently exceeds their `is_call` set are C#,
474    /// Java, Groovy, Kotlin, C++, Mozcpp, JavaScript, MozJS,
475    /// TypeScript, TSX, PHP, Ruby (`super` / `yield` count as branches)
476    /// and Rust (`?` does).
477    ///
478    /// The relation is not a plain superset everywhere. Four languages
479    /// depart from it, each in its own direction:
480    ///
481    /// * Tcl and iRules bill a mutator command (`incr`, `append`,
482    ///   `lappend`) as an ABC *assignment* rather than a branch while
483    ///   `is_call` still counts it. `set` is not one of those: it has a
484    ///   grammar production of its own (`Tcl::Set` / `Irules::Set`)
485    ///   rather than being a `command`, so it is an ABC assignment that
486    ///   `is_call` never matches either.
487    /// * Elixir excludes its definition and directive `Call`s (`def`,
488    ///   `defmodule`, `alias`, `import`, …) from `branches` while
489    ///   counting each `|>` pipeline step.
490    /// * Perl counts a bareword call only at the outermost dispatch
491    ///   site: in `print shift;` the wrapper
492    ///   `call_expression_with_spaced_args` is the branch and the
493    ///   `print` bareword it holds is skipped, so that bareword is an
494    ///   `is_call` match with no branch behind it.
495    ///
496    /// The two sets coincide only in C, Objective-C, Go, Python, Lua
497    /// and Bash, which have neither a separate construction syntax nor
498    /// a gated branch arm.
499    // `is_call` and each language's ABC branch arm classify the same
500    // nodes through parallel `matches!()` tables, so they drift
501    // silently; the divergence is pinned by
502    // `groovy_is_call_excludes_constructors` (#430) and
503    // `csharp_is_call_excludes_constructors` in this file's tests.
504    #[inline]
505    #[must_use]
506    fn is_call(_: &Node) -> bool {
507        false
508    }
509    /// Whether `node`, met among a parameter list's children, is
510    /// punctuation or a keyword rather than a parameter (`nargs` skips it).
511    #[inline]
512    #[must_use]
513    fn is_non_arg(_: &Node) -> bool {
514        false
515    }
516    /// Whether `param` is a language's way of spelling *"this function
517    /// takes nothing"* inside an otherwise ordinary parameter list.
518    ///
519    /// C's `int f(void)` declares zero parameters, but the grammar
520    /// emits a real `parameter_declaration` for the `void`, so the
521    /// negative filters in `big_code_analysis::nargs` counted it as one — `f` and
522    /// `int f(int)` both reported 1 despite one of them taking no
523    /// argument at all.
524    ///
525    /// It takes `code` because the question is not structural: a
526    /// `parameter_declaration` holding a bare `primitive_type` and no
527    /// declarator is *also* how an unnamed parameter is spelled, and
528    /// `void f(int)` really does take one. Only the bytes tell them
529    /// apart — `.claude/rules/grammar-dispatch.md` §10.
530    #[inline]
531    #[must_use]
532    fn is_empty_param_marker(_param: &Node, _code: &[u8]) -> bool {
533        false
534    }
535    /// Whether `node` — the value of a function's `parameters` field —
536    /// is itself a *single formal parameter* rather than a parameter
537    /// *list*.
538    ///
539    /// `compute_args` walks the field node's children, which is right
540    /// for a list and yields zero for a lone parameter that has none.
541    /// Java's `lambda_expression` spells its `parameters` field three
542    /// ways — `formal_parameters` and `inferred_parameters` are lists,
543    /// but the un-parenthesised `x -> …` form puts a bare, childless
544    /// `identifier` there — and C#'s puts a childless
545    /// `implicit_parameter`. Both reported `nargs = 0` for a lambda
546    /// that plainly has one argument (#1185).
547    ///
548    /// Gated per language rather than inferred from `child_count() == 0`
549    /// so a MISSING or zero-width ERROR node under `parameters` on a
550    /// broken parse cannot silently conjure an argument.
551    #[inline]
552    #[must_use]
553    fn is_bare_param(_: &Node) -> bool {
554        false
555    }
556    /// Whether `node` is a string literal, under every aliased kind the
557    /// grammar emits for one.
558    ///
559    /// This is the *kind table* half of the answer. Where a grammar
560    /// spells a string literal and something else with one kind, the
561    /// table cannot separate them and the walk must ask
562    /// [`is_string_with_code`](Self::is_string_with_code) instead.
563    #[inline]
564    #[must_use]
565    fn is_string(_: &Node) -> bool {
566        false
567    }
568    /// Source-aware variant of [`is_string`](Self::is_string), and the
569    /// spelling every walk calls.
570    ///
571    /// The default forwards to the byte-less predicate, so a language
572    /// whose string literals are a closed set of kinds needs no
573    /// override. The Tcl family is the case that does: `braced_word` is
574    /// both the literal of `lappend x {a b}` and the *script* of a
575    /// `proc` body or an iRules `when` handler, and only the enclosing
576    /// command's leading word tells the two apart (#1318, #1381). So
577    /// `bca find --type string` reported every script body in the file
578    /// as a string literal.
579    ///
580    /// **A call site reaching for the byte-less spelling reads as
581    /// correct against every language without an override, and is wrong
582    /// only on the one that has one** —
583    /// `.claude/rules/grammar-dispatch.md` §7. There is one call site,
584    /// the `"string"` arm of [`ParserTrait::filters`], and it takes this
585    /// spelling; `tcl_script_body_is_a_string_only_to_the_byteless_spelling`
586    /// pins that the two really do disagree, so a future call site that
587    /// picks the wrong one is not silently right.
588    ///
589    /// [`ParserTrait::filters`]: crate::traits::ParserTrait::filters
590    #[inline]
591    #[must_use]
592    fn is_string_with_code<'a>(
593        node: &Node<'a>,
594        _code: &[u8],
595        _ancestors: Ancestors<'a, '_>,
596    ) -> bool {
597        Self::is_string(node)
598    }
599    /// Whether `node` is the `if` that continues an `else if` chain,
600    /// rather than a freshly nested branch.
601    ///
602    /// `ancestors` is the chain the caller descended through; the
603    /// grammars that model `else if` as nesting need the parent (or the
604    /// preceding `else` token) to tell the two apart, and resolving
605    /// either from the node alone costs `O(depth)` (#1084). Languages
606    /// with a dedicated `elsif` clause node ignore it.
607    #[inline]
608    #[must_use]
609    fn is_else_if(_: &Node, _: Ancestors<'_, '_>) -> bool {
610        false
611    }
612    /// Whether `node` is a primitive type name (`int`, `double`, …),
613    /// which Halstead keys by text so each distinct primitive counts once.
614    #[inline]
615    #[must_use]
616    fn is_primitive(_node: &Node) -> bool {
617        false
618    }
619
620    /// Whether `node` is, or contains, a tree-sitter `ERROR` node.
621    #[must_use]
622    fn is_error(node: &Node) -> bool {
623        node.has_error()
624    }
625
626    /// Answer [`SubtreeSkip::SKIP`] to elide this node and all its
627    /// descendants from every metric. Used by language modules to
628    /// filter test-only / generated / preprocessor-disabled subtrees.
629    ///
630    /// The default retains every node, preserving the pre-#182
631    /// behavior. Language overrides drive opt-in skips (currently:
632    /// `RustCode` filters `#[cfg(test)]` items, gated by the runtime
633    /// `MetricsOptions::exclude_tests` flag).
634    ///
635    /// `ancestors` is the chain the caller descended through. Rust's
636    /// override needs the parent to read the run of `#[…]` siblings
637    /// before an item, and resolving siblings from the node alone
638    /// costs `O(depth)` per step (#1100).
639    ///
640    /// A verdict that took a *sibling-run* to reach should say so with
641    /// [`SubtreeSkip::covering_through`], so the walker asks once per
642    /// run rather than once per member. See that method for what makes
643    /// such a claim sound.
644    #[inline]
645    #[must_use]
646    fn should_skip_subtree<'a>(
647        _node: &Node<'a>,
648        _code: &[u8],
649        _ancestors: Ancestors<'a, '_>,
650    ) -> SubtreeSkip {
651        SubtreeSkip::RETAIN
652    }
653
654    /// Source-aware variant of [`is_func_space`](Self::is_func_space). The default forwards
655    /// to the byte-less predicate so languages whose function-space
656    /// classification is encoded in distinct grammar productions (Java,
657    /// Rust, Python, …) need no override. Languages whose function
658    /// boundaries are macro-shaped — Elixir's `def` / `defp` /
659    /// `defmacro` / `defmacrop` / `defmodule` — override this to
660    /// disambiguate `Call` nodes by their target identifier text
661    /// (#275).
662    ///
663    /// `ancestors` is the chain the caller descended through, so a
664    /// language whose classification depends on an enclosing construct
665    /// (Elixir's `quote` template) can answer without paying
666    /// `Node::parent`'s `O(depth)` per step (#1084).
667    #[inline]
668    #[must_use]
669    fn is_func_space_with_code<'a>(
670        node: &Node<'a>,
671        _code: &[u8],
672        _ancestors: Ancestors<'a, '_>,
673    ) -> bool {
674        Self::is_func_space(node)
675    }
676
677    /// Source-aware variant of [`is_func`](Self::is_func). Same rationale as
678    /// [`is_func_space_with_code`](Self::is_func_space_with_code) (#275).
679    #[inline]
680    #[must_use]
681    fn is_func_with_code<'a>(node: &Node<'a>, _code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
682        Self::is_func(node, ancestors)
683    }
684
685    /// Combined predicate the walker uses to decide whether to promote
686    /// `node` to a new function-space frame. Default forwards to
687    /// `is_func_with_code || is_func_space_with_code` so existing
688    /// languages need no override — each kept the freedom to expose
689    /// `is_func` and `is_func_space` as disjoint sets (Rust includes
690    /// closures in `is_func_space`, Java keeps lambdas out, …) and
691    /// this method preserves that flexibility while letting Elixir
692    /// halve its per-`Call` source-text lookups: a single
693    /// `elixir_call_keyword` call answers both halves at once
694    /// (#310 follow-on perf).
695    #[inline]
696    #[must_use]
697    fn promotes_to_func_space_with_code<'a>(
698        node: &Node<'a>,
699        code: &[u8],
700        ancestors: Ancestors<'a, '_>,
701    ) -> bool {
702        Self::is_func_with_code(node, code, ancestors)
703            || Self::is_func_space_with_code(node, code, ancestors)
704    }
705
706    /// Whether the function space `node` opens belongs to something
707    /// other than the container that syntactically encloses it.
708    ///
709    /// The space tree is built by a single-pass stack walk, so a space
710    /// is a child of whichever space was open when the walk reached it
711    /// — syntactic containment, never membership. The two normally
712    /// coincide. C++ `friend` is the case where they do not: a friend
713    /// with an inline body is written inside the class braces but is a
714    /// free function the class merely grants access to, so `npm` does
715    /// not count it as a method while `wmc` weighted it anyway (#1301).
716    /// Objective-C has the same shape in a different spelling: a plain
717    /// C `function_definition` inside `@implementation` / `@interface`
718    /// / `@protocol` is a file-static helper, never a method (#1356).
719    ///
720    /// `wmc` is the sole consumer: WMC is defined as the sum over a
721    /// class's *methods*, so a `true` here keeps the space out of the
722    /// enclosing class's WMC while leaving it a `Function` space like
723    /// any other — its own metrics, its `nom` contribution, and the
724    /// file-level cyclomatic roll-up are all unaffected.
725    ///
726    /// A `true` for a function no container encloses is inert rather
727    /// than wrong, since `merge` credits a `Function` child only to a
728    /// `Class` or `Interface` parent. So a language whose grammar makes
729    /// the answer unconditional — Objective-C, where a method is always
730    /// a `method_definition` — may answer on node kind alone, which is
731    /// total where a parent-kind list is not.
732    ///
733    /// Deliberately one method rather than a byte-less / `_with_code`
734    /// pair: the pair forwards by default, so a call site reaching for
735    /// the wrong spelling reads as correct against every language
736    /// without an override (`.claude/rules/grammar-dispatch.md` §7).
737    /// One spelling has nothing to disagree with.
738    #[inline]
739    #[must_use]
740    fn is_non_member_function<'a>(
741        _node: &Node<'a>,
742        _code: &[u8],
743        _ancestors: Ancestors<'a, '_>,
744    ) -> bool {
745        false
746    }
747}
748
749mod bash;
750mod c;
751mod ccomment;
752mod cpp;
753mod csharp;
754mod elixir;
755mod go;
756mod groovy;
757mod irules;
758mod java;
759mod javascript;
760mod kotlin;
761mod lua;
762mod mozcpp;
763mod mozjs;
764mod objc;
765mod perl;
766mod php;
767mod preproc;
768mod python;
769pub mod ruby;
770mod rust;
771mod tcl;
772mod tsx;
773mod typescript;
774
775/// Returns the `class_body` child of a Java `object_creation_expression`
776/// when the node is an anonymous class (`new Runnable() { ... }`), or
777/// `None` for a plain constructor call (`new Foo()`). Shared by
778/// `JavaCode::is_func_space` and `JavaCode::get_space_kind` so both agree
779/// on exactly which `object_creation_expression` nodes open a Class space
780/// (#463); a lambda is a distinct `lambda_expression` node and never
781/// reaches this path.
782#[must_use]
783pub fn java_anonymous_class_body<'a>(node: &Node<'a>) -> Option<Node<'a>> {
784    node.first_child(|id| id == Java::ClassBody as u16)
785}
786
787/// Counts the `accessor_declaration` children (`get` / `set` / `init`) of a C#
788/// `property_declaration` / `indexer_declaration` by walking its
789/// `accessor_list`. Returns `0` for the expression-bodied form
790/// (`this[int i] => _d[i];` / `int W => _w;`), which has no accessor list.
791///
792/// Shared by `csharp_member_has_accessors` (here) and the npm reference
793/// `csharp_count_member`, which keeps its own `.max(1)` fallback so an
794/// accessor-less expression-bodied member still counts as one method (#464).
795#[must_use]
796pub fn csharp_accessor_count(node: &Node) -> usize {
797    node.children()
798        .filter(|c| c.kind_id() == Csharp::AccessorList as u16)
799        .flat_map(|list| list.children())
800        .filter(|c| c.kind_id() == Csharp::AccessorDeclaration as u16)
801        .count()
802}
803
804/// Returns `true` when a C# `indexer_declaration` / `property_declaration`
805/// carries bodied accessors — an `accessor_list` containing at least one
806/// `accessor_declaration` (`get` / `set` / `init`). Returns `false` for the
807/// expression-bodied form (`this[int i] => _d[i];` / `int W => _w;`), which
808/// has no accessor list and defines a single implicit getter.
809///
810/// Shared by `CsharpCode::is_func` / `is_func_space` and
811/// `CsharpCode::get_space_kind` so all three agree on when an indexer or
812/// property opens its own Function space versus deferring to its accessor
813/// children. This mirrors the npm reference (`csharp_count_member`): the
814/// member counts as its accessor count, falling back to 1 for the
815/// accessor-less expression-bodied form (#464 indexer, #472 property).
816#[must_use]
817pub fn csharp_member_has_accessors(node: &Node) -> bool {
818    csharp_accessor_count(node) > 0
819}
820
821/// Whether `node` is a bare keyword token the C# grammar *aliased* to
822/// `modifier`, rather than the `modifier` rule itself.
823///
824/// The rule is `choice('public', 'static', 'async', 'readonly', …)`, so
825/// a real `modifier` wraps one keyword leaf that the getter already
826/// classifies on its own. Two other rules alias a bare token onto the
827/// same named kind, and those nodes are **childless**:
828///
829/// - `_parameter_type_with_modifiers` — `this`, `scoped`, `ref`, `out`,
830///   `in`, `readonly` in parameter position, the six #1418 bills.
831/// - `_lambda_expression_init` and `anonymous_method_expression` —
832///   `static` and `async` on a lambda or `delegate { … }`. These reach
833///   [`CsharpCode::get_op_type_with_code`]'s fallthrough and stay
834///   unclassified, exactly as they were before #1418; billing them is a
835///   separate decision, since the *declaration* spelling of the same two
836///   keywords is billed through its leaf.
837///
838/// Child-presence is therefore what separates an alias from the rule,
839/// and it is the grammar's shape rather than an inference about a broken
840/// parse — contrast [`Checker::is_bare_param`], which refuses the same
841/// test precisely because there it would be a guess.
842///
843/// Shared by [`CsharpCode::get_op_type_with_code`], which decides the
844/// keyword's Halstead role from its text, and
845/// [`CsharpCode::is_primitive`], which routes the operator half through
846/// the lexeme-keyed map so the alias and the bare token are one
847/// operator (#1418).
848///
849/// [`CsharpCode::get_op_type_with_code`]: crate::Getter::get_op_type_with_code
850/// [`CsharpCode::is_primitive`]: Checker::is_primitive
851#[must_use]
852pub(crate) fn csharp_is_aliased_modifier(node: &Node) -> bool {
853    node.kind_id() == Csharp::Modifier as u16 && node.child_count() == 0
854}
855
856/// Whether `param` is C's `(void)` marker — the spelling for an empty
857/// parameter list — rather than a parameter.
858///
859/// Shared by C, C++, Mozcpp and Objective-C, which all inherit the
860/// idiom and all emit a real `parameter_declaration` for it.
861///
862/// The declarator check is what separates the marker from `void *p`,
863/// which is an ordinary parameter of type `void *`, and the byte
864/// comparison is what separates it from an unnamed parameter of any
865/// other type: `f(int)` and `f(void)` are the same shape and different
866/// arities.
867#[must_use]
868pub fn c_family_void_parameter(param: &Node, code: &[u8]) -> bool {
869    param.child_by_field_name("declarator").is_none()
870        && param
871            .child_by_field_name("type")
872            .and_then(|ty| code.get(ty.start_byte()..ty.end_byte()))
873            == Some(b"void".as_slice())
874}
875
876/// Strip the `#` / `#!` marker plus the `[...]` brackets from a
877/// Rust `AttributeItem` / `InnerAttributeItem` token's raw text,
878/// returning the inner body. Returns `None` if the input shape is
879/// unexpected — callers skip silently rather than feed the matcher
880/// the literal `#[...]` form.
881fn rust_attribute_body<'a>(text: &'a str, marker: &str) -> Option<&'a str> {
882    text.trim()
883        .strip_prefix(marker)
884        .and_then(|t| t.trim_start().strip_prefix('['))
885        .and_then(|t| t.trim().strip_suffix(']'))
886}
887
888/// Classifies one attribute node's body, given the `#` / `#!` marker
889/// its kind carries. Returns `false` for a node whose text is not
890/// valid UTF-8 or does not have the `marker[...]` shape.
891fn rust_attribute_node_marks_test(node: &Node, code: &[u8], marker: &str) -> bool {
892    node.utf8_text(code)
893        .and_then(|text| rust_attribute_body(text, marker))
894        .is_some_and(rust_attribute_marks_test)
895}
896
897fn rust_item_is_test_only<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
898    rust_outer_attr_marks_test(node, code, ancestors) || rust_inner_attr_marks_test(node, code)
899}
900
901/// The item kinds the `exclude_tests` prune removes, given a test-only
902/// verdict.
903///
904/// `RustCode::should_skip_subtree` and
905/// [`rust_attribute_run_marks_test_item`] both consult this, so an
906/// attribute run and the item it decorates can never disagree about
907/// whether that item is prunable: `#[cfg(test)] use foo;` keeps both
908/// rows, because a `use_declaration` is not on this list (#1431).
909fn rust_prunable_item(node: &Node) -> bool {
910    matches!(
911        node.kind_id().into(),
912        Rust::ModItem
913            | Rust::FunctionItem
914            | Rust::ImplItem
915            | Rust::TraitItem
916            | Rust::ConstItem
917            | Rust::StaticItem
918    )
919}
920
921/// Whether the outer-attribute run `node` belongs to decorates an item
922/// the prune removes — which makes `node`'s own row test code too.
923///
924/// The whole run is asked about the *item*, not about itself, so every
925/// row of a stacked `#[cfg(test)]\n#[allow(dead_code)]\nmod t` goes:
926/// the second attribute says nothing about tests, and
927/// [`rust_item_is_test_only`] reads the run from the item end either
928/// way (#1431).
929///
930/// That is also why the answer is reported as covering the rest of the
931/// run. Every attribute from `node` up to the item resolves the same
932/// item and therefore the same verdict, so a walker that re-asks per
933/// row pays the run twice over per row: once in
934/// [`rust_attributed_item`]'s lookahead and once in the backward
935/// reading [`rust_item_is_test_only`] takes. Both are `O(run)` under a
936/// parent inside [`forward_attribute_scan_budget`], and a run of
937/// `3 * depth` attributes is inside it by construction — the diagonal
938/// #1446 measured at 5.2 s / depth 2 000. One ask per run makes it one
939/// pass, not `run` passes.
940fn rust_attribute_run_verdict<'a>(
941    node: &Node<'a>,
942    code: &[u8],
943    ancestors: Ancestors<'a, '_>,
944) -> SubtreeSkip {
945    let Some(item) = rust_attributed_item(node, ancestors) else {
946        // Attributes all the way to the end of the parent: nothing is
947        // decorated, so nothing is pruned — and no later member of this
948        // run will find an item either, so say so once. Without a
949        // parent to bound the claim there is nothing to say, which
950        // costs only a re-ask.
951        return match ancestors.parent(node) {
952            Some(parent) => SubtreeSkip::RETAIN.covering_through(parent.end_byte()),
953            None => SubtreeSkip::RETAIN,
954        };
955    };
956    // `item` is `node`'s sibling, so the chain that describes one
957    // describes the other — parent and depth alike.
958    let skipped = rust_prunable_item(&item) && rust_item_is_test_only(&item, code, ancestors);
959    // Everything strictly between `node` and `item` is an
960    // `AttributeItem` — that is what `rust_attributed_item` searched
961    // for — so the reach holds for the run's remaining members. It
962    // holds for *their* descendants too, which the walk visits whenever
963    // the run is retained: an attribute's children are the `#`, `[`,
964    // `]` tokens and the `attribute` body, none of them a
965    // `rust_prunable_item` or an `AttributeItem`, so `RETAIN` is their
966    // answer as well.
967    SubtreeSkip::of(skipped).covering_through(item.start_byte())
968}
969
970/// The item an outer-attribute run decorates: the first sibling after
971/// `node` that is not itself an `AttributeItem`.
972///
973/// Dispatched on [`forward_attribute_scan_budget`] for the reason the
974/// backward reading of the same run is (#1100), with the directions
975/// swapped: a cursor pass over a narrow parent's children is `O(width)`
976/// and depth-free, while [`Node::next_sibling`] descends from the root
977/// per step. Reading forward unconditionally would be `O(width)` *per
978/// attribute*, which is quadratic on the flat `#[derive]`-per-item file
979/// `bindgen` emits — the exact shape `nom/wide-attributed-fn` prices.
980///
981/// A `parent` that is not `node`'s parent leaves the first pass with
982/// nothing to skip to and answers `None`, i.e. "nothing is pruned" —
983/// the same conservative reading `rust_attribute_run_under` takes, and
984/// a caller error either way (`Ancestors::checked`, #1122).
985///
986/// # What this costs
987///
988/// An attribute now pays a lookahead plus the run reading the item
989/// pays, so a file of nothing but attributed items resolves about 2.5x
990/// the siblings it did. Measured in release: a generated 20 000-item
991/// `#[inline] fn f() {}` file went 245 ms to 305 ms and a 3 000-deep
992/// nest 33 ms to 35 ms, while this repository's own 90 kloc of Rust did
993/// not move off 59 ms — real code is a few percent attributed, not all
994/// of it. Every scaling probe kept its exponent (`nom/wide-attributed-
995/// fn` 1.02 to 1.04, `nom/nested-attributed-fn` 1.00 to 1.02), which is
996/// the property that matters: the constant is the price of asking, and
997/// the class is what #1100 was about.
998///
999/// Reusing [`rust_item_is_test_only`] rather than fusing the two walks
1000/// into one pass is deliberate at that price. A fused scan would save
1001/// one of the three sibling resolutions and would be a third reading of
1002/// the same run, free to drift from the other two — the failure mode
1003/// `.claude/rules/grammar-dispatch.md` §7 is about. #1446 kept that
1004/// choice: what was quadratic there was asking `run` times, not the
1005/// number of passes each ask makes, and
1006/// [`rust_attribute_run_verdict`]'s reach removes the repetition
1007/// without adding a reading.
1008///
1009/// # Why the same budget as the backward reading
1010///
1011/// The dispatch here weighs the same two costs #1100 did, and #1446 —
1012/// which found the diagonal `width ~= 3 * depth` shape this branch is
1013/// slowest on — did not move it. `parent.child_count() <= 3 * depth`
1014/// picks the forward pass exactly when `width * 35 ns` beats
1015/// `depth * 120 ns`, and on that diagonal it is still the cheaper of
1016/// the two: the sibling walk would be `O(run)` *and* `O(depth)` per
1017/// step. The budget was never what made the shape quadratic — asking
1018/// once per attribute was.
1019fn rust_attributed_item<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> Option<Node<'a>> {
1020    match ancestors.parent(node) {
1021        Some(parent) if parent.child_count() <= forward_attribute_scan_budget(ancestors) => parent
1022            .children()
1023            .skip_while(|child| child.id() != node.id())
1024            .find(|child| child.kind_id() != Rust::AttributeItem),
1025        _ => {
1026            let mut sibling = node.next_sibling();
1027            while let Some(candidate) = sibling {
1028                if candidate.kind_id() != Rust::AttributeItem {
1029                    return Some(candidate);
1030                }
1031                sibling = candidate.next_sibling();
1032            }
1033            None
1034        }
1035    }
1036}
1037
1038/// Children a parent may have before reading its child list costs more
1039/// than resolving the sibling from `node` itself.
1040///
1041/// The two scans below grow on different axes and neither dominates.
1042/// [`Node::previous_sibling`] resolves the parent by descending from
1043/// the root, so its cost tracks the node's *depth*; a cursor pass over
1044/// the parent's children tracks the parent's *width*. Measured on this
1045/// workspace's grammar build, one cursor sibling step costs ~68 ns
1046/// against ~420 ns for a whole `previous_sibling` on a shallow node —
1047/// and that 420 ns did not move with the sibling index, so the
1048/// break-even sits near six children.
1049///
1050/// This is the budget at depth 1 only; deeper nodes get
1051/// [`FORWARD_ATTRIBUTE_SCAN_CHILDREN_PER_DEPTH`] more per level. It
1052/// still bounds the shallow-and-wide shape — a generated file of
1053/// thousands of top-level items stays on exactly the walk it had
1054/// before.
1055const MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN: usize = 6;
1056
1057/// Children the budget above gains per level of depth.
1058///
1059/// Dispatching on width alone (#1100) reads the break-even as a
1060/// constant, but only the forward scan's price is: a
1061/// [`Node::previous_sibling`] descends from the root, so every extra
1062/// level makes the backward walk dearer while a cursor step stays flat.
1063/// One child too many therefore returned the whole `O(depth²)` walk —
1064/// measured on nested `mod` bodies of `#[inline] fn`s, a 7-child body
1065/// nested 3_200 deep took 2.67 s against 0.045 s for the same shape one
1066/// child narrower.
1067///
1068/// Sized from a crossover sweep of a forward-only against a
1069/// backward-only build over that fixture family: the *marginal* cost of
1070/// one more level of depth measured ~120 ns against ~35 ns for a cursor
1071/// step, and the observed break-even tracked that ratio — near 6
1072/// children at depth 1, near 13 at depth 3, and past 161 at depth 100.
1073/// (Marginal end-to-end figures, so they run below the isolated ~68 ns
1074/// / ~420 ns above; it is the ratio the budget uses, not either
1075/// absolute.) Three per level rounds the ratio down, keeping the
1076/// backward walk for the shallow-but-wide shape it is genuinely
1077/// cheaper on.
1078const FORWARD_ATTRIBUTE_SCAN_CHILDREN_PER_DEPTH: usize = 3;
1079
1080/// The widest parent worth reading forward from, for a node at
1081/// `ancestors`' depth. A node reached without a chain has no depth to
1082/// scale by, so the shallow cut stands.
1083fn forward_attribute_scan_budget(ancestors: Ancestors<'_, '_>) -> usize {
1084    ancestors
1085        .depth()
1086        .map_or(0, |depth| {
1087            depth.saturating_mul(FORWARD_ATTRIBUTE_SCAN_CHILDREN_PER_DEPTH)
1088        })
1089        .max(MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN)
1090}
1091
1092// The tree-sitter Rust grammar exposes outer attributes (`#[...]`) as
1093// `AttributeItem` siblings *before* the decorated item, so what marks
1094// `node` test-only is the run of attributes immediately preceding it —
1095// a non-attribute sibling (another item, a comment) ends the run. This
1096// scan covers every item kind, including `mod_item`, so
1097// `#[cfg(test)] mod tests` (an outer attribute on the module) is caught
1098// here while `mod tests { #![cfg(test)] }` is caught by the inner scan.
1099//
1100// The two readings agree on every node — `rust_outer_attr_scans_agree`
1101// pins that — so this only picks the cheaper one.
1102fn rust_outer_attr_marks_test<'a>(
1103    node: &Node<'a>,
1104    code: &[u8],
1105    ancestors: Ancestors<'a, '_>,
1106) -> bool {
1107    match ancestors.parent(node) {
1108        Some(parent) if parent.child_count() <= forward_attribute_scan_budget(ancestors) => {
1109            rust_attribute_run_under(&parent, node, code)
1110        }
1111        _ => rust_attribute_run_before(node, code),
1112    }
1113}
1114
1115/// Reads the attribute run ending at `node` forward from `parent`,
1116/// which the walker already holds. `O(parent.child_count())` — two
1117/// cursor passes over the children when a preceding run exists, one
1118/// otherwise — and independent of how deep `node` sits.
1119///
1120/// Two passes and not one: classifying each attribute as the first pass
1121/// goes by would classify every attribute in the parent rather than
1122/// only the adjacent run, and classification is itself `O(len)` in the
1123/// attribute body (#1105). Folding them into a single-pass boolean
1124/// would reclassify a huge `#[cfg(all(all(…)))]` once per subsequent
1125/// sibling and put that quadratic straight back; the second
1126/// [`Node::children`] cursor is the cheaper half of that trade.
1127fn rust_attribute_run_under(parent: &Node, node: &Node, code: &[u8]) -> bool {
1128    let mut run_start = None;
1129    for child in parent.children() {
1130        if child.id() == node.id() {
1131            break;
1132        }
1133        if child.kind_id() == Rust::AttributeItem {
1134            if run_start.is_none() {
1135                run_start = Some(child);
1136            }
1137        } else {
1138            run_start = None;
1139        }
1140    }
1141    // No preceding attribute. A `parent` that is not `node`'s parent is
1142    // a caller error, not a shape answered for here: the first pass then
1143    // runs to the end of `parent`'s children, so the answer depends on
1144    // whether those end in an attribute run and is meaningless either
1145    // way. The walker constructs its chain through `Ancestors::checked`,
1146    // which catches a mismatch that shows in the spans in any debug build
1147    // and every mismatch under `make chain-audit` (#1122).
1148    let Some(run_start) = run_start else {
1149        return false;
1150    };
1151    parent
1152        .children()
1153        .skip_while(|child| child.id() != run_start.id())
1154        .take_while(|child| child.id() != node.id() && child.kind_id() == Rust::AttributeItem)
1155        .any(|attribute| rust_attribute_node_marks_test(&attribute, code, "#"))
1156}
1157
1158/// Reads the same run backward from `node`. Each step resolves the
1159/// parent by descending from the root, so this is `O(attributes x
1160/// depth)` — but it does not touch the siblings it does not need,
1161/// which is what makes it the cheaper reading under a wide parent.
1162fn rust_attribute_run_before(node: &Node, code: &[u8]) -> bool {
1163    let mut sibling = node.previous_sibling();
1164    while let Some(attribute) = sibling {
1165        if attribute.kind_id() != Rust::AttributeItem {
1166            break;
1167        }
1168        if rust_attribute_node_marks_test(&attribute, code, "#") {
1169            return true;
1170        }
1171        sibling = attribute.previous_sibling();
1172    }
1173    false
1174}
1175
1176// `mod_item` additionally accepts inner attributes (`#![cfg(test)]`).
1177// The grammar nests these inside the module's `declaration_list` body,
1178// not as direct `mod_item` children, so descend one level via the
1179// `body` field before scanning. Non-module items have no inner-attribute
1180// test form, so this returns `false` for them immediately.
1181fn rust_inner_attr_marks_test(node: &Node, code: &[u8]) -> bool {
1182    node.kind_id() == Rust::ModItem
1183        && node.child_by_field_name("body").is_some_and(|body| {
1184            body.children()
1185                .filter(|child| child.kind_id() == Rust::InnerAttributeItem)
1186                .any(|attribute| rust_attribute_node_marks_test(&attribute, code, "#!"))
1187        })
1188}
1189
1190#[cfg(test)]
1191#[allow(
1192    clippy::float_cmp,
1193    clippy::cast_precision_loss,
1194    clippy::cast_possible_truncation,
1195    clippy::cast_sign_loss,
1196    clippy::similar_names,
1197    clippy::doc_markdown,
1198    clippy::needless_raw_string_hashes,
1199    clippy::too_many_lines
1200)]
1201mod tests {
1202    use super::*;
1203    use crate::count::count;
1204    use crate::langs::{
1205        BashParser, JavascriptCode, JavascriptParser, MozjsParser, PhpParser, TsxParser,
1206        TypescriptParser,
1207    };
1208    use crate::test_support::{ast_has_kind_id, for_each_node_with_chain};
1209    use std::fmt::Write as _;
1210    use std::path::PathBuf;
1211
1212    #[cfg(feature = "bash")]
1213    fn parse(source: &str) -> BashParser {
1214        BashParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.sh"), None)
1215    }
1216
1217    #[cfg(feature = "bash")]
1218    fn count_strings(source: &str) -> usize {
1219        count(&parse(source), &["string".to_string()]).0
1220    }
1221
1222    // `count`'s filter parser accepts a numeric string as a `kind_id` match
1223    // (parser.rs `filters`), so `has_kind` reuses the same primitive.
1224    #[cfg(feature = "bash")]
1225    fn has_kind(source: &str, kind_id: u16) -> bool {
1226        count(&parse(source), &[kind_id.to_string()]).0 > 0
1227    }
1228
1229    #[cfg(feature = "bash")]
1230    #[test]
1231    fn bash_is_string_excludes_word_tokens() {
1232        // `echo hello world` produces three Word nodes — none of them are
1233        // string literals. Regression for #44 (Word must not match
1234        // is_string).
1235        assert_eq!(count_strings("echo hello world\n"), 0);
1236        assert_eq!(
1237            count_strings("if [ -f file.txt ]; then cat file.txt; fi\n"),
1238            0
1239        );
1240    }
1241
1242    #[cfg(feature = "bash")]
1243    #[test]
1244    fn bash_is_string_matches_quoted_literals() {
1245        // Regular double-quoted string -> `string` (Bash::String).
1246        assert_eq!(count_strings("echo \"double\"\n"), 1);
1247        // Single-quoted string -> `raw_string` (Bash::RawString).
1248        assert_eq!(count_strings("echo 'single'\n"), 1);
1249        // ANSI-C quoting -> `ansi_c_string` (Bash::AnsiCString).
1250        assert_eq!(count_strings("echo $'ansi-c'\n"), 1);
1251    }
1252
1253    #[cfg(feature = "bash")]
1254    #[test]
1255    fn bash_is_string_matches_translated_string() {
1256        // tree-sitter-bash only emits a visible `translated_string` node
1257        // in assignment-style contexts; in command arguments the `$"..."`
1258        // tokenizes as `$` plus a regular `string`. Use an assignment so
1259        // the wrapper actually appears in the AST.
1260        let src = "x=$\"translated\"\n";
1261        assert!(
1262            has_kind(src, Bash::TranslatedString as u16),
1263            "expected a translated_string node in {src:?}"
1264        );
1265        // The wrapper plus its inner `string` child both match is_string,
1266        // so count is 2.
1267        assert_eq!(count_strings(src), 2);
1268    }
1269
1270    #[cfg(feature = "bash")]
1271    #[test]
1272    fn bash_is_string_matches_heredoc_bodies() {
1273        // Plain heredoc body.
1274        assert_eq!(
1275            count_strings("cat <<EOF\nhello world\nEOF\n"),
1276            1,
1277            "heredoc body should be counted as a string literal"
1278        );
1279        // Quoted-tag heredoc disables expansions but is still a string.
1280        assert_eq!(
1281            count_strings("cat <<'EOF'\nliteral $not_expanded\nEOF\n"),
1282            1
1283        );
1284        // Heredoc with an embedded expansion still yields exactly one
1285        // body node (parallel to a JS template string with `${x}`).
1286        assert_eq!(count_strings("cat <<EOF\nhi $name\nEOF\n"), 1);
1287    }
1288
1289    // ===== PHP `is_string` regression tests (issue #288) =====
1290
1291    #[cfg(feature = "php")]
1292    fn parse_php(source: &str) -> PhpParser {
1293        PhpParser::new(source.as_bytes().to_vec(), &PathBuf::from("test.php"), None)
1294    }
1295
1296    #[cfg(feature = "php")]
1297    fn count_php_strings(source: &str) -> usize {
1298        count(&parse_php(source), &["string".to_string()]).0
1299    }
1300
1301    #[cfg(feature = "php")]
1302    #[test]
1303    fn php_is_string_matches_single_quoted_literal() {
1304        // `Php::String` is the named single-quoted literal. Inert
1305        // single-quoted strings have always been matched; this anchors
1306        // the baseline before exercising the alias kinds.
1307        assert_eq!(count_php_strings("<?php $x = 'single';"), 1);
1308    }
1309
1310    #[cfg(feature = "php")]
1311    #[test]
1312    fn php_is_string_matches_encapsed_heredoc_nowdoc_shell() {
1313        // `EncapsedString` (double-quoted), `Heredoc`, `Nowdoc`, and
1314        // `ShellCommandExpression` (backticks) must all match
1315        // `is_string`. Pre-#288 the alterator/checker arms were almost
1316        // aligned for these named literals — this test locks in the
1317        // shape.
1318        assert_eq!(count_php_strings("<?php $x = \"double\";"), 1);
1319        assert_eq!(
1320            count_php_strings("<?php $x = <<<EOT\nbody\nEOT;\n"),
1321            1,
1322            "heredoc should match is_string"
1323        );
1324        assert_eq!(
1325            count_php_strings("<?php $x = <<<'EOT'\nbody\nEOT;\n"),
1326            1,
1327            "nowdoc should match is_string"
1328        );
1329        assert_eq!(
1330            count_php_strings("<?php $x = `ls`;"),
1331            1,
1332            "shell command (backtick) should match is_string"
1333        );
1334    }
1335
1336    #[cfg(feature = "php")]
1337    #[test]
1338    fn php_is_string_excludes_type_keyword_alias_1474() {
1339        // `Php::String2` (kind_id 25) is the `string` *type* keyword,
1340        // not a literal: `bca dump` finds it only as the sole child of
1341        // a `primitive_type` wrapper. #288 listed it in `is_string` for
1342        // kind-name identity and for parity with `get_op_type` and the
1343        // `Alterator`; #1293 moved it into `get_op_type`'s
1344        // `primitive_type`-suppression arm, so #1474 withdrew it here
1345        // too — the same reversal #1261 made for TS's `String2` and
1346        // TSX's `String3`. On the repo's PHP corpus this took
1347        // `find -t string` from 24 hits to 12 on `strings.php` and from
1348        // 27 to 8 on `classes.php`.
1349        //
1350        // The fixture carries every position the keyword appears in —
1351        // property type, plain parameter, `?string`, a `string|int`
1352        // union and the return type. Today's predicate is a `kind_id`
1353        // `matches!`, so it cannot tell those five apart; they are here
1354        // against a future ancestor-sensitive `is_string_with_code`
1355        // override of the kind Tcl already carries (#1381), which could
1356        // suppress one position and not another. The literals anchor
1357        // the other direction: without them this test would also pass
1358        // if `is_string` were emptied out entirely.
1359        let src = concat!(
1360            "<?php\n",
1361            "class C {\n",
1362            "    private string $prop = 'p';\n",
1363            "    public function f(string $a, ?string $b, string|int $c): string {\n",
1364            "        $d = \"y\";\n",
1365            "        $e = <<<EOT\n",
1366            "        body\n",
1367            "        EOT;\n",
1368            "        $g = <<<'EOT'\n",
1369            "        lit\n",
1370            "        EOT;\n",
1371            "        return (string) $a;\n",
1372            "    }\n",
1373            "}\n",
1374        );
1375        let parser = parse_php(src);
1376        // Pin the keyword spellings by *count*, not with a boolean
1377        // `ast_has_kind_id`. The zero below holds for one spelling
1378        // exactly as for five, so a fixture trimmed to a single
1379        // `string` keyword would keep every assertion here green while
1380        // the paragraph above claimed full coverage — measured, by
1381        // stripping four of the five. The count also subsumes the
1382        // "is the keyword in the parse at all" guard the boolean gave.
1383        let keyword_nodes = parser
1384            .root()
1385            .preorder()
1386            .filter(|n| n.kind_id() == Php::String2 as u16)
1387            .count();
1388        assert_eq!(
1389            keyword_nodes, 5,
1390            "fixture must keep all five `string` type-keyword positions: property, \
1391             parameter, `?string`, `string|int` union, return type",
1392        );
1393        assert_eq!(
1394            count_string_matches_for_kind(&parser, Php::String2 as u16, PhpCode::is_string),
1395            0,
1396            "Php::String2 (type keyword) must not match is_string",
1397        );
1398        // The `(string)` cast is the negative control: a childless
1399        // `cast_type`, a different kind_id, so the four below would be
1400        // five if anything ever classified a cast as a literal. Pinned
1401        // so the control cannot be trimmed out of the fixture silently.
1402        assert_eq!(
1403            parser
1404                .root()
1405                .preorder()
1406                .filter(|n| n.kind_id() == Php::CastType as u16)
1407                .count(),
1408            1,
1409            "fixture must keep the `(string)` cast negative control",
1410        );
1411        assert!(
1412            count_string_matches_for_kind(&parser, Php::String as u16, PhpCode::is_string) > 0,
1413            "the `'p'` literal must still match is_string",
1414        );
1415        // End-to-end through the filter chain `bca find` / `bca count`
1416        // actually use (`is_string_with_code`, `parser.rs`), not just
1417        // the predicate: `'p'`, `"y"`, the heredoc and the nowdoc — the
1418        // four literals, and none of the five keywords.
1419        assert_eq!(count_php_strings(src), 4);
1420    }
1421
1422    // ===== JS-family `is_string` regression tests (issue #283) =====
1423
1424    // For each language, count nodes whose kind_id is exactly `target`
1425    // *and* simultaneously match `is_string`. A non-zero result proves
1426    // both that the alias appears in the parse and that the checker
1427    // accepts it. Pre-fix this would be zero for the alias kinds.
1428    #[cfg(any(
1429        feature = "bash",
1430        feature = "c",
1431        feature = "c-family-helpers",
1432        feature = "cpp",
1433        feature = "csharp",
1434        feature = "elixir",
1435        feature = "go",
1436        feature = "groovy",
1437        feature = "irules",
1438        feature = "java",
1439        feature = "javascript",
1440        feature = "kotlin",
1441        feature = "lua",
1442        feature = "mozcpp",
1443        feature = "mozjs",
1444        feature = "objc",
1445        feature = "perl",
1446        feature = "php",
1447        feature = "python",
1448        feature = "ruby",
1449        feature = "rust",
1450        feature = "tcl",
1451        feature = "typescript",
1452    ))]
1453    fn count_string_matches_for_kind<P: ParserTrait, F: Fn(&Node) -> bool>(
1454        parser: &P,
1455        target: u16,
1456        is_string: F,
1457    ) -> usize {
1458        let mut stack = vec![parser.root()];
1459        let mut hits = 0;
1460        while let Some(node) = stack.pop() {
1461            if node.kind_id() == target && is_string(&node) {
1462                hits += 1;
1463            }
1464            for i in (0..node.child_count()).rev() {
1465                if let Some(c) = node.child(i) {
1466                    stack.push(c);
1467                }
1468            }
1469        }
1470        hits
1471    }
1472
1473    #[cfg(feature = "javascript")]
1474    #[test]
1475    fn javascript_is_string_matches_string2_alias() {
1476        // `Javascript::String2` (kind_id 221) aliases to `"string"`
1477        // (see `language_javascript.rs`). The alterator already
1478        // flattens it (#119); the generic `string` filter must agree
1479        // (#283). Use a source mix that exercises both the primary
1480        // `String` and the anonymous `String2` productions.
1481        let src = "const a = 'single';\nconst b = \"double\";\nimport \"m\";\n";
1482        let parser = JavascriptParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.js"), None);
1483        // First confirm String2 actually surfaces in this parse —
1484        // otherwise the assertion below would be vacuously true.
1485        assert!(
1486            ast_has_kind_id(&parser, Javascript::String2 as u16),
1487            "expected Javascript::String2 to appear in the parse",
1488        );
1489        // Then assert every String2 node matches is_string.
1490        assert!(
1491            count_string_matches_for_kind(
1492                &parser,
1493                Javascript::String2 as u16,
1494                JavascriptCode::is_string,
1495            ) > 0,
1496            "Javascript::String2 nodes must match is_string",
1497        );
1498    }
1499
1500    #[cfg(feature = "mozjs")]
1501    #[test]
1502    fn mozjs_is_string_matches_string2_alias() {
1503        // Parallel coverage for the MozJS dialect; same `String2`
1504        // alias as upstream JavaScript (kind_id 220 here).
1505        let src = "const a = 'single';\nconst b = \"double\";\nimport \"m\";\n";
1506        let parser = MozjsParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.js"), None);
1507        assert!(
1508            ast_has_kind_id(&parser, Mozjs::String2 as u16),
1509            "expected Mozjs::String2 to appear in the parse",
1510        );
1511        assert!(
1512            count_string_matches_for_kind(&parser, Mozjs::String2 as u16, MozjsCode::is_string) > 0,
1513            "Mozjs::String2 nodes must match is_string",
1514        );
1515    }
1516
1517    #[cfg(feature = "typescript")]
1518    #[test]
1519    fn typescript_is_string_excludes_type_keyword_alias_1261() {
1520        // TypeScript's only anonymous `"string"` alias, `String2`
1521        // (kind_id 135), is the `: string` type-annotation keyword —
1522        // not a literal. #313 made it match `is_string` for parity
1523        // with the operand classification; #1261 dropped it from both,
1524        // so `find string` / `count string` report literals only.
1525        // The `String` literal kinds must keep matching (the fixture's
1526        // `'x'` / `'y'`), otherwise this test would pass by
1527        // over-narrowing.
1528        let src = "const a: string = 'x';\nfunction f(): string { return 'y'; }\n";
1529        let parser = TypescriptParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.ts"), None);
1530        assert!(
1531            ast_has_kind_id(&parser, Typescript::String2 as u16),
1532            "expected Typescript::String2 to appear in the parse",
1533        );
1534        assert_eq!(
1535            count_string_matches_for_kind(
1536                &parser,
1537                Typescript::String2 as u16,
1538                TypescriptCode::is_string,
1539            ),
1540            0,
1541            "Typescript::String2 (type keyword) must not match is_string",
1542        );
1543        assert!(
1544            count_string_matches_for_kind(
1545                &parser,
1546                Typescript::String as u16,
1547                TypescriptCode::is_string,
1548            ) > 0,
1549            "Typescript::String literals must still match is_string",
1550        );
1551    }
1552
1553    #[cfg(feature = "typescript")]
1554    #[test]
1555    fn tsx_is_string_matches_literal_alias_not_type_keyword_1261() {
1556        // TSX uniquely carries two anonymous `"string"` aliases:
1557        // `String3` (kind_id 141, the type-annotation keyword) and
1558        // `String2` (kind_id 261, the string-literal alias). Both must
1559        // appear in this fixture: the `: string` annotation produces
1560        // `String3`, and the `'x'` / `"y"` / `"m"` / `"c"` literals
1561        // produce `String2`. Asserting presence of both *before*
1562        // checking `is_string` ensures a future grammar bump that
1563        // stops emitting either alias fails loudly here rather than
1564        // silently dropping coverage. The literal alias matches
1565        // (#283); the type keyword must not (#1261).
1566        let src = "const a: string = 'x';\n\
1567                   const b = \"y\";\n\
1568                   import \"m\";\n\
1569                   const el = <div className=\"c\">{\"t\"}</div>;\n";
1570        let parser = TsxParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.tsx"), None);
1571        assert!(
1572            ast_has_kind_id(&parser, Tsx::String3 as u16),
1573            "expected Tsx::String3 (type-keyword `string`) in the parse",
1574        );
1575        assert!(
1576            ast_has_kind_id(&parser, Tsx::String2 as u16),
1577            "expected Tsx::String2 (string-literal alias) in the parse",
1578        );
1579        assert_eq!(
1580            count_string_matches_for_kind(&parser, Tsx::String3 as u16, TsxCode::is_string),
1581            0,
1582            "Tsx::String3 (type keyword) must not match is_string",
1583        );
1584        assert!(
1585            count_string_matches_for_kind(&parser, Tsx::String2 as u16, TsxCode::is_string) > 0,
1586            "Tsx::String2 (string-literal alias) nodes must match is_string",
1587        );
1588    }
1589
1590    // Walk the AST and return the first node whose `kind_id` equals
1591    // `target`. Used by the `is_else_if` tests below to fish a
1592    // specific node out of the parse tree without depending on the
1593    // `count` helper above.
1594    #[cfg(any(
1595        feature = "c",
1596        feature = "cpp",
1597        feature = "csharp",
1598        feature = "groovy",
1599        feature = "mozcpp",
1600        feature = "python",
1601        feature = "rust",
1602    ))]
1603    fn find_first_kind<P: ParserTrait>(parser: &P, target: u16) -> Option<Node<'_>> {
1604        let mut stack = vec![parser.root()];
1605        while let Some(node) = stack.pop() {
1606            if node.kind_id() == target {
1607                return Some(node);
1608            }
1609            for i in (0..node.child_count()).rev() {
1610                if let Some(c) = node.child(i) {
1611                    stack.push(c);
1612                }
1613            }
1614        }
1615        None
1616    }
1617
1618    /// `#[cfg(test)] mod tests { … }` carries the test marker as an
1619    /// *outer* attribute sibling before the `mod_item`. The outer scan
1620    /// must run for `mod_item` nodes too, so this case is caught by
1621    /// `rust_outer_attr_marks_test` — not the inner scan. Pins the key
1622    /// invariant of the helper split: a `mod_item`'s outer attributes are
1623    /// never skipped in favour of only its inner attributes.
1624    #[cfg(feature = "rust")]
1625    #[test]
1626    fn rust_outer_attr_on_mod_is_test_only() {
1627        let src = "#[cfg(test)]\nmod tests {\n    fn t() {}\n}\n";
1628        let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.rs"), None);
1629        let code = parser.code();
1630        let node = find_first_kind(&parser, Rust::ModItem as u16).expect("mod_item");
1631
1632        assert!(rust_item_is_test_only(&node, code, Ancestors::unknown()));
1633        // The marker lives on the outer scan; the inner scan sees no
1634        // `#![cfg(test)]` and must report false.
1635        assert!(rust_outer_attr_marks_test(
1636            &node,
1637            code,
1638            Ancestors::unknown()
1639        ));
1640        assert!(!rust_inner_attr_marks_test(&node, code));
1641    }
1642
1643    /// `mod tests { #![cfg(test)] … }` carries the marker as an *inner*
1644    /// attribute nested in the module body. The outer sibling scan finds
1645    /// nothing; `rust_inner_attr_marks_test` descends via the `body`
1646    /// field and catches it.
1647    #[cfg(feature = "rust")]
1648    #[test]
1649    fn rust_inner_attr_in_mod_is_test_only() {
1650        let src = "mod tests {\n    #![cfg(test)]\n    fn t() {}\n}\n";
1651        let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.rs"), None);
1652        let code = parser.code();
1653        let node = find_first_kind(&parser, Rust::ModItem as u16).expect("mod_item");
1654
1655        assert!(rust_item_is_test_only(&node, code, Ancestors::unknown()));
1656        assert!(!rust_outer_attr_marks_test(
1657            &node,
1658            code,
1659            Ancestors::unknown()
1660        ));
1661        assert!(rust_inner_attr_marks_test(&node, code));
1662    }
1663
1664    /// A plain, unattributed item is not test-only — neither scan matches.
1665    #[cfg(feature = "rust")]
1666    #[test]
1667    fn rust_plain_item_is_not_test_only() {
1668        let src = "fn foo() {}\n";
1669        let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.rs"), None);
1670        let code = parser.code();
1671        let node = find_first_kind(&parser, Rust::FunctionItem as u16).expect("function_item");
1672
1673        assert!(!rust_item_is_test_only(&node, code, Ancestors::unknown()));
1674        assert!(!rust_outer_attr_marks_test(
1675            &node,
1676            code,
1677            Ancestors::unknown()
1678        ));
1679        assert!(!rust_inner_attr_marks_test(&node, code));
1680    }
1681
1682    /// Reading the `#[…]` run forward from the parent must answer
1683    /// exactly what walking backward from the item answers, on every
1684    /// node of every attribute shape.
1685    ///
1686    /// This is the acceptance criterion for #1100, not a nicety:
1687    /// `rust_outer_attr_marks_test` picks between the two by the
1688    /// parent's width against the node's depth, so a disagreement would
1689    /// make `exclude_tests` prune a different set of subtrees on one
1690    /// shape than on another and move published metric values —
1691    /// including as the budget is retuned. The comparison is against the
1692    /// backward reading directly rather than against the dispatcher,
1693    /// which would compare the backward walk with itself on every wide
1694    /// parent.
1695    ///
1696    /// The counted `marked` / `unmarked` totals are what stop it from
1697    /// passing vacuously — two readings that both answered `false`
1698    /// everywhere would agree perfectly.
1699    #[cfg(feature = "rust")]
1700    #[test]
1701    fn rust_outer_attr_scans_agree() {
1702        // One attribute run longer than the file's actual code, so the
1703        // run length cannot be confused with the child count.
1704        let long_run = format!("{}fn f() {{}}\n", "#[allow(dead_code)]\n".repeat(64));
1705        // A parent with hundreds of children: the shallow-but-wide
1706        // shape a per-step sibling scan would have been worst on.
1707        let mut wide = String::new();
1708        for i in 0..200 {
1709            let _ = writeln!(wide, "#[inline] fn f{i}() {{}}");
1710        }
1711        let sources = [
1712            // Attribute run opening the file, so the marked item is
1713            // its parent's first non-attribute child.
1714            "#[cfg(test)]\nfn a() {}\n",
1715            // Every recognised marker form, plus the `not(test)` form
1716            // that must *not* mark (#278).
1717            "#[test]\nfn a() {}\n#[tokio::test]\nfn b() {}\n\
1718             #[cfg(all(test, feature = \"x\"))]\nfn c() {}\n\
1719             #[cfg(not(test))]\nfn d() {}\n",
1720            // A run whose marker is not the attribute adjacent to the
1721            // item, the same run reversed, and one where an
1722            // intervening comment ends the run. Both orders matter:
1723            // only the second distinguishes a scan that stops at the
1724            // node it was asked about from one that reads the whole
1725            // run, since the query node here is itself an attribute.
1726            "#[cfg(test)]\n#[allow(dead_code)]\nfn a() {}\n\
1727             #[allow(dead_code)]\n#[cfg(test)]\nfn b() {}\n\
1728             #[cfg(test)]\n// break\nfn c() {}\n",
1729            // Nested modules, inner attributes, and an attributed item
1730            // inside a function body — parents other than the root.
1731            "mod outer {\n#![cfg(test)]\nmod inner {\n#[test]\nfn a() {}\n}\n}\n\
1732             fn b() {\n#[cfg(test)]\nfn c() {}\nlet x = 1;\n}\n",
1733            // A bare item: no parent for the root, no preceding
1734            // sibling for the item.
1735            "fn a() {}\n",
1736            // The dispatch boundary at depth 1, where the budget is
1737            // `MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN`: three attributed
1738            // items make a `source_file` exactly six children wide, and
1739            // a fourth bare item makes seven. Neither width occurred in
1740            // any fixture above, so `<=` against `<` went untested.
1741            "#[cfg(test)]\nfn a() {}\n#[inline]\nfn b() {}\n#[cfg(test)]\nfn c() {}\n",
1742            "#[cfg(test)]\nfn a() {}\n#[inline]\nfn b() {}\n#[cfg(test)]\nfn c() {}\nfn d() {}\n",
1743            // The same seven items one level down: two braces make the
1744            // `declaration_list` nine wide, which is exactly the budget
1745            // at depth 3, so the reading flips back to forward.
1746            "mod m {\n#[cfg(test)]\nfn a() {}\n#[inline]\nfn b() {}\n\
1747             #[cfg(test)]\nfn c() {}\nfn d() {}\n}\n",
1748            &long_run,
1749            &wide,
1750        ];
1751
1752        let (mut marked, mut unmarked) = (0_usize, 0_usize);
1753        let (mut read_forward, mut read_backward) = (0_usize, 0_usize);
1754        for source in sources {
1755            let code = source.as_bytes();
1756            let visited = for_each_node_with_chain::<RustCode>(code, |node, chain| {
1757                let Some(parent) = chain.last() else {
1758                    return; // The root has no parent to read forward from.
1759                };
1760                let backward = rust_attribute_run_before(node, code);
1761                assert_eq!(
1762                    rust_attribute_run_under(parent, node, code),
1763                    backward,
1764                    "outer-attribute readings disagree on {} at row {}",
1765                    node.kind(),
1766                    node.start_row(),
1767                );
1768                assert_eq!(
1769                    rust_outer_attr_marks_test(node, code, Ancestors::known(chain)),
1770                    backward,
1771                    "the width dispatch changed the answer on {} at row {}",
1772                    node.kind(),
1773                    node.start_row(),
1774                );
1775                if backward {
1776                    marked += 1;
1777                } else {
1778                    unmarked += 1;
1779                }
1780                if parent.child_count() > forward_attribute_scan_budget(Ancestors::known(chain)) {
1781                    read_backward += 1;
1782                } else {
1783                    read_forward += 1;
1784                }
1785            });
1786            assert!(visited > 0, "fixture must have nodes to compare");
1787        }
1788        assert!(
1789            marked > 0 && unmarked > 0,
1790            "the comparison saw only one answer ({marked} marked, {unmarked} unmarked), \
1791             so agreeing proves nothing"
1792        );
1793        // Both arms have to fire: the dispatcher assertion above
1794        // compares the backward walk with itself on every node that
1795        // took the backward arm, so without forward readings it proves
1796        // nothing, and without backward ones the `wide` shape is unmet.
1797        assert!(
1798            read_forward > 0 && read_backward > 0,
1799            "one dispatch arm went unexercised ({read_forward} forward, \
1800             {read_backward} backward)"
1801        );
1802    }
1803
1804    /// The budget is a width *per level of depth*, not a flat width.
1805    ///
1806    /// Pinned as a table because the shape of the formula is the whole
1807    /// point of #1100's follow-up: a flat cut returned the `O(depth²)`
1808    /// walk to any parent one child too wide, however deep it sat.
1809    #[cfg(feature = "rust")]
1810    #[test]
1811    fn the_forward_attribute_scan_budget_grows_with_depth() {
1812        assert_eq!(
1813            forward_attribute_scan_budget(Ancestors::unknown()),
1814            MAX_FORWARD_ATTRIBUTE_SCAN_CHILDREN,
1815            "no chain means no depth to scale by"
1816        );
1817
1818        // `depth` reads the chain's length and nothing else, so one
1819        // real node repeated stands in for any ancestry — which is what
1820        // lets the table reach depths a fixture could not.
1821        let mut checked = 0_usize;
1822        for_each_node_with_chain::<RustCode>(b"fn a() {}\n", |node, _| {
1823            checked += 1;
1824            // The shallow cut floors the scaled one, so it governs
1825            // until the depth term overtakes it at depth 3.
1826            for (depth, want) in [(0, 6), (1, 6), (2, 6), (3, 9), (10, 30), (1_000, 3_000)] {
1827                let chain = vec![*node; depth];
1828                assert_eq!(
1829                    forward_attribute_scan_budget(Ancestors::known(&chain)),
1830                    want,
1831                    "budget at depth {depth}"
1832                );
1833            }
1834        });
1835        assert!(
1836            checked > 0,
1837            "fixture must yield a node to build chains from"
1838        );
1839    }
1840
1841    /// The same equivalence one level up, on the hook the walker calls.
1842    ///
1843    /// `rust_item_is_test_only` folds the inner-attribute scan in, and
1844    /// `should_skip_subtree` adds the item-kind filter and the #1431
1845    /// attribute arm, so pinning it here is what carries the
1846    /// predicate-level agreement above through to the set of subtrees
1847    /// `exclude_tests` actually prunes.
1848    ///
1849    /// The oracle reads every run backward and every lookahead forward
1850    /// with the raw sibling accessors — no depth-against-width budget —
1851    /// so it disagrees with production the moment either dispatch arm
1852    /// answers differently from the other. The fixture spans both:
1853    /// `source_file` is wide enough at depth 1 to take the sibling
1854    /// arm, and `mod narrow`'s body is not.
1855    ///
1856    /// It also pins the [`SubtreeSkip`] reach, by replaying exactly what
1857    /// the walker does with it: carry the last covering verdict, and on
1858    /// every node it claims to answer for, check the claim against what
1859    /// the classifier says when asked directly. An over-broad reach —
1860    /// one byte past the item, say, or a run start taken as the run end
1861    /// — is a *silently wrong prune* in production, because the walker
1862    /// never asks again (#1446).
1863    #[cfg(feature = "rust")]
1864    #[test]
1865    fn rust_should_skip_subtree_matches_the_backward_reading() {
1866        let source = "#[cfg(test)]\nmod tests {\nfn a() {}\n}\n\
1867                      #[allow(dead_code)]\nstatic S: i32 = 1;\n\
1868                      mod inner {\n#![cfg(test)]\nconst C: i32 = 1;\n}\n\
1869                      #[rstest]\nfn b() {}\nfn c() {}\n\
1870                      #[cfg(test)]\nuse std::fmt;\n\
1871                      #[cfg(test)]\n#[allow(dead_code)]\nfn d() {}\n\
1872                      #[inline]\n#[allow(dead_code)]\n#[must_use]\n\
1873                      fn f() -> i32 {\n#[cfg(test)]\nfn inner() {}\n1\n}\n\
1874                      mod narrow {\n#[cfg(test)]\nfn e() {}\n}\n\
1875                      #[inline]\n#[allow(dead_code)]\n";
1876        let code = source.as_bytes();
1877        // Spelled out rather than reused from the production predicate:
1878        // the point is to pin which kinds the prune considers, so
1879        // borrowing `rust_prunable_item` would assert nothing.
1880        let is_item = |node: &Node| {
1881            matches!(
1882                node.kind_id().into(),
1883                Rust::ModItem
1884                    | Rust::FunctionItem
1885                    | Rust::ImplItem
1886                    | Rust::TraitItem
1887                    | Rust::ConstItem
1888                    | Rust::StaticItem
1889            )
1890        };
1891        let is_test_only = |node: &Node| {
1892            rust_attribute_run_before(node, code) || rust_inner_attr_marks_test(node, code)
1893        };
1894        let mut pruned = 0_usize;
1895        // The walker's own state, replayed: the last verdict that
1896        // claimed to answer for the nodes after it, and the end of the
1897        // deepest subtree pruned so far. The second is what makes the
1898        // replay a replay — the walker never pushes a skipped node's
1899        // children, so a reach owes them nothing and asserting over
1900        // them would fail on correct code.
1901        let mut reach = SubtreeSkip::RETAIN;
1902        let (mut pruned_through, mut reused) = (0_usize, 0_usize);
1903        let visited = for_each_node_with_chain::<RustCode>(code, |node, chain| {
1904            let reference = if node.kind_id() == Rust::AttributeItem {
1905                // Walk to the item this run decorates with the raw
1906                // forward accessor, independent of the budget dispatch.
1907                let mut following = node.next_sibling();
1908                while following.is_some_and(|s| s.kind_id() == Rust::AttributeItem) {
1909                    following = following.and_then(|s| s.next_sibling());
1910                }
1911                following.is_some_and(|item| is_item(&item) && is_test_only(&item))
1912            } else {
1913                is_item(node) && is_test_only(node)
1914            };
1915            let verdict = RustCode::should_skip_subtree(node, code, Ancestors::known(chain));
1916            let skipped = verdict.is_skipped();
1917            assert_eq!(
1918                skipped,
1919                reference,
1920                "prune decision moved on {} at row {}",
1921                node.kind(),
1922                node.start_row(),
1923            );
1924            if node.start_byte() >= pruned_through {
1925                if reach.answers_for(node.start_byte()) {
1926                    reused += 1;
1927                    assert_eq!(
1928                        reach.is_skipped(),
1929                        reference,
1930                        "a reach answered {} for {} at row {}",
1931                        reach.is_skipped(),
1932                        node.kind(),
1933                        node.start_row(),
1934                    );
1935                } else {
1936                    reach = verdict;
1937                }
1938                if skipped {
1939                    pruned_through = pruned_through.max(node.end_byte());
1940                }
1941            }
1942            pruned += usize::from(skipped);
1943        });
1944        assert!(visited > 0, "fixture must have nodes to compare");
1945        // Both halves of the reach have to fire, or the assertion above
1946        // is decoration: a run whose members are pruned (`fn d`'s two
1947        // rows) and one whose members are kept (`fn f`'s three, plus
1948        // the trailing pair that decorates nothing at all), the latter
1949        // also carrying each attribute's `#` / `[` / `]` / `attribute`
1950        // children through the same reach.
1951        //
1952        // `fn f`'s body is what makes an over-broad reach fail rather
1953        // than agree by luck. A reach is *always* right about the item
1954        // it stops at — the run's verdict is that item's verdict, by
1955        // construction — so a reach extended one node too far reads as
1956        // correct. Extended over the item's *body*, it swallows the
1957        // nested `#[cfg(test)] fn inner`, and the two answers part
1958        // company (verified by perturbation: `item.start_byte()` to
1959        // `item.end_byte()` fails this assertion, and without `fn f`'s
1960        // body it fails nothing).
1961        assert!(
1962            reused > 4,
1963            "only {reused} nodes were answered by a reach; the fixture \
1964             must carry runs long enough for the walker to reuse one"
1965        );
1966        // Seven test-only items — `mod tests`, `mod inner`, `fn b`,
1967        // `fn d`, `fn inner`, `fn e`, and `fn a` inside `mod tests` —
1968        // plus the six `#[…]` rows that mark five of them (`fn d`
1969        // carries two, `mod inner` an inner attribute that its own
1970        // subtree covers). Neither `#[allow(dead_code)]` on the
1971        // production `static`, nor `#[cfg(test)]` on the `use`, nor the
1972        // trailing run that decorates nothing, is the prune's to take.
1973        // A hook that pruned everything would satisfy the equality
1974        // above only if the oracle agreed; this pins the count the
1975        // fixture was written for.
1976        assert_eq!(
1977            pruned, 12,
1978            "expected the test-only items and the attributes marking them"
1979        );
1980    }
1981
1982    /// A run that decorates nothing still answers for the rest of
1983    /// itself.
1984    ///
1985    /// `#[inline]` with no item after it parses cleanly — no ERROR node,
1986    /// just a `source_file` whose last children are attributes — so the
1987    /// lookahead runs off the end and there is no item to take the reach
1988    /// from. Answering per row there is *correct*, which is exactly why
1989    /// [`rust_should_skip_subtree_matches_the_backward_reading`] cannot
1990    /// see it: dropping this reach costs only time, and a trailing run
1991    /// is unbounded in length like any other.
1992    #[cfg(feature = "rust")]
1993    #[test]
1994    fn a_trailing_attribute_run_answers_for_its_whole_run() {
1995        let source = "fn a() {}\n#[inline]\n#[allow(dead_code)]\n";
1996        let code = source.as_bytes();
1997        let mut attributes = Vec::new();
1998        for_each_node_with_chain::<RustCode>(code, |node, chain| {
1999            if node.kind_id() == Rust::AttributeItem {
2000                attributes.push((
2001                    node.start_byte(),
2002                    RustCode::should_skip_subtree(node, code, Ancestors::known(chain)),
2003                ));
2004            }
2005        });
2006        let [(_, first), (second_start, second)] = attributes[..] else {
2007            panic!("fixture must hold exactly two attributes, got {attributes:?}");
2008        };
2009        assert!(
2010            !first.is_skipped() && !second.is_skipped(),
2011            "a run decorating nothing prunes nothing"
2012        );
2013        assert!(
2014            first.answers_for(second_start),
2015            "the first row's verdict must answer for the rest of the run"
2016        );
2017    }
2018
2019    #[cfg(feature = "groovy")]
2020    #[test]
2021    fn groovy_is_else_if_recognises_else_followed_by_if() {
2022        // Direct assertion that `GroovyCode::is_else_if` returns true
2023        // for an `if_statement` whose previous sibling is the `else`
2024        // token. Defends the sibling-token strategy against accidental
2025        // regression to a `false` stub (lesson 10, #115 / #239).
2026        let src = "if (x) { } else if (y) { } else { }";
2027        let parser =
2028            GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.groovy"), None);
2029        let outer =
2030            find_first_kind(&parser, Groovy::IfStatement as u16).expect("outer if_statement");
2031        // Locate the inner if_statement (in the `alternative` slot of
2032        // the outer if, after the `else` token).
2033        let mut inner: Option<Node> = None;
2034        for i in 0..outer.child_count() {
2035            if let Some(c) = outer.child(i)
2036                && c.kind_id() == Groovy::IfStatement as u16
2037            {
2038                inner = Some(c);
2039                break;
2040            }
2041        }
2042        let inner = inner.expect("expected an inner if_statement");
2043        assert!(
2044            GroovyCode::is_else_if(&inner, Ancestors::unknown()),
2045            "inner if_statement after `else` must be recognised as else-if"
2046        );
2047        assert!(
2048            !GroovyCode::is_else_if(&outer, Ancestors::unknown()),
2049            "outer if_statement must not be recognised as else-if"
2050        );
2051    }
2052
2053    #[cfg(feature = "groovy")]
2054    #[test]
2055    fn groovy_is_else_if_false_for_standalone_if() {
2056        // A bare `if` (no `else` preceding it) must NOT register as
2057        // an else-if.
2058        let src = "if (x) { println(x) }";
2059        let parser =
2060            GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.groovy"), None);
2061        let node = find_first_kind(&parser, Groovy::IfStatement as u16).expect("if_statement");
2062        assert!(!GroovyCode::is_else_if(&node, Ancestors::unknown()));
2063    }
2064
2065    #[cfg(feature = "groovy")]
2066    #[test]
2067    fn groovy_is_call_excludes_constructors() {
2068        // Regression for #430. `GroovyCode::is_call` previously matched
2069        // `ObjectCreationExpression`, so `new Foo()` was counted as a
2070        // call in Groovy but not in Java/C# (whose `is_call` is method
2071        // invocation only). The Java-family convention is that `is_call`
2072        // (the `--ops`/`call` filter) counts method/function call sites
2073        // only; constructors are an ABC concern. This test mixes all
2074        // three Groovy call-shaped forms and pins the count at 2:
2075        //   * `new Foo()`        -> object_creation_expression (NOT a call)
2076        //   * `a.bar()`          -> method_invocation           (a call)
2077        //   * `println "hi"`     -> command_chain               (a call)
2078        // Pre-fix this count was 3 (the constructor was miscounted).
2079        let src = "def m() {\n  def a = new Foo()\n  a.bar()\n  println \"hi\"\n}\n";
2080        let parser =
2081            GroovyParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.groovy"), None);
2082        assert_eq!(
2083            count(&parser, &["call".to_string()]).0,
2084            2,
2085            "is_call must count method_invocation + command_chain only, not the constructor"
2086        );
2087
2088        // Direct predicate assertions: the constructor node must be
2089        // rejected while both genuine call forms are accepted.
2090        let ctor = find_first_kind(&parser, Groovy::ObjectCreationExpression as u16)
2091            .expect("object_creation_expression");
2092        assert!(
2093            !GroovyCode::is_call(&ctor),
2094            "object_creation_expression must not be a call"
2095        );
2096        let method =
2097            find_first_kind(&parser, Groovy::MethodInvocation as u16).expect("method_invocation");
2098        assert!(
2099            GroovyCode::is_call(&method),
2100            "method_invocation must be a call"
2101        );
2102        let chain = find_first_kind(&parser, Groovy::CommandChain as u16).expect("command_chain");
2103        assert!(GroovyCode::is_call(&chain), "command_chain must be a call");
2104    }
2105
2106    #[cfg(feature = "csharp")]
2107    #[test]
2108    fn csharp_is_call_excludes_constructors() {
2109        // The C# half of the `is_call` contract documented on the
2110        // `Checker::is_call` trait method (#1456), mirroring the #430 test
2111        // above. C# spells four call-shaped constructs that ABC counts
2112        // as branches and this filter must not count as call sites:
2113        //
2114        //   * `new Foo()`                  -> object_creation_expression
2115        //   * `: this(a)`                  -> constructor_initializer
2116        //   * `class Sub(int x) : Base(x)` -> base_list > argument_list
2117        //   * `record R(int x) : Base(x)`  -> base_list >
2118        //                                     primary_constructor_base_type
2119        //
2120        // paired with one genuine call, `Helper(f)`. The fixture scores
2121        // `abc.branches` 5 and `call` 1, so widening `is_call` to any of
2122        // the four moves the count asserted here.
2123        let src = "class Sub(int x) : Base(x) {
2124                       public Sub(int a, int b) : this(a) { }
2125                       void M() { var f = new Foo(); Helper(f); }
2126                   }
2127                   record R(int x) : Base(x);";
2128        let parser = CsharpParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.cs"), None);
2129        assert_eq!(
2130            count(&parser, &["call".to_string()]).0,
2131            1,
2132            "is_call must count `Helper(f)` only, not the four constructions"
2133        );
2134
2135        let call = find_first_kind(&parser, Csharp::InvocationExpression3 as u16)
2136            .expect("invocation_expression");
2137        assert!(
2138            CsharpCode::is_call(&call),
2139            "an invocation_expression is a call"
2140        );
2141        // §2 drift markers for the other two members of
2142        // `csharp_invocation_expr_kinds!()`. All three render to
2143        // `"invocation_expression"`, and at the 0.23.5 pin every
2144        // spelling probed with `bca dump` — plain, `this.`-qualified,
2145        // `base.`-qualified, inside a query clause, a lambda body, an
2146        // attribute argument, an interpolation, a `when` guard and a
2147        // constant pattern — emits only the aliased id. The claim these
2148        // pin is therefore about *this* fixture; a grammar bump that
2149        // starts spelling an ordinary call with either id fails the
2150        // count above as well, since neither is `find_first_kind`'s
2151        // target here.
2152        for (id, name) in [
2153            (Csharp::InvocationExpression, "InvocationExpression"),
2154            (Csharp::InvocationExpression2, "InvocationExpression2"),
2155        ] {
2156            assert!(
2157                !ast_has_kind_id(&parser, id as u16),
2158                "{name} is unobserved at this grammar pin; its arm is defensive"
2159            );
2160        }
2161
2162        for (id, what) in [
2163            (Csharp::ObjectCreationExpression as u16, "`new Foo()`"),
2164            (Csharp::ConstructorInitializer as u16, "`: this(a)`"),
2165            (
2166                Csharp::PrimaryConstructorBaseType as u16,
2167                "a record's `: Base(x)`",
2168            ),
2169        ] {
2170            let node = find_first_kind(&parser, id)
2171                .unwrap_or_else(|| panic!("fixture must contain {what}"));
2172            assert!(
2173                !CsharpCode::is_call(&node),
2174                "{what} is an ABC branch, not a call site"
2175            );
2176        }
2177
2178        // The `class` spelling of the primary-constructor base call is a
2179        // bare `argument_list` under the `base_list`, and preorder reaches
2180        // `Sub`'s before the four that belong to `: this(a)`, `new Foo()`,
2181        // `Helper(f)` and the record's `: Base(x)` — the parent assertion
2182        // is what keeps this pointing at the construction rather than at
2183        // one of those.
2184        let base_args =
2185            find_first_kind(&parser, Csharp::ArgumentList as u16).expect("argument_list");
2186        assert_eq!(
2187            base_args.parent().map(|p| p.kind_id()),
2188            Some(Csharp::BaseList2 as u16),
2189            "the first argument_list must be `class Sub(int x) : Base(x)`'s"
2190        );
2191        assert!(
2192            !CsharpCode::is_call(&base_args),
2193            "a class's primary-constructor base call is an ABC branch, not a call site"
2194        );
2195    }
2196
2197    // ===== C-family `is_call` regression tests (issue #1254) =====
2198
2199    // Every C-family grammar here references `preproc_call_expression`
2200    // only under an `alias(…, $.call_expression)` — zero bare references
2201    // in `grammar.json`, and no entry in `node-types.json` — so the
2202    // unsuffixed `CallExpression` never reaches `kind_id()` while the
2203    // aliased `CallExpression2` carries every real call.
2204    //
2205    // The two halves do different jobs. The first keeps the fixture an
2206    // honest witness: a grammar bump that stops emitting the alias must
2207    // fail here rather than silently weaken the counts below. The second
2208    // cannot fail against the current parse tables — `ts_symbol_map`
2209    // rewrites the unsuffixed symbol onto the aliased one for *every*
2210    // input — so it is a drift marker, not a live guard: it fires only if
2211    // a bump renumbers the enum or stops aliasing the rule, which is
2212    // exactly what justifies keeping the unsuffixed arm defensively
2213    // (the grammar-dispatch §2 technique, applied to an alias-mapped
2214    // rule rather than a hidden one).
2215    #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp"))]
2216    #[track_caller]
2217    fn assert_call_kinds_are_aliased<P: ParserTrait>(
2218        parser: &P,
2219        lang: &str,
2220        aliased: u16,
2221        unsuffixed: u16,
2222    ) {
2223        assert!(
2224            ast_has_kind_id(parser, aliased),
2225            "{lang}: fixture must emit the aliased call_expression kind"
2226        );
2227        assert!(
2228            !ast_has_kind_id(parser, unsuffixed),
2229            "{lang}: the unsuffixed CallExpression is the aliased-away \
2230             preproc_call_expression and must not surface"
2231        );
2232    }
2233
2234    // Six calls — statement position, initializer, `if` condition, both
2235    // binary operands, and the `return` operand. The other parenthesised
2236    // shapes are deliberate negatives: the `int f(int);` prototype's
2237    // `function_declarator`, the `sizeof(int)` operand, and the `(int)x`
2238    // cast. Six differs from the C++ fixture's five, so a test that
2239    // reaches for the wrong parser cannot pass on the right number.
2240    #[cfg(feature = "c")]
2241    const C_CALL_SHAPES: &str = "int f(int);\n\
2242                                 int main(void) {\n\
2243                                 f(1);\n\
2244                                 int x = g(2);\n\
2245                                 if (h(3)) { }\n\
2246                                 int y = a(4) + b(5);\n\
2247                                 int z = sizeof(int) + (int)x + y;\n\
2248                                 return c(z);\n\
2249                                 }\n";
2250
2251    // Five calls — member (`t.m()`), arrow member (`p->m()`), qualified
2252    // (`ns::f()`), and both function-pointer spellings (`fp(2)`,
2253    // `(*fp)(3)`). `T t(1)` and `new T(4)` are the negatives that matter:
2254    // object creation is an ABC concern, not a call site, matching Groovy /
2255    // Java / C# (#430). Functional-style `T(4)` is deliberately absent
2256    // from the fixture — the grammar emits it as a plain `call_expression`,
2257    // so it *is* counted, and including it here would blur what the five
2258    // is pinning.
2259    #[cfg(any(feature = "cpp", feature = "mozcpp"))]
2260    const CPP_CALL_SHAPES: &str = "struct T { T(int); int m(); };\n\
2261                                   namespace ns { int f(); }\n\
2262                                   int main() {\n\
2263                                   T t(1);\n\
2264                                   t.m();\n\
2265                                   T *p = &t;\n\
2266                                   p->m();\n\
2267                                   ns::f();\n\
2268                                   int (*fp)(int) = nullptr;\n\
2269                                   fp(2);\n\
2270                                   (*fp)(3);\n\
2271                                   T *q = new T(4);\n\
2272                                   delete q;\n\
2273                                   return sizeof(T);\n\
2274                                   }\n";
2275
2276    /// Every `call_expression` the pinned C grammar emits carries the
2277    /// *aliased* `CallExpression2` kind_id. The unsuffixed variant is the
2278    /// grammar's `preproc_call_expression`, referenced only under an
2279    /// `alias(…, $.call_expression)` — it is absent from `node-types.json`
2280    /// and never reaches `kind_id()`. Matching it alone left `is_call`
2281    /// dead: `bca count -t call` reported 0 on ordinary C (#1254).
2282    #[cfg(feature = "c")]
2283    #[test]
2284    fn c_is_call_matches_the_aliased_call_expression() {
2285        let parser = CParser::new(
2286            C_CALL_SHAPES.as_bytes().to_vec(),
2287            &PathBuf::from("test.c"),
2288            None,
2289        );
2290
2291        assert_call_kinds_are_aliased(
2292            &parser,
2293            "C",
2294            C::CallExpression2 as u16,
2295            C::CallExpression as u16,
2296        );
2297
2298        assert_eq!(
2299            count(&parser, &["call".to_string()]).0,
2300            6,
2301            "is_call must match every call_expression the C grammar emits"
2302        );
2303
2304        // Parenthesised non-call shapes stay out. The exact count above is
2305        // the sharper guard — these rows document the intended boundary
2306        // and catch a future edit that widens the match to them.
2307        for (kind, label) in [
2308            (C::SizeofExpression as u16, "sizeof_expression"),
2309            (C::CastExpression as u16, "cast_expression"),
2310            (C::FunctionDeclarator as u16, "function_declarator"),
2311        ] {
2312            let node = find_first_kind(&parser, kind).expect(label);
2313            assert!(!CCode::is_call(&node), "{label} must not be a call");
2314        }
2315    }
2316
2317    /// C++ counterpart of [`c_is_call_matches_the_aliased_call_expression`].
2318    /// Member, qualified and function-pointer calls all arrive as the same
2319    /// aliased `CallExpression2`; object construction does not (#1254).
2320    #[cfg(feature = "cpp")]
2321    #[test]
2322    fn cpp_is_call_matches_the_aliased_call_expression() {
2323        let parser = CppParser::new(
2324            CPP_CALL_SHAPES.as_bytes().to_vec(),
2325            &PathBuf::from("test.cpp"),
2326            None,
2327        );
2328
2329        assert_call_kinds_are_aliased(
2330            &parser,
2331            "Cpp",
2332            Cpp::CallExpression2 as u16,
2333            Cpp::CallExpression as u16,
2334        );
2335
2336        assert_eq!(
2337            count(&parser, &["call".to_string()]).0,
2338            5,
2339            "is_call must match member, qualified and function-pointer \
2340             calls, and only those"
2341        );
2342
2343        // `new T(4)` and `sizeof(T)` are call-shaped but not call sites.
2344        for (kind, label) in [
2345            (Cpp::NewExpression as u16, "new_expression"),
2346            (Cpp::SizeofExpression as u16, "sizeof_expression"),
2347            (Cpp::InitDeclarator as u16, "init_declarator"),
2348        ] {
2349            let node = find_first_kind(&parser, kind).expect(label);
2350            assert!(!CppCode::is_call(&node), "{label} must not be a call");
2351        }
2352    }
2353
2354    /// `Mozcpp` owns no file extension, so nothing routes to it and it
2355    /// gets no integration-snapshot coverage. Pin it against its
2356    /// extension-owning sibling instead: the Mozilla fork inherits
2357    /// upstream C++'s aliasing, so the same source must yield the same
2358    /// calls (grammar-dispatch, "sweep the rest").
2359    #[cfg(all(feature = "cpp", feature = "mozcpp"))]
2360    #[test]
2361    fn mozcpp_is_call_agrees_with_cpp() {
2362        let source = CPP_CALL_SHAPES.as_bytes().to_vec();
2363        let mozcpp = MozcppParser::new(source.clone(), &PathBuf::from("test.cpp"), None);
2364        let cpp = CppParser::new(source, &PathBuf::from("test.cpp"), None);
2365
2366        assert_call_kinds_are_aliased(
2367            &mozcpp,
2368            "Mozcpp",
2369            Mozcpp::CallExpression2 as u16,
2370            Mozcpp::CallExpression as u16,
2371        );
2372
2373        let mozcpp_calls = count(&mozcpp, &["call".to_string()]).0;
2374        assert_eq!(
2375            mozcpp_calls, 5,
2376            "Mozcpp must count the same five calls as C++"
2377        );
2378        assert_eq!(
2379            mozcpp_calls,
2380            count(&cpp, &["call".to_string()]).0,
2381            "Mozcpp is the Mozilla C++ fork; is_call must not diverge \
2382             from CppCode on shared source"
2383        );
2384
2385        let new_expr =
2386            find_first_kind(&mozcpp, Mozcpp::NewExpression as u16).expect("new_expression");
2387        assert!(
2388            !MozcppCode::is_call(&new_expr),
2389            "new_expression must not be a call"
2390        );
2391    }
2392
2393    #[cfg(feature = "python")]
2394    fn parse_python(src: &str) -> PythonParser {
2395        PythonParser::new(src.as_bytes().to_vec(), &PathBuf::from("test.py"), None)
2396    }
2397
2398    // Walk the AST and return every node whose `kind_id` equals `target`,
2399    // in DFS pre-order. Used by the Python `is_else_if` tests below to
2400    // distinguish the outer if from the inner one in an `else: if` chain.
2401    #[cfg(feature = "python")]
2402    fn find_all_kinds<P: ParserTrait>(parser: &P, target: u16) -> Vec<Node<'_>> {
2403        let mut out = Vec::new();
2404        let mut stack = vec![parser.root()];
2405        while let Some(node) = stack.pop() {
2406            if node.kind_id() == target {
2407                out.push(node);
2408            }
2409            for i in (0..node.child_count()).rev() {
2410                if let Some(c) = node.child(i) {
2411                    stack.push(c);
2412                }
2413            }
2414        }
2415        out
2416    }
2417
2418    #[cfg(feature = "python")]
2419    #[test]
2420    fn python_is_else_if_recognises_if_inside_else_clause() {
2421        // `else: if b:` chains parse as `else_clause → block → if_statement`
2422        // (the `block` wrapper is Python-specific). `is_else_if` must
2423        // walk through that wrapper. Regression for #276 (the stub
2424        // returned `false` unconditionally).
2425        let src = "if a:\n    pass\nelse:\n    if b:\n        pass\n";
2426        let parser = parse_python(src);
2427        let outer =
2428            find_first_kind(&parser, Python::IfStatement as u16).expect("outer if_statement");
2429        let inner =
2430            find_python_if_inside_else_block(&parser).expect("inner if_statement under else");
2431        assert!(
2432            PythonCode::is_else_if(&inner, Ancestors::unknown()),
2433            "if_statement inside else_clause's block must be recognised as else-if"
2434        );
2435        assert!(
2436            !PythonCode::is_else_if(&outer, Ancestors::unknown()),
2437            "outer if_statement must not be recognised as else-if"
2438        );
2439    }
2440
2441    #[cfg(feature = "python")]
2442    #[test]
2443    fn python_is_else_if_false_for_standalone_if() {
2444        // A bare `if` whose parent is the module / function body must
2445        // NOT register as an else-if.
2446        let src = "if a:\n    pass\n";
2447        let parser = parse_python(src);
2448        let node = find_first_kind(&parser, Python::IfStatement as u16).expect("if_statement");
2449        assert!(!PythonCode::is_else_if(&node, Ancestors::unknown()));
2450    }
2451
2452    #[cfg(feature = "python")]
2453    #[test]
2454    fn python_is_else_if_false_for_outer_if_with_elif_alternative() {
2455        // `elif` parses as an `ElifClause`, not an `IfStatement`, so the
2456        // only `IfStatement` in `if … elif …` is the outer one. Its
2457        // parent is the module / function body, not an `else_clause`.
2458        // Pins that the presence of `elif` in the AST does not trip
2459        // `is_else_if` for the outer `if`.
2460        let src = "if a:\n    pass\nelif b:\n    pass\n";
2461        let parser = parse_python(src);
2462        let outer = find_first_kind(&parser, Python::IfStatement as u16).expect("if_statement");
2463        assert!(!PythonCode::is_else_if(&outer, Ancestors::unknown()));
2464    }
2465
2466    // Return the inner `if_statement` that sits directly inside an
2467    // `else_clause`'s `block` wrapper, or `None` if no such node exists.
2468    // Used by tests below instead of relying on `find_all_kinds`'s DFS
2469    // pre-order to land at `ifs[1]`.
2470    #[cfg(feature = "python")]
2471    fn find_python_if_inside_else_block(parser: &PythonParser) -> Option<Node<'_>> {
2472        find_all_kinds(parser, Python::IfStatement as u16)
2473            .into_iter()
2474            .find(|n| {
2475                n.parent().is_some_and(|p| {
2476                    matches!(p.kind_id().into(), Python::Block | Python::Block2)
2477                        && p.parent()
2478                            .is_some_and(|gp| gp.kind_id() == Python::ElseClause)
2479                })
2480            })
2481    }
2482
2483    #[cfg(feature = "python")]
2484    #[test]
2485    fn python_is_else_if_false_when_else_body_has_siblings() {
2486        // `else: if b:` followed by another statement at the same indent
2487        // is a real nested-if, not a chain. The block has 2 named
2488        // children, so `is_else_if` must return false.
2489        let src = "if a:\n    pass\nelse:\n    if b:\n        pass\n    pass\n";
2490        let parser = parse_python(src);
2491        let inner =
2492            find_python_if_inside_else_block(&parser).expect("inner if_statement under else");
2493        assert!(
2494            !PythonCode::is_else_if(&inner, Ancestors::unknown()),
2495            "inner if must NOT be recognised as else-if when its block has siblings"
2496        );
2497    }
2498
2499    // Drift guard for #419: tree-sitter-python lists hidden aliases for
2500    // both `block` and `lambda` that are NOT emitted today —
2501    // `Block` (135, the hidden `_block` supertype; only `Block2` 160 is
2502    // emitted) and `Lambda2` (197, an unseen lambda alias; only
2503    // `Lambda` 196 and the `lambda` keyword token `Lambda3` 73 appear).
2504    // Several metric sites (`is_closure` — which now routes through
2505    // `cognitive::python_is_lambda`, the three cognitive lambda-scope
2506    // sites it feeds (#422), `is_else_if`, `python_is_block` in npa,
2507    // npm's class-body lookup, loc's no-op arm) already enumerate these
2508    // aliases defensively so a future grammar bump that promotes either
2509    // supertype to a concrete node cannot silently undercount.
2510    // This test pins their current absence across representative Python
2511    // (function, class, if/for bodies, lambda); if a bump ever emits one,
2512    // the guard flips red and forces a positive assertion to be added —
2513    // mirroring the `Php::String3` hidden-supertype guard above.
2514    #[cfg(feature = "python")]
2515    #[test]
2516    fn python_hidden_block_and_lambda_aliases_stay_unseen() {
2517        let src = "def f(a, b):\n    if a:\n        return b\n    for x in b:\n        print(x)\n\nclass C:\n    def m(self):\n        pass\n\ng = lambda x: x + 1\n";
2518        let parser = parse_python(src);
2519
2520        // Confirm the LIVE aliases actually surface, so this fixture is
2521        // a meaningful witness (a guard that never parses a block or a
2522        // lambda would pass vacuously).
2523        assert!(
2524            ast_has_kind_id(&parser, Python::Block2 as u16),
2525            "expected Python::Block2 (160, the emitted `block`) in the parse",
2526        );
2527        assert!(
2528            ast_has_kind_id(&parser, Python::Lambda as u16),
2529            "expected Python::Lambda (196, the emitted `lambda`) in the parse",
2530        );
2531
2532        // The hidden supertypes must NOT surface. If either now appears,
2533        // add a positive assertion routing it through the relevant
2534        // predicate (see #419).
2535        assert!(
2536            !ast_has_kind_id(&parser, Python::Block as u16),
2537            "Python::Block (135) is the hidden `_block` supertype; if it now appears, route it through python_is_block and assert positively (#419)",
2538        );
2539        assert!(
2540            !ast_has_kind_id(&parser, Python::Lambda2 as u16),
2541            "Python::Lambda2 (197) is an unseen lambda alias; if it now appears, cognitive::python_is_lambda (reused by is_closure and the three cognitive lambda-scope sites) must detect it and a positive closure assertion is required (#419/#422)",
2542        );
2543    }
2544
2545    // #422: the cognitive lambda-alias chokepoint and `is_closure` must
2546    // recognise the *same* live `lambda` node. Before #422, cognitive's
2547    // three lambda sites compared against `Lambda` (196) only while
2548    // `is_closure` accepted `Lambda | Lambda2`; both now route through
2549    // `cognitive::python_is_lambda`, so this pins that they agree on the
2550    // emitted `Lambda` and that the predicate is not vacuously true (it
2551    // rejects the enclosing `FunctionDefinition`). The unseen `Lambda2`
2552    // half of the set is covered by the drift guard above.
2553    #[cfg(feature = "python")]
2554    #[test]
2555    fn python_is_lambda_matches_live_lambda_and_agrees_with_is_closure() {
2556        use crate::lang_helpers::python::python_is_lambda;
2557
2558        let parser = parse_python("def f():\n    return lambda x: x and x\n");
2559        let lambda = find_first_kind(&parser, Python::Lambda as u16)
2560            .expect("the lambda expression must parse as Python::Lambda (196)");
2561
2562        assert!(
2563            python_is_lambda(&lambda),
2564            "python_is_lambda must accept the emitted Lambda node",
2565        );
2566        assert!(
2567            PythonCode::is_closure(&lambda, Ancestors::unknown()),
2568            "is_closure must agree with python_is_lambda on the same lambda node",
2569        );
2570
2571        // Not vacuously true: a non-lambda node (the enclosing function)
2572        // must be rejected, so the predicate is discriminating.
2573        let func = find_first_kind(&parser, Python::FunctionDefinition as u16)
2574            .expect("the def must parse as Python::FunctionDefinition");
2575        assert!(
2576            !python_is_lambda(&func),
2577            "python_is_lambda must reject a non-lambda node",
2578        );
2579    }
2580
2581    /// The two `is_else_if` implementations that answer a flat `false`
2582    /// are a behavioural claim, not a placeholder: the language has no
2583    /// `else if` *chain* to collapse, so every `if` it contains is a
2584    /// freshly nested branch and pays cognitive nesting.
2585    ///
2586    /// Neither is reachable from a metric walk — `Preproc` and
2587    /// `Ccomment` (which inherit the `Checker` default) have no `if`
2588    /// construct at all, and Elixir's branching is `cond do` or nested
2589    /// `if/else` — so nothing else in the suite executes them. #1084
2590    /// changed both signatures to take `Ancestors`, which is exactly the
2591    /// kind of edit that can quietly invert a one-line predicate, so pin
2592    /// the contract directly.
2593    #[cfg(all(feature = "c-family-helpers", feature = "elixir"))]
2594    #[test]
2595    fn languages_without_else_if_chains_answer_false() {
2596        // The `Checker` default, via the two grammars that do not
2597        // override it.
2598        let preproc = PreprocParser::new(b"#define A 1\n".to_vec(), &PathBuf::from("a.h"), None);
2599        let ccomment = CcommentParser::new(b"/* c */\n".to_vec(), &PathBuf::from("a.c"), None);
2600        assert!(
2601            !PreprocCode::is_else_if(&preproc.root(), Ancestors::unknown()),
2602            "the Checker default must answer false"
2603        );
2604        assert!(
2605            !CcommentCode::is_else_if(&ccomment.root(), Ancestors::unknown()),
2606            "the Checker default must answer false"
2607        );
2608
2609        // Elixir's explicit override, asserted on a real `if` node so the
2610        // test would catch an impl that started consulting the ancestor
2611        // chain and got the answer wrong.
2612        let parser = ElixirParser::new(
2613            b"defmodule M do\n  def f(a) do\n    if a do\n      if a do\n        :ok\n      end\n    end\n  end\nend\n".to_vec(),
2614            std::path::Path::new("m.ex"),
2615            None,
2616        );
2617        let ifs: Vec<_> = parser
2618            .root()
2619            .preorder()
2620            .filter(|n| {
2621                n.kind() == "call"
2622                    && n.utf8_text(parser.code())
2623                        .is_some_and(|t| t.starts_with("if"))
2624            })
2625            .collect();
2626        assert!(
2627            ifs.len() >= 2,
2628            "fixture must contain a nested if, else the assertion below is vacuous"
2629        );
2630        for node in &ifs {
2631            assert!(
2632                !ElixirCode::is_else_if(node, Ancestors::unknown()),
2633                "Elixir has no else-if chain; every if is a fresh branch"
2634            );
2635        }
2636    }
2637
2638    // Regression for #301: every language consolidated under
2639    // `impl_simple_is_string!` must still recognise its canonical
2640    // string literal via the `"string"` filter (which routes through
2641    // `Checker::is_string`). The positive test now drills down to
2642    // every individual variant of every multi-variant language so a
2643    // future macro invocation that drops a variant (e.g. forgetting
2644    // `Cpp::ConcatenatedString` after a grammar bump) fails loudly.
2645    //
2646    // JS-family languages (Mozjs/Javascript/Typescript/Tsx) keep
2647    // their dedicated `impl_js_family_is_string!` macro and have
2648    // their own alias-aware tests above; they are intentionally
2649    // not duplicated here.
2650    #[cfg(any(
2651        feature = "bash",
2652        feature = "c-family-helpers",
2653        feature = "cpp",
2654        feature = "csharp",
2655        feature = "elixir",
2656        feature = "go",
2657        feature = "groovy",
2658        feature = "irules",
2659        feature = "java",
2660        feature = "kotlin",
2661        feature = "lua",
2662        feature = "perl",
2663        feature = "php",
2664        feature = "python",
2665        feature = "ruby",
2666        feature = "rust",
2667        feature = "tcl",
2668    ))]
2669    fn count_with_parser<P: ParserTrait>(parser: &P) -> usize {
2670        count(parser, &["string".to_string()]).0
2671    }
2672
2673    // Assert that `target` kind_id appears in the parse and every
2674    // such node matches `is_string`. The two-step check makes test
2675    // failures unambiguous: a presence failure means the fixture no
2676    // longer produces the variant (likely grammar drift); a match
2677    // failure means a macro invocation dropped the variant.
2678    #[cfg(any(
2679        feature = "bash",
2680        feature = "c-family-helpers",
2681        feature = "cpp",
2682        feature = "csharp",
2683        feature = "elixir",
2684        feature = "go",
2685        feature = "groovy",
2686        feature = "irules",
2687        feature = "java",
2688        feature = "kotlin",
2689        feature = "lua",
2690        feature = "perl",
2691        feature = "php",
2692        feature = "python",
2693        feature = "ruby",
2694        feature = "rust",
2695        feature = "tcl",
2696    ))]
2697    fn assert_variant_is_string<P: ParserTrait, F: Fn(&Node) -> bool>(
2698        parser: &P,
2699        target: u16,
2700        is_string: F,
2701        lang: &str,
2702        variant: &str,
2703    ) {
2704        assert!(
2705            ast_has_kind_id(parser, target),
2706            "{lang}::{variant} (kind_id {target}) did not appear in the parse — fixture broken",
2707        );
2708        assert!(
2709            count_string_matches_for_kind(parser, target, is_string) > 0,
2710            "{lang}::{variant} must route through is_string",
2711        );
2712    }
2713
2714    // Collapses the 6-line-per-variant `assert_variant_is_string`
2715    // call into a single token per variant. `$lang` is the language
2716    // enum (e.g. `Cpp`); `$code` is the `Checker`-implementing type
2717    // (e.g. `CppCode`); the trailing list names the enum variants to
2718    // exercise. The macro feeds `stringify!` for both the language
2719    // and variant labels so test failures keep the same "Lang::Variant"
2720    // wording the helper already emits.
2721    #[cfg(all(
2722        feature = "bash",
2723        feature = "c-family-helpers",
2724        feature = "cpp",
2725        feature = "csharp",
2726        feature = "elixir",
2727        feature = "go",
2728        feature = "groovy",
2729        feature = "irules",
2730        feature = "java",
2731        feature = "kotlin",
2732        feature = "lua",
2733        feature = "perl",
2734        feature = "php",
2735        feature = "python",
2736        feature = "ruby",
2737        feature = "rust",
2738        feature = "tcl",
2739    ))]
2740    macro_rules! assert_variants_is_string {
2741        ($parser:expr, $lang:ident, $code:ident, [$($variant:ident),+ $(,)?]) => {
2742            $(
2743                assert_variant_is_string(
2744                    $parser,
2745                    $lang::$variant as u16,
2746                    $code::is_string,
2747                    stringify!($lang),
2748                    stringify!($variant),
2749                );
2750            )+
2751        };
2752    }
2753
2754    // Parse `$src` with `$parser_ty` and assert the generic `"string"`
2755    // filter yields zero matches. Used by the negative test, which
2756    // walks every language consolidated under `impl_simple_is_string!`
2757    // with identical per-language shape (parse → count → assert_eq! 0).
2758    #[cfg(all(
2759        feature = "bash",
2760        feature = "c-family-helpers",
2761        feature = "cpp",
2762        feature = "csharp",
2763        feature = "elixir",
2764        feature = "go",
2765        feature = "groovy",
2766        feature = "irules",
2767        feature = "java",
2768        feature = "kotlin",
2769        feature = "lua",
2770        feature = "perl",
2771        feature = "php",
2772        feature = "python",
2773        feature = "ruby",
2774        feature = "rust",
2775        feature = "tcl",
2776    ))]
2777    macro_rules! assert_no_string_matches {
2778        ($parser_ty:ident, $path:expr, $src:expr, $lang:literal $(,)?) => {{
2779            let parser = $parser_ty::new($src.to_vec(), $path, None);
2780            assert_eq!(count_with_parser(&parser), 0, $lang);
2781        }};
2782    }
2783
2784    #[cfg(all(
2785        feature = "bash",
2786        feature = "c-family-helpers",
2787        feature = "cpp",
2788        feature = "csharp",
2789        feature = "elixir",
2790        feature = "go",
2791        feature = "groovy",
2792        feature = "irules",
2793        feature = "java",
2794        feature = "kotlin",
2795        feature = "lua",
2796        feature = "perl",
2797        feature = "php",
2798        feature = "python",
2799        feature = "ruby",
2800        feature = "rust",
2801        feature = "tcl",
2802    ))]
2803    #[test]
2804    fn simple_is_string_macro_recognises_each_language() {
2805        use crate::langs::{
2806            CcommentParser, CppParser, CsharpParser, ElixirParser, GoParser, GroovyParser,
2807            IrulesParser, JavaParser, KotlinParser, LuaParser, PerlParser, PreprocParser,
2808            PythonParser, RubyParser, RustParser, TclParser,
2809        };
2810
2811        let path = PathBuf::from("test");
2812
2813        // ---- Preproc (2 variants): StringLiteral, RawStringLiteral ----
2814        let src = b"#include \"foo.h\"\nR\"(raw)\"\n".to_vec();
2815        let parser = PreprocParser::new(src, &path, None);
2816        assert_variants_is_string!(
2817            &parser,
2818            Preproc,
2819            PreprocCode,
2820            [StringLiteral, RawStringLiteral]
2821        );
2822
2823        // ---- Ccomment (2 variants): StringLiteral, RawStringLiteral ----
2824        // The Ccomment grammar is a stub that only emits Comment /
2825        // StringLiteral / RawStringLiteral; feed it both forms.
2826        let src = b"\"hello\"\nR\"(raw)\"\n".to_vec();
2827        let parser = CcommentParser::new(src, &path, None);
2828        assert_variants_is_string!(
2829            &parser,
2830            Ccomment,
2831            CcommentCode,
2832            [StringLiteral, RawStringLiteral]
2833        );
2834
2835        // ---- Cpp (3 variants): StringLiteral, ConcatenatedString, RawStringLiteral ----
2836        // C++ string concatenation (`"a" "b"`) produces a
2837        // `concatenated_string` node wrapping the literals.
2838        let src =
2839            b"const char* a = \"hi\";\nconst char* b = \"a\" \"b\";\nconst char* c = R\"(raw)\";\n"
2840                .to_vec();
2841        let parser = CppParser::new(src, &path, None);
2842        assert_variants_is_string!(
2843            &parser,
2844            Cpp,
2845            CppCode,
2846            [StringLiteral, ConcatenatedString, RawStringLiteral]
2847        );
2848
2849        // ---- Python (2 variants): String, ConcatenatedString ----
2850        // Python concatenates adjacent string literals into a
2851        // `concatenated_string` node.
2852        let src = b"a = \"hi\"\nb = \"a\" \"b\"\n".to_vec();
2853        let parser = PythonParser::new(src, &path, None);
2854        assert_variants_is_string!(&parser, Python, PythonCode, [String, ConcatenatedString]);
2855
2856        // ---- Java (2 variants): StringLiteral, MultilineStringLiteral ----
2857        // `Java::MultilineStringLiteral` maps to `_multiline_string_literal`
2858        // (leading-underscore hidden supertype) and does NOT surface as a
2859        // concrete kind_id in observed parses. Triple-quoted text blocks
2860        // instead produce regular `StringLiteral` nodes. The variant is
2861        // intentionally listed in the macro so a future grammar revision
2862        // that promotes it can't bypass `is_string`; presence is asserted
2863        // below to flag drift.
2864        let src = b"class C { String a = \"hi\"; String b = \"\"\"\nmulti\n\"\"\"; }\n".to_vec();
2865        let parser = JavaParser::new(src, &path, None);
2866        assert_variants_is_string!(&parser, Java, JavaCode, [StringLiteral]);
2867        assert!(
2868            !ast_has_kind_id(&parser, Java::MultilineStringLiteral as u16),
2869            "Java::MultilineStringLiteral is documented as the hidden _multiline_string_literal supertype; if it now appears in parses, replace this with a positive variant assertion",
2870        );
2871
2872        // ---- Csharp (4 variants): StringLiteral, VerbatimStringLiteral,
2873        // RawStringLiteral, InterpolatedStringExpression ----
2874        // Verbatim: `@"..."`; raw: triple-double-quote; interpolated: `$"..."`.
2875        let src = b"class C { string a = \"hi\"; string b = @\"verb\"; string c = \"\"\"raw\"\"\"; string d = $\"int{1}\"; }\n".to_vec();
2876        let parser = CsharpParser::new(src, &path, None);
2877        assert_variants_is_string!(
2878            &parser,
2879            Csharp,
2880            CsharpCode,
2881            [
2882                StringLiteral,
2883                VerbatimStringLiteral,
2884                RawStringLiteral,
2885                InterpolatedStringExpression,
2886            ]
2887        );
2888
2889        // ---- Rust (2 variants): StringLiteral, RawStringLiteral ----
2890        let src = b"fn main() { let a = \"hi\"; let b = r\"raw\"; }\n".to_vec();
2891        let parser = RustParser::new(src, &path, None);
2892        assert_variants_is_string!(&parser, Rust, RustCode, [StringLiteral, RawStringLiteral]);
2893
2894        // ---- Go (2 variants): InterpretedStringLiteral, RawStringLiteral ----
2895        // Backtick-delimited string is the raw form.
2896        let src = b"package main\nfunc main() { _ = \"hi\"; _ = `raw` }\n".to_vec();
2897        let parser = GoParser::new(src, &path, None);
2898        assert_variants_is_string!(
2899            &parser,
2900            Go,
2901            GoCode,
2902            [InterpretedStringLiteral, RawStringLiteral]
2903        );
2904
2905        // ---- Kotlin (2 variants): StringLiteral, MultilineStringLiteral ----
2906        let src = b"fun main() { val a = \"hi\"; val b = \"\"\"multi\"\"\" }\n".to_vec();
2907        let parser = KotlinParser::new(src, &path, None);
2908        assert_variants_is_string!(
2909            &parser,
2910            Kotlin,
2911            KotlinCode,
2912            [StringLiteral, MultilineStringLiteral]
2913        );
2914
2915        // ---- Lua (1 variant): String ----
2916        let src = b"local a = \"hi\"\nlocal b = [[long]]\n".to_vec();
2917        let parser = LuaParser::new(src, &path, None);
2918        assert_variants_is_string!(&parser, Lua, LuaCode, [String]);
2919
2920        // ---- Perl (7 variants): StringSingleQuoted, StringDoubleQuoted,
2921        // StringQQuoted, StringQqQuoted, BacktickQuoted, CommandQxQuoted,
2922        // HeredocBodyStatement ----
2923        let src = b"my $a = 'single';\nmy $b = \"double\";\nmy $c = q(qquoted);\nmy $d = qq(qqquoted);\nmy $e = `cmd`;\nmy $f = qx(qxcmd);\nmy $g = <<EOT;\nbody\nEOT\n".to_vec();
2924        let parser = PerlParser::new(src, &path, None);
2925        assert_variants_is_string!(
2926            &parser,
2927            Perl,
2928            PerlCode,
2929            [
2930                StringSingleQuoted,
2931                StringDoubleQuoted,
2932                StringQQuoted,
2933                StringQqQuoted,
2934                BacktickQuoted,
2935                CommandQxQuoted,
2936                HeredocBodyStatement,
2937            ]
2938        );
2939
2940        // ---- Bash (5 variants): String, RawString, AnsiCString,
2941        // TranslatedString, HeredocBody2 ----
2942        // TranslatedString surfaces as a wrapper node only in
2943        // assignment-style contexts (see `bash_is_string_matches_translated_string`).
2944        let src = b"a=\"d\"\nb='r'\nc=$'ansi'\nd=$\"t\"\ncat <<EOF\nbody\nEOF\n".to_vec();
2945        let parser = crate::langs::BashParser::new(src, &path, None);
2946        assert_variants_is_string!(
2947            &parser,
2948            Bash,
2949            BashCode,
2950            [
2951                String,
2952                RawString,
2953                AnsiCString,
2954                TranslatedString,
2955                HeredocBody2
2956            ]
2957        );
2958
2959        // ---- Tcl (3 variants): QuotedWord, BracedWord, BracedWordSimple ----
2960        // Tcl uses two `braced_word` rules: the named rule `braced_word`
2961        // (an argument-position braced expression that admits commands
2962        // and substitutions, e.g. `proc ... { body }`'s `body` arg) and
2963        // `braced_word_simple` (a plain inert braced word like
2964        // `{braced}` in `set v {braced}`). The fixture below mixes a
2965        // `proc ... {body}` (BracedWord), a simple `set` assignment
2966        // (BracedWordSimple), and a `set` to a quoted literal
2967        // (QuotedWord).
2968        let src = b"set a \"quoted\"\nset b {braced}\nproc p {x y} { return $x }\n".to_vec();
2969        let parser = TclParser::new(src, &path, None);
2970        assert_variants_is_string!(
2971            &parser,
2972            Tcl,
2973            TclCode,
2974            [QuotedWord, BracedWordSimple, BracedWord]
2975        );
2976
2977        // ---- iRules (3 variants): QuotedWord, BracedWord, BracedWordSimple ----
2978        // Same Tcl-family braced-word split as Tcl above: the `proc`
2979        // body is a named `braced_word` (BracedWord), the simple `set`
2980        // assignment is a `braced_word_simple` (BracedWordSimple), and
2981        // the quoted literal is a `QuotedWord`.
2982        let src = b"set a \"quoted\"\nset b {braced}\nproc p {x y} { return $x }\n".to_vec();
2983        let parser = IrulesParser::new(src, &path, None);
2984        assert_variants_is_string!(
2985            &parser,
2986            Irules,
2987            IrulesCode,
2988            [QuotedWord, BracedWordSimple, BracedWord]
2989        );
2990
2991        // ---- Php (6 variants): String, String3, EncapsedString,
2992        // Heredoc, Nowdoc, ShellCommandExpression ----
2993        // The `: string` return type in the fixture produces a
2994        // `String2` — the `string` *type* keyword, which is NOT a
2995        // variant here: #1474 withdrew it, and
2996        // `php_is_string_excludes_type_keyword_alias_1474` pins its
2997        // absence. String3 is the hidden `_string` supertype (kind_id
2998        // => "_string" — name starts with `_`), which tree-sitter does
2999        // NOT emit as a concrete node — see its empirical absence
3000        // asserted below.
3001        let src = b"<?php function f(): string { $a = 'single'; $b = \"double\"; $c = <<<EOT\nbody\nEOT;\n$d = <<<'EOT'\nnow\nEOT;\n$e = `ls`; return $a; }\n".to_vec();
3002        let parser = PhpParser::new(src, &path, None);
3003        assert_variants_is_string!(&parser, Php, PhpCode, [String]);
3004        // `Php::String3` is the hidden `_string` supertype — never
3005        // surfaces as a concrete kind_id in observed parses; the
3006        // checker still lists it so future grammar revisions that
3007        // promote it cannot silently bypass `is_string`. Verified
3008        // unreachable empirically (assertion below proves the
3009        // fixture does not produce it; the variant is intentionally
3010        // unverifiable through a positive test until then).
3011        assert!(
3012            !ast_has_kind_id(&parser, Php::String3 as u16),
3013            "Php::String3 is documented as the hidden _string supertype; if it now appears in parses, add a positive variant assertion",
3014        );
3015        assert_variants_is_string!(
3016            &parser,
3017            Php,
3018            PhpCode,
3019            [EncapsedString, Heredoc, Nowdoc, ShellCommandExpression]
3020        );
3021
3022        // ---- Elixir (3 variants): String, Charlist, Sigil ----
3023        // Charlists use single quotes; sigils use `~s(...)` etc.
3024        let src = b"a = \"hi\"\nb = 'charlist'\nc = ~s(sigil)\n".to_vec();
3025        let parser = ElixirParser::new(src, &path, None);
3026        assert_variants_is_string!(&parser, Elixir, ElixirCode, [String, Charlist, Sigil]);
3027
3028        // ---- Ruby (11 variants): String, ChainedString, BareString,
3029        // Subshell, Regex, HeredocBody, DelimitedSymbol, SimpleSymbol,
3030        // StringArray, SymbolArray, Character ----
3031        // ChainedString: two adjacent string literals (`"a" "b"`).
3032        // BareString: an unquoted element inside `%w[...]` / inside
3033        // a string array context. `%w[bare1 bare2]` emits a
3034        // StringArray whose children are BareString nodes — both
3035        // surface in the parse. Use a script that produces every
3036        // form so the per-variant assertion can hit each.
3037        let src = b"a = \"hi\"\nb = \"x\" \"y\"\nc = `cmd`\nd = /re/\ne = <<EOT\nbody\nEOT\nf = :sym\ng = :\"dsym\"\nh = %w[bare1 bare2]\ni = %i[s1 s2]\nj = ?A\n".to_vec();
3038        let parser = RubyParser::new(src, &path, None);
3039        assert_variants_is_string!(
3040            &parser,
3041            Ruby,
3042            RubyCode,
3043            [
3044                String,
3045                ChainedString,
3046                BareString,
3047                Subshell,
3048                Regex,
3049                HeredocBody,
3050                DelimitedSymbol,
3051                SimpleSymbol,
3052                StringArray,
3053                SymbolArray,
3054                Character,
3055            ]
3056        );
3057
3058        // ---- Groovy (1 variant): StringLiteral ----
3059        // The dekobon Groovy grammar consolidates every string shape
3060        // (single / double / triple-quoted, slashy `/.../`, dollar-
3061        // slashy `$/.../$`, GString-interpolated) under one
3062        // `string_literal` rule, so a single variant suffices —
3063        // unlike Java, character literals are not a separate node.
3064        let src =
3065            b"def m() { def a = \"hi\"; def b = \"\"\"multi\"\"\"; def c = /pat/ }\n".to_vec();
3066        let parser = GroovyParser::new(src, &path, None);
3067        assert_variants_is_string!(&parser, Groovy, GroovyCode, [StringLiteral]);
3068    }
3069
3070    #[cfg(all(
3071        feature = "bash",
3072        feature = "c-family-helpers",
3073        feature = "cpp",
3074        feature = "csharp",
3075        feature = "elixir",
3076        feature = "go",
3077        feature = "groovy",
3078        feature = "irules",
3079        feature = "java",
3080        feature = "kotlin",
3081        feature = "lua",
3082        feature = "perl",
3083        feature = "php",
3084        feature = "python",
3085        feature = "ruby",
3086        feature = "rust",
3087        feature = "tcl",
3088    ))]
3089    #[test]
3090    fn simple_is_string_macro_rejects_non_string_nodes() {
3091        // Pure-identifier source must produce zero string matches.
3092        // Catches a regression where a macro invocation accidentally
3093        // included a too-broad variant (e.g. `Identifier`). Covers
3094        // every language consolidated under `impl_simple_is_string!`.
3095        use crate::langs::{
3096            BashParser, CcommentParser, CppParser, CsharpParser, ElixirParser, GoParser,
3097            GroovyParser, IrulesParser, JavaParser, KotlinParser, LuaParser, PerlParser,
3098            PreprocParser, PythonParser, RubyParser, RustParser, TclParser,
3099        };
3100
3101        let path = PathBuf::from("test");
3102
3103        // Each fixture is the most minimal identifier-only input that
3104        // still parses for the target language. Per-language comments
3105        // below flag the few cases where the input choice is load-
3106        // bearing (Bash `s=$y` to avoid `string`-kind expansion nodes,
3107        // Tcl `set x $y` to keep the bareword as `Word`, etc.).
3108        assert_no_string_matches!(PreprocParser, &path, b"#define FOO 1\n", "Preproc");
3109        // Ccomment: input that lexes as a single line comment only.
3110        assert_no_string_matches!(CcommentParser, &path, b"// just a comment\n", "Ccomment");
3111        assert_no_string_matches!(CppParser, &path, b"int main() { return x; }\n", "Cpp");
3112        assert_no_string_matches!(PythonParser, &path, b"x = y\n", "Python");
3113        assert_no_string_matches!(JavaParser, &path, b"class C { int x = y; }\n", "Java");
3114        // Csharp: identifier-only field initializer.
3115        assert_no_string_matches!(CsharpParser, &path, b"class C { int x = y; }\n", "Csharp");
3116        assert_no_string_matches!(RustParser, &path, b"fn main() { let x = y; }\n", "Rust");
3117        assert_no_string_matches!(
3118            GoParser,
3119            &path,
3120            b"package main\nfunc main() { _ = x }\n",
3121            "Go"
3122        );
3123        assert_no_string_matches!(KotlinParser, &path, b"fun main() { val x = y }\n", "Kotlin");
3124        // Perl: identifier-only assignment with no quoted forms.
3125        assert_no_string_matches!(PerlParser, &path, b"my $x = $y;\n", "Perl");
3126        assert_no_string_matches!(LuaParser, &path, b"local x = y\n", "Lua");
3127        // Bash: assignment of one variable to another, no literals.
3128        // `s=$y` produces only Variable/SimpleExpansion nodes; no
3129        // string-kind node should appear.
3130        assert_no_string_matches!(BashParser, &path, b"s=$y\n", "Bash");
3131        // Tcl: `set` of an unquoted identifier word. The unquoted
3132        // bareword surfaces as `Word`, not any of the three string
3133        // kinds.
3134        assert_no_string_matches!(TclParser, &path, b"set x $y\n", "Tcl");
3135        // iRules: same Tcl-family `set` of an unquoted identifier word —
3136        // the bareword surfaces as `Word`, not any string kind.
3137        assert_no_string_matches!(IrulesParser, &path, b"set x $y\n", "Irules");
3138        // Php: identifier-only assignment.
3139        assert_no_string_matches!(PhpParser, &path, b"<?php $x = $y;\n", "Php");
3140        // Elixir: integer assignment, no string/charlist/sigil.
3141        assert_no_string_matches!(ElixirParser, &path, b"x = 1\n", "Elixir");
3142        assert_no_string_matches!(RubyParser, &path, b"x = y\n", "Ruby");
3143        // Groovy: identifier-only method body.
3144        assert_no_string_matches!(GroovyParser, &path, b"def m() { def x = y }\n", "Groovy");
3145    }
3146
3147    #[cfg(feature = "mozjs")]
3148    #[test]
3149    fn mozjs_parses_using_declaration() {
3150        // Drift marker for the JS-base-grammar bump 0.23.1 -> 0.25.0
3151        // (#407): the `using` / `await using` explicit-resource-
3152        // management declaration is a 0.25.0 grammar feature. The
3153        // bundled mozjs parser was previously stale at 0.23.1 while
3154        // its marker claimed 0.25.0 (the #400 baseline lie), so this
3155        // node could not appear. Asserting it surfaces pins the regen
3156        // against reversion — it fails against the pre-#407 parser.
3157        let src = "function f() {\n  using r = acquire();\n  return r;\n}\n";
3158        let parser = MozjsParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.js"), None);
3159        assert!(
3160            ast_has_kind_id(&parser, Mozjs::UsingDeclaration as u16),
3161            "expected Mozjs::UsingDeclaration to appear in the parse",
3162        );
3163    }
3164
3165    #[cfg(feature = "go")]
3166    #[test]
3167    fn go_rune_literal_is_not_a_string() {
3168        // #699 verdict: Go `RuneLiteral` is an operand in `get_op_type`
3169        // and is flattened by the alterator, but it is deliberately
3170        // excluded from `is_string` — a rune is a character (`int32`),
3171        // not a string, exactly as Rust/Cpp `CharLiteral` are operand +
3172        // flattened yet absent from their `is_string`. This pins the
3173        // exclusion: the fixture must produce a `rune_literal` (proving
3174        // the surrounding grammar still emits it) while `GoCode::is_string`
3175        // returns false for it. The flattening half lives in
3176        // `alterator.rs::go_rune_literal_flattened_but_not_a_string_kind`.
3177        use crate::langs::GoParser;
3178        let path = PathBuf::from("test.go");
3179        let src = b"package main\nfunc main() { r := 'x'; _ = r }\n".to_vec();
3180        let parser = GoParser::new(src, &path, None);
3181        let rune_id = Go::RuneLiteral as u16;
3182        assert!(
3183            ast_has_kind_id(&parser, rune_id),
3184            "fixture should produce a rune_literal node",
3185        );
3186        assert_eq!(
3187            count_string_matches_for_kind(&parser, rune_id, GoCode::is_string),
3188            0,
3189            "Go rune_literal must not match is_string (a rune is a char, not a string)",
3190        );
3191    }
3192
3193    #[cfg(all(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
3194    #[test]
3195    fn c_family_char_literal_is_not_a_string() {
3196        // The C-family half of the #699 verdict `go_rune_literal_is_not_a_string`
3197        // cites: a `char_literal` is flattened by the alterator and — since
3198        // #1316 — is a Halstead operand, yet it is deliberately absent from
3199        // `is_string`, because a character is not a string.
3200        //
3201        // #1316 made that citation true rather than aspirational (the operand
3202        // half had never been implemented), so the exclusion is now a live
3203        // decision and wants a pin of its own.
3204        //
3205        // Each row is two-sided. The `char_literal` count must be zero, and
3206        // the `string_literal` in the same fixture must be one — otherwise a
3207        // predicate that had stopped matching anything at all would read as a
3208        // correctly-rejected character. The `ast_has_kind_id` guard covers the
3209        // third way to get a zero: a fixture that no longer parses to a
3210        // `char_literal` at all.
3211        use crate::langs::{CParser, CppParser, MozcppParser, ObjcParser};
3212
3213        #[cfg(any(feature = "c", feature = "cpp", feature = "mozcpp", feature = "objc"))]
3214        fn check<P: ParserTrait, F: Fn(&Node) -> bool + Copy>(
3215            source: &[u8],
3216            file: &str,
3217            (char_literal, string_literal): (u16, u16),
3218            is_string: F,
3219            label: &str,
3220        ) {
3221            let parser = P::new(source.to_vec(), &PathBuf::from(file), None);
3222            assert!(
3223                ast_has_kind_id(&parser, char_literal),
3224                "{label}: fixture should produce a char_literal node",
3225            );
3226            assert_eq!(
3227                count_string_matches_for_kind(&parser, char_literal, is_string),
3228                0,
3229                "{label}: char_literal must not match is_string (a char is not a string)",
3230            );
3231            assert_eq!(
3232                count_string_matches_for_kind(&parser, string_literal, is_string),
3233                1,
3234                "{label}: is_string must still match the fixture's string_literal, or the \
3235                 zero above is a dead predicate rather than a rejected character",
3236            );
3237        }
3238
3239        let src = b"char a = 'x';\nconst char *b = \"s\";\n";
3240        check::<CParser, _>(
3241            src,
3242            "t.c",
3243            (C::CharLiteral as u16, C::StringLiteral as u16),
3244            CCode::is_string,
3245            "c",
3246        );
3247        check::<CppParser, _>(
3248            src,
3249            "t.cpp",
3250            (Cpp::CharLiteral as u16, Cpp::StringLiteral as u16),
3251            CppCode::is_string,
3252            "cpp",
3253        );
3254        check::<MozcppParser, _>(
3255            src,
3256            "t.cpp",
3257            (Mozcpp::CharLiteral as u16, Mozcpp::StringLiteral as u16),
3258            MozcppCode::is_string,
3259            "mozcpp",
3260        );
3261        check::<ObjcParser, _>(
3262            src,
3263            "t.m",
3264            (Objc::CharLiteral as u16, Objc::StringLiteral as u16),
3265            ObjcCode::is_string,
3266            "objc",
3267        );
3268    }
3269
3270    /// Asserts `is_func` / `is_closure` answer the same off a known
3271    /// chain as off a `Node::parent` climb, for every node of `code`.
3272    /// Returns the `(functions, closures)` the chain path counted, so a
3273    /// caller can additionally pin what the predicates actually decided.
3274    ///
3275    /// Both predicates fold their ancestor lookups into one boolean, so
3276    /// a wrong ancestor does not fail — it silently moves a function
3277    /// into the closure column. The counters keep a fixture honest: both
3278    /// answers have to occur, for both predicates, or the parity holds
3279    /// only over nodes the predicates never classify.
3280    #[cfg(any(
3281        feature = "javascript",
3282        feature = "mozjs",
3283        feature = "ruby",
3284        feature = "typescript"
3285    ))]
3286    fn assert_func_parity<L: crate::traits::LanguageInfo + Checker>(
3287        label: &str,
3288        code: &[u8],
3289    ) -> (usize, usize) {
3290        let mut funcs = 0;
3291        let mut closures = 0;
3292        let visited = for_each_node_with_chain::<L>(code, |node, chain| {
3293            let known = Ancestors::known(chain);
3294            let climbing = Ancestors::unknown();
3295            assert_eq!(
3296                L::is_func(node, known),
3297                L::is_func(node, climbing),
3298                "{label}: is_func disagrees on {} at row {}",
3299                node.kind(),
3300                node.start_row()
3301            );
3302            assert_eq!(
3303                L::is_closure(node, known),
3304                L::is_closure(node, climbing),
3305                "{label}: is_closure disagrees on {} at row {}",
3306                node.kind(),
3307                node.start_row()
3308            );
3309            funcs += usize::from(L::is_func(node, known));
3310            closures += usize::from(L::is_closure(node, known));
3311        });
3312        assert!(visited > 20, "{label}: fixture is too small to prove much");
3313        assert!(
3314            funcs > 0 && closures > 0,
3315            "{label}: fixture must contain both a named function and a closure, \
3316             else parity holds over nodes the walk never classifies: \
3317             {funcs} functions, {closures} closures"
3318        );
3319        (funcs, closures)
3320    }
3321
3322    /// The JS-family `is_func` / `is_closure` must classify a node the
3323    /// same whether they read the walker's ancestor chain or climb with
3324    /// `Node::parent` (#1088).
3325    ///
3326    /// These are the predicates with the longest ancestor lookup: an
3327    /// upward walk, its `is_else_if` filter, and a `has_sibling`
3328    /// adjacency check.
3329    #[cfg(all(feature = "javascript", feature = "mozjs", feature = "typescript"))]
3330    #[test]
3331    fn js_func_and_closure_agree_between_known_and_climbing() {
3332        // One of each shape the walk distinguishes: an arrow bound to a
3333        // name, an arrow passed positionally, a `function` expression
3334        // named by an object `pair`, and a nested arrow whose only
3335        // enclosing frame is a `statement_block`.
3336        //
3337        // The returned object literal is load-bearing. It is the one
3338        // shape here where the trailing `has_sibling` adjacency check
3339        // decides the answer: the upward walk stops at the `return`
3340        // having counted no binding, so whether the arrow is a *named*
3341        // function rests entirely on the `property_identifier` beside
3342        // it. Everywhere else a binding is found first and the adjacency
3343        // check is never reached, which would leave a wrong parent
3344        // lookup invisible.
3345        let code = concat!(
3346            "const f = a => { g(() => 1); a => 2; };\n",
3347            "const o = { m: function () { return 1; } };\n",
3348            "function h() { return { k: (a) => a + 1 }; }\n",
3349            "function i() { return function () { return 2; }; }\n",
3350        )
3351        .as_bytes();
3352        // `is_js_func_and_closure_checker!` expands separately against
3353        // each grammar's own `kind_id` enum, so the four impls are four
3354        // distinct variant lists rather than one shared body. Checking
3355        // only JavaScript would leave a drifted id in any of the other
3356        // three uncovered.
3357        assert_func_parity::<crate::langs::JavascriptCode>("javascript", code);
3358        assert_func_parity::<crate::langs::MozjsCode>("mozjs", code);
3359        assert_func_parity::<crate::langs::TypescriptCode>("typescript", code);
3360        assert_func_parity::<crate::langs::TsxCode>("tsx", code);
3361    }
3362
3363    /// Ruby's `is_closure` is the one non-JS predicate that needs an
3364    /// ancestor: a `{ … }` block is a closure unless its parent is the
3365    /// `Lambda` that already counted it (#465), so it reads the chain
3366    /// for the same reason the JS family does (#1088).
3367    ///
3368    /// The stabby lambda is the discriminating shape. Its body block is
3369    /// the only node here whose answer depends on the parent lookup —
3370    /// every other block's parent is a `Call`, which the arm accepts
3371    /// whether the lookup is right or wrong.
3372    #[cfg(feature = "ruby")]
3373    #[test]
3374    fn ruby_block_closure_agrees_between_known_and_climbing() {
3375        let code = concat!(
3376            "def m\n",
3377            "  f = ->(z) { z + 1 }\n",
3378            "  [1].each { |x| x }\n",
3379            "  lambda { 2 }\n",
3380            "end\n",
3381        )
3382        .as_bytes();
3383        let (funcs, closures) = assert_func_parity::<crate::langs::RubyCode>("ruby", code);
3384        assert_eq!(funcs, 1, "only `def m` is a named function");
3385        assert_eq!(
3386            closures, 3,
3387            "the stabby lambda, the `each` block and the `lambda` block \
3388             are three closures — the stabby lambda's own body block must \
3389             not add a fourth, which is the answer the parent lookup decides"
3390        );
3391    }
3392
3393    /// The verdicts the byte-less and the source-aware `is_string` give
3394    /// the `braced_word`-kind nodes of `code`, as their source texts,
3395    /// plus how many string-kind nodes of *any other* kind were seen.
3396    ///
3397    /// Returns `(kept_literal, withdrawn, other_strings)`. Texts rather
3398    /// than counts because a count is polarity-blind: `(1, 1)` holds
3399    /// just as well when the predicate calls the `proc` body a literal
3400    /// and `{a b}` a script, which is the claim inverted. `other_strings`
3401    /// anchors the control the third fixture line supplies — without it
3402    /// the every-other-kind assertion below can be deleted from the
3403    /// fixture and the kind-gate perturbation stops failing anything.
3404    ///
3405    /// Also asserts the two spellings agree off a known chain and off a
3406    /// `Node::parent` climb: the walk now threads a chain, and the
3407    /// answer must not depend on which it gets.
3408    fn braced_word_string_verdicts<L: crate::traits::LanguageInfo + Checker>(
3409        label: &str,
3410        code: &[u8],
3411        script_kind: u16,
3412    ) -> (Vec<String>, Vec<String>, usize) {
3413        let (mut kept, mut withdrawn, mut other) = (Vec::new(), Vec::new(), 0);
3414        for_each_node_with_chain::<L>(code, |node, chain| {
3415            let known = L::is_string_with_code(node, code, Ancestors::known(chain));
3416            assert_eq!(
3417                known,
3418                L::is_string_with_code(node, code, Ancestors::unknown()),
3419                "{label}: is_string_with_code disagrees between a known chain and a \
3420                 parent climb on {} at row {}",
3421                node.kind(),
3422                node.start_row()
3423            );
3424            if node.kind_id() != script_kind {
3425                // Every other kind must answer identically, or the
3426                // override has widened past the one ambiguous kind.
3427                assert_eq!(
3428                    known,
3429                    L::is_string(node),
3430                    "{label}: the override moved a {} node at row {}",
3431                    node.kind(),
3432                    node.start_row()
3433                );
3434                other += usize::from(known);
3435                return;
3436            }
3437            assert!(
3438                L::is_string(node),
3439                "{label}: the kind table must still list the script kind, or this \
3440                 test is measuring its absence rather than the override"
3441            );
3442            let text = String::from_utf8_lossy(&code[node.start_byte()..node.end_byte()]);
3443            if known {
3444                kept.push(text.into_owned());
3445            } else {
3446                withdrawn.push(text.into_owned());
3447            }
3448        });
3449        (kept, withdrawn, other)
3450    }
3451
3452    /// A Tcl-family script body is a string literal to
3453    /// [`Checker::is_string`] and not to
3454    /// [`Checker::is_string_with_code`] (#1381).
3455    ///
3456    /// This is the test the end-to-end `bca find` ones cannot be: they
3457    /// observe the *result*, which is equally consistent with the
3458    /// override never firing and the byte-less default happening to be
3459    /// right. Asserting the disagreement directly is what makes a future
3460    /// call site that reaches for the byte-less spelling a wrong answer
3461    /// rather than an indistinguishable one
3462    /// (`.claude/rules/grammar-dispatch.md` §7).
3463    ///
3464    /// Each fixture holds a braced value, a script body reached through
3465    /// a *modelled* construct, a script body reached through the
3466    /// **command-name** list (`eval`), and a non-braced string literal —
3467    /// four rows that no constant and no inverted polarity satisfies.
3468    /// The two script routes are structurally independent
3469    /// (grammar-dispatch §11): `is_value_braced_word` answers "script"
3470    /// either because the word fills a modelled slot or because the
3471    /// enclosing command is in `SCRIPT_TAKING_COMMANDS`, and a fixture
3472    /// exercising only the first leaves the second dead.
3473    #[test]
3474    #[cfg(any(feature = "tcl", feature = "irules"))]
3475    fn tcl_script_body_is_a_string_only_to_the_byteless_spelling() {
3476        let mut ran = 0;
3477        #[cfg(feature = "tcl")]
3478        {
3479            ran += 1;
3480            // `{x}` is the parameter list, which the grammar spells
3481            // `arguments` rather than a braced word, so it is neither.
3482            // `set s "q"` is the control for the kind gate: a
3483            // `quoted_word` under a *modelled* command is a node the
3484            // role predicate answers "not a value" for, so an override
3485            // that dropped its `kind_id` test would unstring it — which
3486            // the `other` count below is what notices.
3487            let (kept, withdrawn, other) = braced_word_string_verdicts::<crate::langs::TclCode>(
3488                "tcl",
3489                b"proc p {x} { puts $x }\nlappend l {a b}\neval {puts hi}\nset s \"q\"\n",
3490                Tcl::BracedWord as u16,
3491            );
3492            assert_eq!(kept, ["{a b}"], "tcl: only the braced value is a literal");
3493            assert_eq!(
3494                withdrawn,
3495                ["{ puts $x }", "{puts hi}"],
3496                "tcl: the proc body (a modelled slot) and the `eval` argument \
3497                 (a named script-taking command) are both scripts"
3498            );
3499            assert_eq!(other, 1, "tcl: the `set` quoted word stays a string");
3500        }
3501        #[cfg(feature = "irules")]
3502        {
3503            ran += 1;
3504            // The `"hi"` inside the handler body is this dialect's
3505            // `other` row, so the control sits inside the construct
3506            // under test rather than beside it.
3507            let (kept, withdrawn, other) = braced_word_string_verdicts::<crate::langs::IrulesCode>(
3508                "irules",
3509                b"when HTTP_REQUEST { log local0. \"hi\" }\nlappend l {x y}\neval {puts hi}\n",
3510                Irules::BracedWord as u16,
3511            );
3512            assert_eq!(
3513                kept,
3514                ["{x y}"],
3515                "irules: only the braced value is a literal"
3516            );
3517            assert_eq!(
3518                withdrawn,
3519                ["{ log local0. \"hi\" }", "{puts hi}"],
3520                "irules: the `when` handler body and the `eval` argument are scripts"
3521            );
3522            assert_eq!(other, 1, "irules: the quoted word stays a string");
3523        }
3524        assert!(
3525            ran > 0,
3526            "neither tcl nor irules is enabled; this test asserted nothing"
3527        );
3528    }
3529
3530    /// The value slots of the three constructs whose argument lists hold
3531    /// both roles stay string literals, though `is_value_braced_word`
3532    /// classifies each construct whole as script-taking
3533    /// (`Getter::is_braced_literal_slot`).
3534    ///
3535    /// Every fixture line carries a literal *and* a script in the same
3536    /// construct, so each rule is pinned from both sides: a rule that
3537    /// withdrew the whole construct fails on the literal, and one that
3538    /// rescued it whole fails on the script. The two switch lines are the
3539    /// guards' own rows — an arm whose *pattern* is spelled `proc`, `on`
3540    /// or `trap` parses as that construct around arm bodies, which must
3541    /// stay scripts. They need a line each: `proc {…} on {…}` parses as
3542    /// one `procedure`, so only the second line puts an `on` *command*
3543    /// in front of the owner guard.
3544    ///
3545    /// The Tcl `trap` line stands alone because the walk helper refuses a
3546    /// tree with a parse error, and `try {…} trap …` on one line reaches
3547    /// the same generic `trap` command only through error recovery (a
3548    /// missing terminator the grammar inserts after the `try` body).
3549    #[test]
3550    #[cfg(any(feature = "tcl", feature = "irules"))]
3551    fn tcl_family_value_slots_of_script_takers_stay_strings() {
3552        let mut ran = 0;
3553        #[cfg(feature = "tcl")]
3554        {
3555            ran += 1;
3556            let (kept, withdrawn, _) = braced_word_string_verdicts::<crate::langs::TclCode>(
3557                "tcl",
3558                b"proc {my proc} {x} { puts $x }\n\
3559                  namespace export {a b}\n\
3560                  namespace eval ns { puts hi }\n\
3561                  namespace ensemble create -map {add ::a}\n\
3562                  trap {POSIX ENOENT} {msg} { puts $msg }\n\
3563                  switch $k { proc {puts p} on {puts o} }\n\
3564                  switch $k { on {puts o} trap {puts t} }\n",
3565                Tcl::BracedWord as u16,
3566            );
3567            assert_eq!(
3568                kept,
3569                ["{my proc}", "{a b}", "{add ::a}", "{POSIX ENOENT}", "{msg}"],
3570                "tcl: a proc name, a non-`eval` namespace argument, and a \
3571                 `trap` pattern and variable list are literals"
3572            );
3573            assert_eq!(
3574                withdrawn,
3575                [
3576                    "{ puts $x }",
3577                    "{ puts hi }",
3578                    "{ puts $msg }",
3579                    "{ proc {puts p} on {puts o} }",
3580                    "{puts p}",
3581                    "{puts o}",
3582                    "{ on {puts o} trap {puts t} }",
3583                    "{puts o}",
3584                    "{puts t}",
3585                ],
3586                "tcl: the proc body, the `namespace eval` body, the `trap` \
3587                 script, and the switch arm list and its bodies are scripts"
3588            );
3589        }
3590        #[cfg(feature = "irules")]
3591        {
3592            ran += 1;
3593            let (kept, withdrawn, other) = braced_word_string_verdicts::<crate::langs::IrulesCode>(
3594                "irules",
3595                b"proc {my proc} {x} { log local0. $x }\n\
3596                  namespace export {a b}\n\
3597                  when HTTP_REQUEST { log local0. \"hi\" }\n\
3598                  on error {m2} {drop}\n",
3599                Irules::BracedWord as u16,
3600            );
3601            assert_eq!(
3602                kept,
3603                ["{my proc}", "{a b}", "{m2}"],
3604                "irules: a proc name, a `namespace` value and an `on` variable \
3605                 list are literals"
3606            );
3607            assert_eq!(
3608                withdrawn,
3609                ["{ log local0. $x }", "{ log local0. \"hi\" }", "{drop}"],
3610                "irules: the proc and `when` bodies and the `on` handler are scripts"
3611            );
3612            assert_eq!(other, 1, "irules: the quoted word stays a string");
3613        }
3614        assert!(
3615            ran > 0,
3616            "neither tcl nor irules is enabled; this test asserted nothing"
3617        );
3618    }
3619
3620    /// A command whose signature mixes the two roles keeps its *value*
3621    /// arguments string literals, one rule per row of
3622    /// `lang_helpers::tcl_family::SCRIPT_TAKING_COMMANDS` (#1381 review).
3623    ///
3624    /// `is_value_braced_word` answers by command name alone, so before
3625    /// the slot table every braced argument of a script-taking command
3626    /// was a script: `time {set x 1} {5}` lost the count, `after {100}`
3627    /// the delay, `switch {foo} {…}` the subject, and
3628    /// `namespace eval {foo bar} {…}` the namespace name.
3629    ///
3630    /// Every line carries a value *and* a script, so each rule is pinned
3631    /// from both sides: a rule that withdrew the whole construct fails on
3632    /// the value, and one that rescued it whole fails on the script.
3633    /// Flipping which index a rule calls the value moves a row from one
3634    /// list to the other, so neither list can be satisfied by a constant
3635    /// or by an inverted polarity.
3636    ///
3637    /// The two `uplevel` lines are the level gate's own pair, the last
3638    /// Tcl line is the `is_switch_arm` guard's — an arm whose *pattern*
3639    /// is spelled `after` builds an `after` command around what are
3640    /// really arm bodies, whose first would otherwise become the delay —
3641    /// and the `-matchvar` line pins the subject scan's option arity and
3642    /// its `--` terminator. iRules has no `switch` rows because its
3643    /// grammar models the construct and rejects a braced subject
3644    /// outright (the word lands under an `ERROR`, where the walk helper
3645    /// refuses to go).
3646    #[test]
3647    #[cfg(any(feature = "tcl", feature = "irules"))]
3648    fn tcl_family_mixed_signature_value_arguments_stay_strings() {
3649        let mut ran = 0;
3650        #[cfg(feature = "tcl")]
3651        {
3652            ran += 1;
3653            let (kept, withdrawn, _) = braced_word_string_verdicts::<crate::langs::TclCode>(
3654                "tcl",
3655                b"after {100} {puts a}\n\
3656                  time {puts b} {5}\n\
3657                  uplevel {1} {puts c}\n\
3658                  uplevel {puts d}\n\
3659                  switch {sub} {p {puts e}}\n\
3660                  switch -exact -- {sub2} {q {puts f}}\n\
3661                  switch -- {sub4} {t} {puts m}\n\
3662                  switch -nocase -matchvar {mv} -regexp {sub3} {r {puts g}}\n\
3663                  switch $v {^s} {puts h}\n\
3664                  namespace eval {my ns} {puts i}\n\
3665                  namespace inscope {my ns2} {puts j}\n\
3666                  namespace code {puts k}\n\
3667                  switch $w { after {puts x} time {puts y} }\n",
3668                Tcl::BracedWord as u16,
3669            );
3670            assert_eq!(
3671                kept,
3672                [
3673                    "{100}", "{5}", "{1}", "{sub}", "{sub2}", "{sub4}", "{t}", "{mv}", "{sub3}",
3674                    "{^s}", "{my ns}", "{my ns2}",
3675                ],
3676                "tcl: an `after` delay, a `time` count, an `uplevel` level, a \
3677                 `switch` subject reached past three option spellings, a \
3678                 `-matchvar` operand, two flat-form patterns and the two \
3679                 `namespace` names are literals"
3680            );
3681            assert_eq!(
3682                withdrawn,
3683                [
3684                    "{puts a}",
3685                    "{puts b}",
3686                    "{puts c}",
3687                    "{puts d}",
3688                    "{p {puts e}}",
3689                    "{puts e}",
3690                    "{q {puts f}}",
3691                    "{puts f}",
3692                    "{puts m}",
3693                    "{r {puts g}}",
3694                    "{puts g}",
3695                    "{puts h}",
3696                    "{puts i}",
3697                    "{puts j}",
3698                    "{puts k}",
3699                    "{ after {puts x} time {puts y} }",
3700                    "{puts x}",
3701                    "{puts y}",
3702                ],
3703                "tcl: every arm list, arm body and evaluated argument is a \
3704                 script — including the sole argument of the second \
3705                 `uplevel`, which does not read as a level, and the two \
3706                 bodies of an arm whose pattern is spelled `after`"
3707            );
3708        }
3709        #[cfg(feature = "irules")]
3710        {
3711            ran += 1;
3712            let (kept, withdrawn, _) = braced_word_string_verdicts::<crate::langs::IrulesCode>(
3713                "irules",
3714                b"after {100} {log a}\n\
3715                  time {log b} {5}\n\
3716                  uplevel {1} {log c}\n\
3717                  uplevel {log d}\n\
3718                  namespace eval {my ns} {log e}\n\
3719                  namespace inscope {my ns2} {log f}\n\
3720                  namespace code {log g}\n",
3721                Irules::BracedWord as u16,
3722            );
3723            assert_eq!(
3724                kept,
3725                ["{100}", "{5}", "{1}", "{my ns}", "{my ns2}"],
3726                "irules: the same delay, count, level and namespace names are \
3727                 literals — the two dialects share one predicate and must not \
3728                 drift"
3729            );
3730            assert_eq!(
3731                withdrawn,
3732                [
3733                    "{log a}", "{log b}", "{log c}", "{log d}", "{log e}", "{log f}", "{log g}",
3734                ],
3735                "irules: every evaluated argument is a script"
3736            );
3737        }
3738        assert!(
3739            ran > 0,
3740            "neither tcl nor irules is enabled; this test asserted nothing"
3741        );
3742    }
3743
3744    /// A mixed-signature command whose *layout* cannot be resolved keeps
3745    /// the construct-wide script answer, and a resolvable one beside it
3746    /// still yields its value.
3747    ///
3748    /// Three shapes reach a fallback in
3749    /// `lang_helpers::tcl_family` that no other fixture does:
3750    ///
3751    /// - `namespace $sub {…}` — the subcommand is a substitution, not a
3752    ///   plain word, so no `ScriptSlots` row can be looked up at all.
3753    /// - `switch -matchvar {m} -indexvar {i}` — both options consume the
3754    ///   word after them, so the scan runs off the end without finding a
3755    ///   subject. *Two* pairs, because one leaves the scan at a list of
3756    ///   two whose second word an invented subject 0 would call the arm
3757    ///   list — the same answer, so a single pair proves nothing.
3758    /// - `switch bar {p} {puts c} {q} {puts d}` — the subject is a bare
3759    ///   `simple_word`, the one spelling neither the braced nor the `$v`
3760    ///   fixtures above exercise. *Two* arms, for the same reason: with
3761    ///   one, the trailing argument is the braced arm list either way, so
3762    ///   the subject's index does not change any answer.
3763    ///
3764    /// Pairing each with a resolvable line is what makes the claim
3765    /// two-sided: the exact vectors below say `{my ns}`, `{p}` and `{q}`
3766    /// are values *and* that `{puts b}`, `{m}` and `{i}` are not, so a
3767    /// fallback answering "value" instead moves rows between the lists.
3768    ///
3769    /// The `switch` rows are Tcl-only by construction. The iRules grammar
3770    /// models `switch`, so a well-formed one is never the generic
3771    /// `command` this rule reads, and a malformed one lands under an
3772    /// `ERROR` that `is_braced_literal_slot` rejects before the slot
3773    /// table — the same asymmetry `SCRIPT_TAKING_COMMANDS` records for
3774    /// its `for` and `switch` rows.
3775    #[test]
3776    #[cfg(any(feature = "tcl", feature = "irules"))]
3777    fn tcl_family_unresolvable_slot_layouts_keep_the_script_answer() {
3778        let mut ran = 0;
3779        #[cfg(feature = "tcl")]
3780        {
3781            ran += 1;
3782            let (kept, withdrawn, _) = braced_word_string_verdicts::<crate::langs::TclCode>(
3783                "tcl",
3784                b"namespace eval {my ns} {puts a}\n\
3785                  namespace $sub {puts b}\n\
3786                  switch -matchvar {m} -indexvar {i}\n\
3787                  switch bar {p} {puts c} {q} {puts d}\n",
3788                Tcl::BracedWord as u16,
3789            );
3790            assert_eq!(
3791                kept,
3792                ["{my ns}", "{p}", "{q}"],
3793                "tcl: a resolvable `namespace eval` name and both flat-form \
3794                 patterns behind a bare-word subject are literals"
3795            );
3796            assert_eq!(
3797                withdrawn,
3798                ["{puts a}", "{puts b}", "{m}", "{i}", "{puts c}", "{puts d}"],
3799                "tcl: an unresolvable `namespace` subcommand and a `switch` \
3800                 with no subject keep every braced argument a script"
3801            );
3802        }
3803        #[cfg(feature = "irules")]
3804        {
3805            ran += 1;
3806            let (kept, withdrawn, _) = braced_word_string_verdicts::<crate::langs::IrulesCode>(
3807                "irules",
3808                b"namespace eval {my ns} {log a}\n\
3809                  namespace $sub {log b}\n",
3810                Irules::BracedWord as u16,
3811            );
3812            assert_eq!(
3813                kept,
3814                ["{my ns}"],
3815                "irules: the resolvable `namespace eval` name is a literal"
3816            );
3817            assert_eq!(
3818                withdrawn,
3819                ["{log a}", "{log b}"],
3820                "irules: the unresolvable subcommand keeps its argument a script"
3821            );
3822        }
3823        assert!(
3824            ran > 0,
3825            "neither tcl nor irules is enabled; this test asserted nothing"
3826        );
3827    }
3828}