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