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