Skip to main content

big_code_analysis_ast/
getter.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 [`Getter`] accessors — names, space kinds, Halstead classes — and
10//! their per-language impls.
11
12use crate::token_role::TokenRole;
13
14use crate::lang_helpers::tcl_family::{
15    ArgumentRoles, Dialect, SWITCH_COMMAND, argument_slots_are_readable, fills_script_slot,
16    namespace_script_slots, script_slots,
17};
18use crate::space_kind::SpaceKind;
19use crate::traits::Search;
20
21use crate::*;
22
23/// Bounds- and UTF-8-checked text extraction for a node's byte span.
24///
25/// `code` is `&[u8]`, so slicing it carries no char-boundary
26/// precondition; the two guards here cover two unrelated failure modes:
27///
28/// * `std::str::from_utf8` rejects non-UTF-8 bytes. This one is
29///   reachable from ordinary use — `big_code_analysis::Ast::parse` accepts
30///   arbitrary bytes, so a node span in a partially-binary source need
31///   not be valid UTF-8.
32/// * `code.get` bounds-checks the range. This one is reachable only by
33///   violating the same-parse precondition documented on `Getter`, i.e.
34///   `big_code_analysis::Ast::from_tree_sitter` adopting a tree built from longer
35///   source than the `code` passed alongside it.
36///
37/// Both degrade to `None`. The walker stores a space's name as
38/// `Option<String>` (`spaces.rs`), so that records an unnamed space
39/// rather than crashing — note this is *not* the same path as a node
40/// with no `name` field, which `get_func_space_name` reports as
41/// `Some("<anonymous>")` without reaching here. The unguarded sibling
42/// slice sites return infallible
43/// types feeding metric arithmetic or rendered output; there the only
44/// available fallback would be a fabricated empty value that silently
45/// corrupts a count, so they rely on the precondition instead. The
46/// asymmetry is deliberate: guarding is free here because `Option` is
47/// already part of this signature's contract (#1059).
48#[inline]
49pub(crate) fn node_text<'a>(code: &'a [u8], node: &Node) -> Option<&'a str> {
50    code.get(node.start_byte()..node.end_byte())
51        .and_then(|bytes| std::str::from_utf8(bytes).ok())
52}
53
54macro_rules! get_operator {
55    ($language:ident) => {
56        #[inline]
57        fn get_operator_id_as_str(id: u16) -> &'static str {
58            let typ = id.into();
59            match typ {
60                $language::LPAREN => "()",
61                $language::LBRACK => "[]",
62                $language::LBRACE => "{}",
63                _ => typ.into(),
64            }
65        }
66    };
67}
68
69// Emit a `Getter::get_op_type` body for a JS-family language. The four
70// JS-family grammars (JavaScript, MozJS, TypeScript, TSX) share most of
71// their Halstead operator/operand kind classifications; per-language
72// deltas are passed as bracketed extras so all four impls stay in
73// lockstep when a kind is added or removed (issue #299).
74//
75// `$op_extras` per language:
76//   * JavaScript / MozJS: `OptionalChain` — the bare `?.` token (these
77//     grammars expose no `optional_chain` wrapper).
78//   * TypeScript / TSX:   `QMARKDOT`, `PredefinedType` — `QMARKDOT` is
79//     the bare `?.` token under the `optional_chain` wrapper (issue
80//     #281); `PredefinedType` is the TS type keyword set (`string`,
81//     `number`, `boolean`, …).
82//   * JavaScript / MozJS / TSX: `LTSLASH`, `SLASHGT` — the JSX closing
83//     and self-closing tag delimiters, which these grammars spell as
84//     their own two-character tokens rather than as a `<` / `>` pair.
85//     Until #1395 neither was in either arm, so a JSX element billed
86//     its opening `<` and its `>`s but nothing for `</`, and `<br />`
87//     reported one bracket operator where the source spells two. They
88//     are extras rather than members of the shared arm because
89//     TypeScript is the one grammar of the four with no JSX and
90//     therefore no such variants to name.
91//
92// `$operand_extras` per language:
93//   * JavaScript / MozJS / TSX: `Identifier2`, `String2` — anonymous
94//     keyword aliases the JS grammar exposes for `Identifier` and
95//     `String`. TSX's `String2` (kind_id 261) is its string-literal
96//     alias, so it stays an operand like JS's.
97//   * TypeScript: none. TS's own aliases are either operators or
98//     deliberately unclassified (below).
99//     The `: string` type-keyword aliases (TS `String2`, kind_id 135;
100//     TSX `String3`, kind_id 141) are deliberately absent: they are
101//     emitted only as the child of a `predefined_type` wrapper, which
102//     already contributes the text-keyed `"string"` operator, and
103//     #313's listing of the keyword child as an operand counted one
104//     source token twice (#1261, which also narrowed
105//     `Checker::is_string` to match).
106//
107// **A member access contributes its leaves, never the composite.**
108// `a.b` is the operands `a` and `b` plus the `.` operator — three
109// vocabulary entries, not four. Until #1263 the operand arm listed the
110// `member_expression` wrapper *and* the walker descended into its
111// `object` / `property` children, so every access billed one extra
112// `N2` entry keyed on the whole `a.b` text (and `a.b.c` billed two:
113// `a.b` and `a.b.c`). The composite is what the rest of the workspace
114// already excludes — C, C++, Java, Rust, Python, Go, Kotlin, Ruby, Lua
115// and PHP were all measured leaves-only for the identical shape — so
116// identical code scored differently by language. The same call applies
117// to TS's `nested_identifier` (`namespace N.M`), C#'s
118// `qualified_name` / `generic_name` / `alias_qualified_name`, and
119// Groovy's `qualified_name` / `qualified_type`.
120//
121// This is grammar-dispatch section 5's "any predicate listing both a
122// container and a kind it can contain double-counts", and its section
123// 6 corollary is why `PrivatePropertyIdentifier` joins the operand arm
124// in the same change: `this.#x`'s `#x` leaf was in no operand list, so
125// the composite had been its only count, and a bare deletion would
126// have regressed private-field access to zero operands. (The `#x` in
127// the *field definition* `#x = 1` had never been counted at all — no
128// wrapper covered it — so this fixes that half too.)
129//
130// The `TemplateString` interpolation guard is shared verbatim (issue
131// #192): a bare `` `...` `` mirrors a `"..."` operand, but an
132// interpolated template must yield `Unknown` because its inner
133// `TemplateSubstitution` expressions are walked separately.
134//
135// The `Regex` arms are shared the same way (issue #1314), and for the
136// same reason: all four grammars spell a regex literal identically —
137// a `regex` wrapper whose two delimiters are `SLASH`, the kind id real
138// division uses — so neither arm has a per-language delta to hoist to
139// a call site. Only the ids differ (`SLASH` 87/81/90/87, `Regex`
140// 224/250/264/225) and the macro names both through the enum, so no
141// invocation mentions either. See the arms themselves for the
142// fabrication and the missing-operand halves.
143macro_rules! impl_js_family_get_op_type {
144    (
145        $lang:ident,
146        op_extras: [$($op_extra:ident),* $(,)?],
147        operand_extras: [$($operand_extra:ident),* $(,)?]
148        $(, predefined_void: $predefined_type:ident)? $(,)?
149    ) => {
150        fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
151            use $lang::*;
152
153            // TS/TSX only: a `void` return / parameter type is parsed as a
154            // `predefined_type` wrapper around an inner `void` token. Both
155            // the wrapper (routed through `is_primitive` into the text-keyed
156            // `primitive_operators` map as `"void"`) and the inner `Void`
157            // token (a standalone expression operator, e.g. `void 0`) would
158            // otherwise classify as operators, double-counting one source
159            // `void` as two Halstead operators (issue #453). Only `void` has
160            // an operator-kind child; the `string` keyword's child is an
161            // operand-kind alias, which collided in the other direction
162            // (operator + operand) until #1261 dropped it from the operand
163            // extras. Suppress the wrapper here and
164            // let the inner `Void` token carry the single operator, keeping
165            // the kind_id-keyed count consistent with expression `void 0`
166            // (the lesson-4 `n1 == dedupe(ops.operators)` invariant).
167            $(
168                if node.kind_id() == $predefined_type as u16
169                    && node
170                        .child(0)
171                        .is_some_and(|child| child.kind_id() == Void as u16)
172                {
173                    return TokenRole::Unknown;
174                }
175            )?
176
177            match node.kind_id().into() {
178                // Regex delimiter punctuation. A `regex` literal spells
179                // both of its delimiters with the same kind id real
180                // division uses, so `const a = /abc/g;` reported a `/`
181                // operator with no division in the source and n1/N1
182                // counted the literal's punctuation as arithmetic
183                // (#1314, the JS-family sibling of Elixir #1256 and
184                // Ruby/Perl #1312). The `Regex` node itself is the
185                // operand (below), so the delimiters are suppressed
186                // exactly when their parent is that node — the
187                // compound-leaf guard of grammar-dispatch section 5.
188                //
189                // Parent, not ancestor, for correctness by
190                // construction rather than by observation: unlike
191                // Ruby's `#{…}`, a JS regex admits no nested
192                // expression at all (`regex_pattern` and `regex_flags`
193                // are leaves), so no fixture can tell this guard from
194                // an ancestor-scanning one. The halstead test
195                // `js_regex_delimiter_guard_is_parent_scoped_is_unobservable`
196                // records that, and pins the grammar property it rests
197                // on, rather than implying coverage the suite lacks.
198                //
199                // `SLASH2` — the aliased regex-start token every one of
200                // these grammars carries — needs no arm: it is absent
201                // from the operator list below, so it already lands on
202                // `Unknown`, which is what a delimiter should be. That
203                // is the one place this differs from Ruby's guard,
204                // where `SLASH2` had to be moved off the arithmetic arm.
205                SLASH
206                    if ancestors.parent_has_kind(node, Regex as u16) =>
207                {
208                    TokenRole::Unknown
209                }
210                Export | Import | Import2 | Extends | DOT | From | LPAREN | COMMA | As | STAR
211                | GTGT | GTGTGT | COLON | Return | Delete | Throw | Break | Continue | If
212                | Else | Switch | Case | Default | Async | Do | For | In | Of | While | Try
213                | Catch | Finally | With | EQ | AT | AMPAMP | PIPEPIPE | PLUS | DASH | DASHDASH
214                | PLUSPLUS | SLASH | PERCENT | STARSTAR | PIPE | AMP | LTLT | TILDE | LT | LTEQ
215                | EQEQ | BANGEQ | GTEQ | GT | PLUSEQ | BANG | BANGEQEQ | EQEQEQ | DASHEQ
216                | STAREQ | SLASHEQ | PERCENTEQ | STARSTAREQ | GTGTEQ | GTGTGTEQ | LTLTEQ | AMPEQ
217                | CARET | CARETEQ | PIPEEQ | Yield | LBRACK | LBRACE | Await | QMARK
218                | QMARKQMARK | EQGT | DOTDOTDOT | New | Let | Var | Const | Function
219                | FunctionExpression | SEMI | Typeof | Instanceof | Void
220                // `get`/`set` accessor keywords are operators, matching the
221                // C# getter's `Get | Set | Init | Add | Remove` accessor arm.
222                // Before #695 the JS family classified them as operands, so
223                // the same accessor keyword landed in opposite Halstead
224                // groups across languages, skewing n1/n2 for accessor-heavy
225                // code (#695).
226                | Set | Get
227                $(| $op_extra)* => TokenRole::Operator,
228                // `Regex` is the literal's own node and contributes one
229                // operand, the way Ruby's `Regex` and Elixir's `Sigil`
230                // do. It was in neither arm before #1314, so `/abc/g`
231                // reached the vocabulary from *neither* side: a
232                // fabricated `/` operator and no operand at all. Its
233                // `regex_pattern` / `regex_flags` children stay
234                // unclassified, so the wrapper cannot double-count
235                // them, and no interpolation is possible inside a JS
236                // regex — hence a plain arm here rather than the
237                // `string_operand_type` dispatch the template literal
238                // below needs.
239                // `PrivatePropertyIdentifier` is the `#x` leaf of a
240                // private class field, in both its declaration
241                // (`#x = 1`) and its access (`this.#x`). See the
242                // leaves-not-composites note above the macro for why
243                // it had to be added in the same change that dropped
244                // `MemberExpression*`.
245                //
246                // `MetaProperty` is `import.meta` / `new.target`: one
247                // atomic operand, like `this`, whose `meta` / `target`
248                // leaves are anonymous tokens no arm classifies. It is
249                // the one composite the #1263 drop has to keep
250                // (grammar-dispatch §6) — without it the meta-object
251                // contributes no operand at all while `this.env.x`
252                // still yields three.
253                Identifier | PropertyIdentifier | PrivatePropertyIdentifier | MetaProperty
254                | String | Number | True | False | Null | This | Super | Undefined | Regex
255                $(| $operand_extra)* => TokenRole::Operand,
256                // A `` `...` `` is a string literal; without interpolation it
257                // mirrors `"..."` and contributes one operand. When it has a
258                // `TemplateSubstitution` child the inner expression is already
259                // walked and classified, so counting the wrapper too would
260                // double-count its contribution to `N2` (issue #192, same
261                // pattern as #183 C# / #191 Kotlin / #199 Perl).
262                TemplateString => {
263                    Self::string_operand_type(node, &[TemplateSubstitution as u16])
264                }
265                _ => TokenRole::Unknown,
266            }
267        }
268    };
269}
270
271/// The default space name: the node's `name` field, else `<anonymous>`.
272///
273/// A free function as well as the trait default so a language that needs
274/// to name a *few* kinds specially can delegate the rest rather than
275/// restate the rule — `<get>` / `<set>` / `<init>` / `<static-init>` in
276/// Kotlin, Java and Groovy all do (#1184). Calling `Self::…` there would
277/// recurse.
278#[must_use]
279pub fn default_func_space_name<'a, 'tree>(
280    node: &Node<'tree>,
281    code: &'a [u8],
282    _ancestors: Ancestors<'tree, '_>,
283) -> Option<&'a str> {
284    // we're in a function or in a class
285    if let Some(name) = node.child_by_field_name("name") {
286        node_text(code, &name)
287    } else {
288        Some("<anonymous>")
289    }
290}
291
292/// Per-language accessors the space walker and the Halstead
293/// operator/operand classification dispatch through.
294///
295/// # Precondition
296///
297/// Every method taking a `code: &[u8]` next to a `&Node` slices `code`
298/// by that node's byte range. `code` must be the exact buffer `node` was
299/// parsed from — `big_code_analysis::Ast::source` for a node obtained from the
300/// same `big_code_analysis::Ast`. Pairing a node with any other buffer reads the
301/// wrong bytes at best and panics on an out-of-bounds index at worst;
302/// the same precondition is documented on [`crate::dump_node`] (#795).
303/// `node_text` — reached from the default `get_func_space_name` and
304/// from the per-language `get_func_name` overrides — is the sole
305/// bounds-checked slice; see its docs for why the rest deliberately
306/// are not.
307#[doc(hidden)]
308/// The kinds one Tcl-family dialect spells the braced-word construct
309/// with, for [`Getter::is_subsumed_braced_word`] and
310/// [`Getter::braced_word_op_type`]. A struct rather than a row of `u16`
311/// parameters because the ids are same-typed and positional: a
312/// transposed pair compiles and silently inverts the rule for that
313/// dialect.
314pub struct BracedWordKinds {
315    /// `braced_word_simple`, the literal *value* form.
316    pub value: u16,
317    /// `braced_word`, the *script* form.
318    pub script: u16,
319    /// `comment`, the one named child of a script that is not a
320    /// command.
321    pub comment: u16,
322    /// `command`, the generic command node. Its `name` field carries
323    /// the leading word [`Getter::is_value_braced_word`] recognises the
324    /// construct by, and it is the only parent a *generic* argument
325    /// list hangs from. The hidden `_command` supertype (`Command2` in
326    /// both enums) is deliberately absent: the parser never emits it
327    /// (grammar-dispatch §2).
328    pub command: u16,
329    /// `word_list`, a command's `arguments` field — and, in both
330    /// grammars, also the argument list of the modelled `namespace`
331    /// construct, which is why the rule reads the grandparent rather
332    /// than stopping here.
333    pub word_list: u16,
334    /// `simple_word`, the only spelling of a command name this rule
335    /// resolves. A computed name (`$cmd {…}`, `[pick] {…}`) parses as
336    /// `variable_substitution` / `command_substitution` and is not
337    /// statically resolvable at all.
338    pub simple_word: u16,
339    /// `argument`, one entry of a `proc` parameter list. A modelled
340    /// slot that holds a *value* rather than a script: a defaulted
341    /// parameter (`proc p {a {b {x y}}}`) spells its default as a
342    /// `braced_word`, and a default is never evaluated as code.
343    pub argument: u16,
344    /// `procedure`, the modelled `proc` construct. Its `name` field is a
345    /// `braced_word` when the name holds a space (`proc {my proc} …`),
346    /// and a name is a literal although the construct's other braced
347    /// slot, the body, is a script
348    /// ([`Getter::is_braced_literal_slot`]).
349    pub procedure: u16,
350    /// `namespace`, the modelled construct — not the keyword token that
351    /// shares its name. Its `word_list` holds a subcommand followed by
352    /// that subcommand's arguments, and only three subcommands take a
353    /// script ([`Getter::is_braced_literal_slot`]).
354    pub namespace: u16,
355    /// `{`, the brace opener — the *only* node
356    /// [`Getter::braced_word_op_type`] may revise. A braced word's
357    /// operator children are not the opener alone: `_terminator` is a
358    /// hidden rule, so a `;` separating two commands is inlined as a
359    /// direct child of the `braced_word` and both dialects classify it
360    /// as an operator. Keying the revision on the parent kind alone
361    /// therefore swallowed those separators too, taking
362    /// `lappend x {puts a ; puts b}` to `n1` 0 / `N1` 0 and
363    /// `halstead.effort` — a gated threshold metric — to `0.0`.
364    pub open_brace: u16,
365}
366
367/// Per-language accessors that *name* and *classify* what a node is:
368/// the function or space name, the [`SpaceKind`] a node opens, and the
369/// [`TokenRole`] of a leaf.
370///
371/// Every method has a default that answers "nothing" (`None`,
372/// [`SpaceKind::Unknown`], [`TokenRole::Unknown`]); a language
373/// overrides only the ones its grammar expresses.
374pub trait Getter {
375    /// Names the function `node` declares. Defaults to
376    /// [`get_func_space_name`](Self::get_func_space_name).
377    #[must_use]
378    fn get_func_name<'a, 'tree>(
379        node: &Node<'tree>,
380        code: &'a [u8],
381        ancestors: Ancestors<'tree, '_>,
382    ) -> Option<&'a str> {
383        Self::get_func_space_name(node, code, ancestors)
384    }
385
386    /// Names the space `node` opens.
387    ///
388    /// `ancestors` is the chain the caller descended through. Elixir
389    /// needs it: its `def` / `defmodule` heads are ordinary `Call`
390    /// nodes, and one inside a `quote` template names no space at all,
391    /// which is a question about what encloses the call (#1088).
392    #[must_use]
393    fn get_func_space_name<'a, 'tree>(
394        node: &Node<'tree>,
395        code: &'a [u8],
396        ancestors: Ancestors<'tree, '_>,
397    ) -> Option<&'a str> {
398        default_func_space_name(node, code, ancestors)
399    }
400
401    /// The kind of space `node` opens, or [`SpaceKind::Unknown`] when it
402    /// opens none.
403    #[must_use]
404    fn get_space_kind(_node: &Node) -> SpaceKind {
405        SpaceKind::Unknown
406    }
407
408    /// Source-aware variant of [`get_space_kind`](Self::get_space_kind). The default
409    /// forwards to the byte-less classifier; languages whose space
410    /// kinds are encoded in macro identifier text (Elixir's
411    /// `defmodule` / `def` / `defp` / `defmacro` / `defmacrop` Calls)
412    /// override this so the walker can attribute the correct
413    /// `SpaceKind` to each promoted func space (#275).
414    ///
415    /// `ancestors` is the chain the caller descended through; Elixir
416    /// needs it to see whether the `Call` sits inside a `quote`
417    /// template without paying `Node::parent`'s `O(depth)` (#1084).
418    #[inline]
419    #[must_use]
420    fn get_space_kind_with_code<'a>(
421        node: &Node<'a>,
422        _code: &[u8],
423        _ancestors: Ancestors<'a, '_>,
424    ) -> SpaceKind {
425        Self::get_space_kind(node)
426    }
427
428    /// Classifies `node` as a Halstead operator, operand, or neither.
429    ///
430    /// **Punctuation and delimiters are vocabulary, so they classify as
431    /// operators whatever grammatical role they serve.** Halstead
432    /// counts the alphabet a program is written in and has no notion of
433    /// a decision, so a `<` is the same vocabulary entry whether it
434    /// opens a JSX tag, brackets a Lua `<const>` attribute, names a C#
435    /// `operator <` overload, disambiguates a Kotlin `super<A>`, or
436    /// delimits a Perl `<FH>` readline. Every impl that classifies
437    /// punctuation at all already bills `(`, `{`, `[`, `,`, `;`, `.`
438    /// and `:` on exactly that reading, for each glyph its language
439    /// spells — bar two, which #1395 did not revisit: Python
440    /// classifies only `,` and `.` of that set, and Rust bills `::`
441    /// but not the type-annotation `:`.
442    ///
443    /// A *decision* metric gates the same token by role, and is
444    /// supposed to disagree: ABC's `conditions` counts a `<` only under
445    /// a `binary_expression` (#1274, #1275, #1280, #1297), so each of
446    /// the five constructs above scores zero there while `bca ops`
447    /// still reports the bracket. #1395 settled that the two answers
448    /// are both correct rather than a drift to be reconciled;
449    /// `tests/parity/abc_halstead_bracket_parity.rs` fails if a later
450    /// consistency pass flips either side.
451    ///
452    /// The one suppression that reading admits is **a literal's own
453    /// delimiter**. There the punctuation is not vocabulary in its own
454    /// right — the literal it wraps is already counted, so billing its
455    /// quotes fabricates an arithmetic or comparison operator the
456    /// source never spelled, and the score moves with the author's
457    /// choice of delimiter. It takes two shapes, and only the first is
458    /// an arm of this method.
459    ///
460    /// Nine arms suppress a delimiter whose enclosing literal node is
461    /// itself the operand, each guarded on the delimiter's *parent*
462    /// being that literal: Elixir's sigil (#1256), the regex `/` of the
463    /// JS family (#1314), of Perl and of Ruby (#1312), Ruby's subshell
464    /// backtick (#1360), Groovy's slashy string and the raw-string `(`
465    /// of both C++ grammars (#1314), and Objective-C's `@` before an
466    /// `NSString` literal.
467    ///
468    /// The tenth is [`braced_word_op_type`], which Tcl and iRules route
469    /// their `{` through. A braced *value* is a literal to
470    /// [`Checker::is_string_with_code`] and to the dump, so its opener
471    /// is withdrawn for the same reason — `puts {c d}` and
472    /// `puts "c d"` have to agree on the operator column — but the
473    /// operands are the words *inside* it rather than the braced word
474    /// itself, so the parent test above does not describe it. #1318
475    /// established that arm and #1382 widened it to the value slots of
476    /// a script-taking command.
477    ///
478    /// A bracket delimiting *syntax* fits neither shape: no literal
479    /// would absorb its contribution, so it stays an operator.
480    ///
481    /// `ancestors` is the chain the walker descended through. Six
482    /// impls read a parent from it to disambiguate a token whose role
483    /// depends on what encloses it: Python's `not` / `in` / `is` inside
484    /// the compound `not in` / `is not`, Rust's `||` and `!` inside a
485    /// binary expression rather than a doc-comment marker, the
486    /// namespace identifier in both C++ grammars, Bash's `$name`, and
487    /// iRules' `$var`. Reaching those parents with [`Node::parent`]
488    /// instead costs `O(depth)` per node (#1096).
489    ///
490    /// [`braced_word_op_type`]: Self::braced_word_op_type
491    /// [`Checker::is_string_with_code`]: crate::Checker::is_string_with_code
492    #[must_use]
493    fn get_op_type<'a>(_node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> TokenRole {
494        TokenRole::Unknown
495    }
496
497    /// Source-aware variant of [`get_op_type`]. The default forwards
498    /// to the byte-less classifier; languages whose Halstead operand
499    /// classification depends on token text override this. Kotlin uses
500    /// it to recover the variable in a short-form string template
501    /// (`"Hi $name"`), which the grammar emits as bare `string_content`
502    /// tokens with no structured interpolation node — the distinction
503    /// between an interpolated `$name` and a literal `$5` is only
504    /// visible in the source bytes (#454).
505    ///
506    /// [`get_op_type`]: Self::get_op_type
507    #[inline]
508    #[must_use]
509    fn get_op_type_with_code<'a>(
510        node: &Node<'a>,
511        _code: &[u8],
512        ancestors: Ancestors<'a, '_>,
513    ) -> TokenRole {
514        Self::get_op_type(node, ancestors)
515    }
516
517    /// Returns the source-byte slice used to key a Halstead *operand*.
518    /// The default keys on the operand node's full byte range. Kotlin
519    /// overrides this to narrow a short-interpolation name token
520    /// (`$name`) to its leading identifier prefix, because the grammar
521    /// glues trailing inter-segment text onto the name token
522    /// (`"$a $b"` → `"a "`); keying the raw bytes would record a
523    /// distinct `"a "` operand and break parity with the long `${a}`
524    /// form (#454).
525    #[inline]
526    #[must_use]
527    fn get_operand_id<'a>(
528        node: &Node<'a>,
529        code: &'a [u8],
530        _ancestors: Ancestors<'a, '_>,
531    ) -> &'a [u8] {
532        &code[node.start_byte()..node.end_byte()]
533    }
534
535    /// Classifies a string-literal `node` as a single Halstead
536    /// operand, *unless* it wraps an interpolation child drawn from
537    /// `interp_kinds` — in which case the wrapper yields
538    /// [`TokenRole::Unknown`] because the inner expressions are
539    /// walked and counted separately. Counting the wrapper too would
540    /// double-count their contribution to `N2`.
541    ///
542    /// This declares the per-language interpolation skip once (issue
543    /// #420), replacing nine independently-added regression fixes
544    /// (#183 / #184 / #191 / #192 / #199 / #277, …). Each language
545    /// supplies only its own grammar's interpolation child-kind ids;
546    /// the per-call rationale lives at each call site.
547    #[must_use]
548    fn string_operand_type(node: &Node, interp_kinds: &[u16]) -> TokenRole {
549        if node.wraps_any(interp_kinds) {
550            TokenRole::Unknown
551        } else {
552            TokenRole::Operand
553        }
554    }
555
556    /// Whether `node` is a fragment of a Tcl-family braced word rather
557    /// than a token of its own, and so contributes no operand (#1354).
558    ///
559    /// The two dialects are deliberate clones, so the rule is declared
560    /// once and instantiated with each grammar's [`BracedWordKinds`];
561    /// the per-call rationale lives at each call site. It answers yes
562    /// in two cases:
563    ///
564    /// - `node` sits directly inside a braced *value*
565    ///   (`braced_word_simple`). Tcl evaluates nothing between braces,
566    ///   so the value is its whole span and the parts inside it are
567    ///   letters — the wrapper carries the one operand. This subsumes
568    ///   #1314's narrower guard on the opening `{`, which is one more
569    ///   such child.
570    /// - `node` is a braced *script* (`braced_word`) holding at least
571    ///   one command — a named child other than a comment. Commands are
572    ///   what the walk descends into and counts in their own right, so
573    ///   billing the block as well would count its whole text a second
574    ///   time. A comment is named too, but no arm classifies it, so a
575    ///   body holding only comments is billed like an empty one rather
576    ///   than like nothing at all.
577    ///
578    /// A `script` with no command is deliberately not subsumed: the
579    /// same kind serves as the value slot of every command the grammar
580    /// does not special-case, where `lappend l {}` is an empty list and
581    /// the brace pair is its only carrier (grammar-dispatch §6). An
582    /// empty or comment-only `proc` body is spelled identically and so
583    /// also scores one operand, its whole text. #1318 can now tell the
584    /// two apart by the enclosing command, but it bills them alike, so
585    /// this arm needs no help from it.
586    ///
587    /// This is the *byte-less* half of the braced-word rule and it is
588    /// not the whole of it. [`braced_word_op_type`] suppresses the `{`
589    /// of a script-kind word the enclosing command shows to be a plain
590    /// value, and both Tcl-family getters reach it through
591    /// [`get_op_type_with_code`], which is what the walk calls
592    /// (grammar-dispatch §7). It revises no operand, so this arm's
593    /// answers stand unchanged.
594    ///
595    /// [`braced_word_op_type`]: Self::braced_word_op_type
596    /// [`get_op_type_with_code`]: Self::get_op_type_with_code
597    #[must_use]
598    fn is_subsumed_braced_word<'a>(
599        node: &Node<'a>,
600        ancestors: Ancestors<'a, '_>,
601        kinds: &BracedWordKinds,
602    ) -> bool {
603        ancestors.parent_has_kind(node, kinds.value)
604            || (node.kind_id() == kinds.script
605                && node
606                    .children()
607                    .any(|child| child.is_named() && child.kind_id() != kinds.comment))
608    }
609
610    /// The generic `command` node `word` is an argument of, paired with
611    /// that command's own ancestry — or `None` when `word` instead
612    /// fills a slot of a construct the grammar models, and so is a
613    /// script by construction.
614    ///
615    /// Both dialects hang a *generic* command's arguments off a
616    /// `word_list` and a *modelled* construct's slots off that
617    /// construct's own node (`procedure`, `if`, `else`, `elseif`,
618    /// `while`, `foreach`, `catch`, `try`, `finally`, `argument`, plus
619    /// iRules' `when_event`, `for`, `switch_arm`, `on_handler`,
620    /// `trap_handler` and the three `dict` loops). Asking the parent
621    /// kind therefore answers "is this a generic argument" without
622    /// enumerating the modelled parents — a list that would be a
623    /// coverage claim to re-derive on every grammar bump
624    /// (grammar-dispatch §1), and that a grammar gaining one more
625    /// modelled construct would silently invalidate.
626    ///
627    /// A `word_list` whose own parent is *not* a `command` is
628    /// `namespace`'s argument list — the one modelled construct in
629    /// either grammar that reaches its body through a `word_list`. The
630    /// grandparent test answers `None` there, so `namespace eval ns
631    /// {…}` keeps its body a script.
632    ///
633    /// A braced word directly under a `command` is the command *name*
634    /// (`{puts} hi`), not an argument, and also answers `None`: its
635    /// caller decides that case before asking, because a name is
636    /// always a literal and must not pick up the `switch`-arm rescue
637    /// below.
638    #[must_use]
639    fn generic_argument_command<'tree, 'chain>(
640        word: &Node<'tree>,
641        ancestors: Ancestors<'tree, 'chain>,
642        kinds: &BracedWordKinds,
643    ) -> Option<(Node<'tree>, Ancestors<'tree, 'chain>)> {
644        let mut chain = ancestors.iter(word);
645        let (parent, _) = chain.next()?;
646        if parent.kind_id() != kinds.word_list {
647            return None;
648        }
649        let (grandparent, grandparent_ancestors) = chain.next()?;
650        (grandparent.kind_id() == kinds.command).then_some((grandparent, grandparent_ancestors))
651    }
652
653    /// Whether a braced *script*-kind word is really a plain value —
654    /// the literal `{a b}` of `lappend x {a b}` rather than the block
655    /// of `eval {…}` (#1318).
656    ///
657    /// The two roles share one kind, so no kind-scoped arm can separate
658    /// them; recognition is out-of-band, by the enclosing command's
659    /// leading word (grammar-dispatch §9). A word that fills a modelled
660    /// construct's slot is a script; a word passed to a command in
661    /// `SCRIPT_TAKING_COMMANDS` is a script; **everything else is a
662    /// value**.
663    ///
664    /// That default is the load-bearing choice, and it is deliberately
665    /// the opposite of "keep today's answer for anything unrecognised":
666    ///
667    /// - The script-taking set is *closed* and small — Tcl defines no
668    ///   user-extensible control structures, so a command that
669    ///   evaluates a braced argument is either a core command listed
670    ///   above or a proc that forwards to `eval` / `uplevel`. The
671    ///   value-taking set is *open*: every user proc and every package
672    ///   command that takes a list or a pattern is in it. An
673    ///   unrecognised name is therefore far likelier to be value-taking.
674    /// - Defaulting to script *fabricates* — it reports a `{}` operator
675    ///   for a block the source does not contain. Defaulting to value
676    ///   can only *omit*: one `N1` occurrence per script handed to an
677    ///   unlisted command, and — when no modelled construct in the same
678    ///   space opens a block — the `{}` vocabulary entry with them.
679    ///   That last case is where the cost shows: a top-level
680    ///   `dict for {k v} $d { puts $k }` with nothing around it has no
681    ///   operator left, and Halstead's difficulty is a product with
682    ///   `n1` in it, so `effort` reads `0.0` rather than a little low.
683    ///   Inside a `proc` the `proc` keyword and body brace keep the
684    ///   space non-zero, which is the scope `halstead.effort` is gated
685    ///   at; and the same `0.0` was already the answer for a file of
686    ///   `set x {a b}` lines, so it is Halstead's shape on an
687    ///   operator-free space, not a new failure mode. The omission
688    ///   stays bounded only because the answer is scoped to the brace:
689    ///   see [`braced_word_op_type`] for the measurement that decided
690    ///   it, and for why suppressing the *contents* of a value would
691    ///   have made it unbounded instead.
692    /// - It stops the score moving with the author's choice of
693    ///   delimiter, which is what #695, #1312 and #1314 each restored
694    ///   elsewhere: `puts {c d}` and `puts "c d"` now agree on the
695    ///   operator column.
696    ///
697    /// The cost is a script passed to an unlisted command — a Tk
698    /// `-command {…}` callback, `trace add variable v w {…}`, a
699    /// subcommand dispatch (`dict for`, `interp eval`), a user-defined
700    /// `with_lock {…}` — losing the `{}` its block deserves. No
701    /// structural signal distinguishes those cases: the grammar parses
702    /// `{a b}` and `{puts hi}` into the same shape. The code inside
703    /// them is still counted, so the loss is one operator occurrence
704    /// per such block, plus the vocabulary entry in the case above.
705    ///
706    /// [`braced_word_op_type`]: Self::braced_word_op_type
707    #[must_use]
708    fn is_value_braced_word<'a>(
709        word: &Node<'a>,
710        code: &[u8],
711        ancestors: Ancestors<'a, '_>,
712        kinds: &BracedWordKinds,
713    ) -> bool {
714        // Two positions answer "value" without consulting any command
715        // name, and both must be asked before the lookup below,
716        // because neither hangs off a `command` the way an argument
717        // does — and the second must not reach the `switch`-arm
718        // rescue.
719        //
720        // A defaulted `proc` parameter (`proc p {a {b {x y}}}`) is the
721        // one modelled slot holding a value: a default is data the
722        // interpreter assigns, never a script it evaluates.
723        //
724        // A braced word directly under a `command` is that command's
725        // *name* (`{puts} hi` invokes `puts`), so it is a literal word.
726        // Inside a `switch` arm list the name is the arm's *pattern*,
727        // and a braced pattern (`switch -regexp $v { {^a.*b$} {…} }`)
728        // is idiomatic — rescuing it as an arm body would fabricate a
729        // block around a regex.
730        if ancestors
731            .parent(word)
732            .is_some_and(|parent| [kinds.argument, kinds.command].contains(&parent.kind_id()))
733        {
734            return true;
735        }
736        let Some((command, command_ancestors)) =
737            Self::generic_argument_command(word, ancestors, kinds)
738        else {
739            return false;
740        };
741        if Self::command_leading_word(&command, code, kinds)
742            .and_then(script_slots)
743            .is_some()
744        {
745            return false;
746        }
747        !(Self::is_switch_arm(&command, code, command_ancestors, kinds)
748            && Self::is_switch_arm_body(word, &command))
749    }
750
751    /// Whether `node` is a Tcl-family braced *script* — a `proc` or `if`
752    /// body, an iRules `when` handler — rather than a braced literal
753    /// (#1381).
754    ///
755    /// The positive form of [`is_value_braced_word`], narrowed to the
756    /// script kind so it answers `false` for every node of every other
757    /// kind and for the two literal spellings, and narrowed again by the
758    /// value slots [`is_braced_literal_slot`] recognises. That makes it
759    /// the question the *non-Halstead* classifiers ask:
760    /// `Checker::is_string_with_code` must not call a `proc` body a
761    /// string literal, and `Alterator::keeps_children` must stop the dump
762    /// flattening one into a leaf, which dropped the body from it. Both
763    /// once listed `braced_word` beside `quoted_word` and
764    /// `braced_word_simple`, which is right for the literal role and
765    /// wrong for the script role the same kind also serves.
766    ///
767    /// Stated here rather than in each of the five call sites so the
768    /// string, dump and Halstead classifiers cannot drift apart on the
769    /// same bytes (grammar-dispatch §7). Halstead was the deliberate
770    /// exception between #1318 and #1382: `braced_word_op_type` asked
771    /// [`is_value_braced_word`] alone, so the braces of the value slots
772    /// [`is_braced_literal_slot`] adds still billed a `{}` operator
773    /// while the other two called the same bytes a literal. #1382
774    /// closed it — that call site now asks this predicate — so a braced
775    /// value is a value to all three, and `after {100} {puts hi}` bills
776    /// the one `{}` its `{puts hi}` block earns rather than two.
777    ///
778    /// The remaining §7 note is one of scope, not of disagreement: the
779    /// three differ only where Halstead's *operand* half keys on
780    /// whether a body holds a command, which makes an empty or
781    /// comment-only `proc` body an operand there and not a string here.
782    ///
783    /// [`is_value_braced_word`]: Self::is_value_braced_word
784    /// [`is_braced_literal_slot`]: Self::is_braced_literal_slot
785    #[must_use]
786    fn is_braced_script_word<'a>(
787        node: &Node<'a>,
788        code: &[u8],
789        ancestors: Ancestors<'a, '_>,
790        kinds: &BracedWordKinds,
791    ) -> bool {
792        node.kind_id() == kinds.script
793            && !Self::is_value_braced_word(node, code, ancestors, kinds)
794            && !Self::is_braced_literal_slot(node, code, ancestors, kinds)
795    }
796
797    /// Whether `word`, a braced word [`is_value_braced_word`] calls a
798    /// script, fills a slot whose documented syntax takes a *value*.
799    ///
800    /// [`is_value_braced_word`] answers by command name alone. Most of the
801    /// names it recognises have a mixed signature, though, so that answer
802    /// made a *value* — a millisecond count, a namespace name, a `switch`
803    /// subject — a script (#1381 review). This
804    /// is the per-argument half: `proc` has one value slot the grammar
805    /// names as a field, and every other construct declares its layout in
806    /// one of the two tables in `lang_helpers::tcl_family`, which
807    /// `fills_script_slot` applies.
808    ///
809    /// Each value here was a string and a flat dump leaf before #1381, and
810    /// would otherwise have become a script under it: the dump rendered
811    /// `{my proc}` as a command named `my` and `{100}` as one named `100`,
812    /// and `namespace export {…}` and `namespace ensemble create -map {…}`
813    /// both occur in the Tcl 8.6 standard library. Since #1382 Halstead
814    /// reads the same answer through [`is_braced_script_word`], so the
815    /// braces of these values quote rather than open (`after {100} {…}`
816    /// scores the `{}` of its script and no other).
817    ///
818    /// Two guards keep the construct-wide answer. A switch arm list parses
819    /// as commands, so an arm whose *pattern* is spelled `proc`,
820    /// `namespace`, `after` or `switch` builds one of these shapes around
821    /// what are really arm bodies — [`is_switch_arm`] recognises it first.
822    /// And an argument list whose own slot sequence is unreadable has no
823    /// positions worth trusting.
824    ///
825    /// That second guard is `tcl_family::argument_slots_are_readable`,
826    /// which asks the *slots* rather than the owner — asking
827    /// [`Node::has_error`] of the command withdrew every value slot of a
828    /// command whose only error sat inside one of its script arguments.
829    /// Its doc carries the measurement. A list *longer* than the
830    /// signature admits is the second way to be unreadable, and
831    /// `tcl_family::ScriptSlots::LastOf` is where that one lives.
832    ///
833    /// A `try … trap` clause is out of reach only where the grammar
834    /// wraps it: inside a `proc` or a `namespace eval` body the Tcl
835    /// grammar recovers into an `ERROR` node and no role signal
836    /// survives, but at the top level the same clause parses as a plain
837    /// `trap` command whose slots this does read. An earlier revision
838    /// of this paragraph said "the multi-line `try … trap` clause" was
839    /// out of reach outright, which is where the `finally` defect
840    /// `LastOf` now guards hid (#1382 review).
841    ///
842    /// [`Node::has_error`]: crate::Node::has_error
843    /// [`is_braced_script_word`]: Self::is_braced_script_word
844    /// [`is_value_braced_word`]: Self::is_value_braced_word
845    /// [`is_switch_arm`]: Self::is_switch_arm
846    #[must_use]
847    fn is_braced_literal_slot<'a>(
848        word: &Node<'a>,
849        code: &[u8],
850        ancestors: Ancestors<'a, '_>,
851        kinds: &BracedWordKinds,
852    ) -> bool {
853        let mut chain = ancestors.iter(word);
854        // A value slot is an *argument*, so it sits at least two levels
855        // below the root; neither grammar puts a braced word at the root
856        // or directly under it (a statement-position braced word is
857        // wrapped in a `command`, so `{a b}` alone on a line is still two
858        // deep). Both exits are therefore unreachable from a walk and are
859        // here because this is a `pub` trait method any node may be
860        // handed — `braced_slot_tests` calls it at both depths.
861        let Some((parent, above_parent)) = chain.next() else {
862            return false;
863        };
864        if parent.kind_id() == kinds.procedure {
865            let is_name =
866                matches!(parent.child_by_field_name("name"), Some(name) if name.id() == word.id());
867            return is_name && !Self::is_switch_arm(&parent, code, above_parent, kinds);
868        }
869        let Some((owner, above_owner)) = chain.next() else {
870            return false;
871        };
872        if parent.kind_id() != kinds.word_list
873            || !argument_slots_are_readable(&owner, &parent)
874            || Self::is_switch_arm(&owner, code, above_owner, kinds)
875        {
876            return false;
877        }
878        let dialect = Dialect { code, kinds };
879        // At both pinned grammars a `word_list` has exactly two possible
880        // owners — `command`'s `arguments` field and `namespace`'s child
881        // — so the third arm is unreachable and stays as the answer a
882        // grammar that grew a third owner should get: no layout, hence
883        // the construct-wide script answer.
884        // `a_word_list_is_owned_only_by_a_command_or_a_namespace` fails
885        // when that stops being true, which is the event this arm exists
886        // for.
887        let roles = if owner.kind_id() == kinds.namespace {
888            namespace_script_slots(&parent, dialect).map(|slots| ArgumentRoles { slots, first: 1 })
889        } else if owner.kind_id() == kinds.command {
890            Self::command_leading_word(&owner, code, kinds)
891                .and_then(script_slots)
892                .map(|slots| ArgumentRoles { slots, first: 0 })
893        } else {
894            None
895        };
896        roles.is_some_and(|roles| !fills_script_slot(word, &parent, roles, dialect))
897    }
898
899    /// Whether `word`, an argument of a `switch` arm command
900    /// ([`is_switch_arm`]), sits in a *body* position rather than a
901    /// *pattern* position.
902    ///
903    /// The arm list is a flat run of `pattern body pattern body …`,
904    /// and the grammar breaks it into commands at newlines. One arm per
905    /// line gives each command a pattern for its name and a body for
906    /// its sole argument; several arms on one line give the first
907    /// pattern's command the whole run as arguments, so the arguments
908    /// alternate `body pattern body …`. The command-name test above
909    /// keeps the *first* braced pattern a literal, but without this
910    /// gate every later one was rescued as a body, and a one-line
911    /// `switch -regexp $v { {^a} {…} {^b} {…} }` fabricated a `{}`
912    /// around `{^b}` that the same arms written one per line did not —
913    /// the score moving with layout, which is what the rule exists to
914    /// stop.
915    ///
916    /// Position is the only signal because it is what Tcl itself uses:
917    /// `switch` pairs the list up by index, with no marker on either
918    /// half. The parity holds across the words that can interpose — a
919    /// `-` fall-through body and a `default` pattern are both
920    /// `simple_word`s and take a slot each — so an even index is a body
921    /// and an odd one a pattern. This is the one place the rule needs a
922    /// sibling *index* rather than a parent kind, and it runs once per
923    /// braced argument of such a command.
924    ///
925    /// The index comes from [`Cursor::goto_first_child_for_byte`], which
926    /// is `O(log n)` in the argument count. A `children().position(..)`
927    /// scan answers identically in `O(n)`, and since every braced word
928    /// of a one-line arm list asks, that made the Halstead walk — and,
929    /// once #1381 routed them through this rule, `find` / `count
930    /// --type string` and the `Ast` dump — quadratic in the width of
931    /// one line: seconds per request at tens of KB (#1381 review). The
932    /// id check keeps the scan's answer for a word that is not one of
933    /// the command's arguments at all.
934    ///
935    /// [`is_switch_arm`]: Self::is_switch_arm
936    /// [`Cursor::goto_first_child_for_byte`]: crate::node::Cursor::goto_first_child_for_byte
937    #[must_use]
938    fn is_switch_arm_body(word: &Node<'_>, command: &Node<'_>) -> bool {
939        // `is_value_braced_word` asks only for a word it has already
940        // placed inside this command's `arguments`, so the field is
941        // always there on that path. It is not always there on the
942        // method's own contract — an argument-less `switch` is a
943        // `command` with a `name` and nothing else — and such a command
944        // has no body position at all.
945        let Some(arguments) = command.child_by_field_name("arguments") else {
946            return false;
947        };
948        let mut cursor = arguments.cursor();
949        let index = cursor.goto_first_child_for_byte(word.start_byte());
950        cursor.node().id() == word.id() && matches!(index, Some(index) if index.is_multiple_of(2))
951    }
952
953    /// A command's leading word, when it is a statically resolvable
954    /// `simple_word`.
955    ///
956    /// Located by field rather than by index (grammar-dispatch §3): the
957    /// name is `command`'s `name` field in both grammars. A computed
958    /// name (`$cmd {…}`, `[pick] {…}`) parses as a substitution node and
959    /// is not resolvable at all, and a name spelled in quotes or braces
960    /// (`"eval" {…}`) is legal Tcl that this deliberately leaves
961    /// unresolved — the same limitation `tcl_command_name` records for
962    /// the Cognitive and Cyclomatic walkers.
963    #[must_use]
964    fn command_leading_word<'c>(
965        command: &Node<'_>,
966        code: &'c [u8],
967        kinds: &BracedWordKinds,
968    ) -> Option<&'c str> {
969        let name = command.child_by_field_name("name")?;
970        if name.kind_id() != kinds.simple_word {
971            return None;
972        }
973        let text = node_text(code, &name)?;
974        // `::eval` *is* `eval`, and without the strip the qualified form
975        // fell to the value default and lost its block. The rule and the
976        // reason it stops at the *leading* qualifier live with the
977        // helper, which the four metrics that resolve a leading word
978        // without this table share (#1381 review).
979        Some(crate::lang_helpers::strip_global_qualifier(text))
980    }
981
982    /// Whether `command` is really one `pattern body` pair of a Tcl
983    /// `switch` arm list rather than a command of its own.
984    ///
985    /// Tcl models no `switch`, so `switch $v {a {…} b {…}}` parses the
986    /// arm list as a braced word whose interior is a `command` named
987    /// after the *first pattern* — `a` here — with the arm bodies as
988    /// its arguments (grammar-dispatch §9; the Cognitive walker reads
989    /// the same shape through `tcl_switch_arm_list`). Without this
990    /// test the bodies would read as literals passed to a command
991    /// called `a`, which is exactly what the value default is for
992    /// everywhere else. iRules models `switch` with `switch_arm`
993    /// children, so its arm bodies never reach here.
994    #[must_use]
995    fn is_switch_arm<'a>(
996        command: &Node<'a>,
997        code: &[u8],
998        ancestors: Ancestors<'a, '_>,
999        kinds: &BracedWordKinds,
1000    ) -> bool {
1001        ancestors
1002            .iter(command)
1003            .next()
1004            .is_some_and(|(arm_list, above)| {
1005                arm_list.kind_id() == kinds.script
1006                    && Self::generic_argument_command(&arm_list, above, kinds)
1007                        .and_then(|(switch, _)| Self::command_leading_word(&switch, code, kinds))
1008                        == Some(SWITCH_COMMAND)
1009            })
1010    }
1011
1012    /// Whether the `{` of `node`'s parent braced word opens a block,
1013    /// or merely quotes a literal (#1318).
1014    ///
1015    /// This is #1314's guard with its kind test replaced by a role
1016    /// test. #1314 suppressed the opener of a `braced_word_simple`,
1017    /// the literal form the grammars emit only in the value slots they
1018    /// special-case; everywhere else a literal is a `braced_word`, the
1019    /// same kind a block uses, and its `{` still reported a `{}`
1020    /// operator for a block the source does not contain —
1021    /// `lappend x {a b}`, `puts {c d}`, and every user proc taking a
1022    /// list.
1023    ///
1024    /// The role is [`is_braced_script_word`], the same predicate
1025    /// `Checker::is_string_with_code` and `Alterator::keeps_children`
1026    /// ask, so the three classifiers answer one question about one set
1027    /// of bytes (grammar-dispatch §7). That was not so between #1318
1028    /// and #1382: this call site asked [`is_value_braced_word`] alone
1029    /// and billed a `{}` for the value slots of a script-taking
1030    /// command, which the other two had called literals since #1381.
1031    /// The exception, and what closing it moved, is recorded on
1032    /// [`is_braced_script_word`], which owns the §7 story for all five
1033    /// call sites.
1034    ///
1035    /// **It revises the operator only, and deliberately leaves the
1036    /// words inside a value alone.** Suppressing them too would make
1037    /// `lappend x {a b}` score like its `set x {a b}` synonym, which
1038    /// is the tidier answer for that line — and the wrong one in
1039    /// general, because the same braced argument of an unrecognised
1040    /// command is just as often real code: an `oo::class create C {…}`
1041    /// body, a `tcltest` `-body {…}`, an `apply {{x} {…}}` lambda.
1042    /// Suppressing contents was built and measured on a file holding
1043    /// one of each; it took n1 5 → 2, N1 8 → 2, n2 25 → 15 and
1044    /// N2 32 → 15, the class body, the test body and the lambda each
1045    /// collapsing into a single operand, and `halstead.effort` — a
1046    /// gated threshold metric — collapsing with them. So the rule only
1047    /// ever withdraws a claim the classifier cannot support; it never
1048    /// discards code the walk has already read.
1049    ///
1050    /// The residual is the asymmetry #1382 opens with, and it is the
1051    /// **contract**, not an outstanding defect: a braced value scores
1052    /// one operand where the grammar names the command (`set x {a b}`)
1053    /// and one per word where only the command name would
1054    /// (`lappend x {a b}`). #1382 weighed the two ways of closing it
1055    /// and took neither. Reading the *contents* — no substitution, no
1056    /// nested block, therefore list-like — is the measurement above,
1057    /// already taken and already rejected. Widening the one-operand
1058    /// treatment to a *value*-taking command list needs the mirror of
1059    /// `SCRIPT_TAKING_COMMANDS`, and that set is open where this one is
1060    /// closed: every user proc is in it, so the list is incomplete by
1061    /// construction and would leave three behaviours where there are
1062    /// two. Both trade a bounded over-report of the operand vocabulary
1063    /// for an unbounded loss of real code, which is the trade the
1064    /// paragraph above declines.
1065    ///
1066    /// Keeping to the operator also keeps the whole thing cheap: only
1067    /// a braced word's own opener can change answer, so the test is one
1068    /// kind comparison and one parent lookup, the same scope #1354 and
1069    /// #1314 use. Two lookups are `O(log n)` rather than `O(1)`, both
1070    /// cursor seeks by byte and both reached only from a *command whose
1071    /// name resolves*: [`is_switch_arm_body`] needs a `switch` arm
1072    /// word's index among its command's arguments, and — since #1382 —
1073    /// [`is_braced_literal_slot`] needs the same index to place a word
1074    /// against the layout `lang_helpers::tcl_family` records. An
1075    /// ancestor scan would have been `O(depth)` per node and quadratic
1076    /// on a deeply nested `expr`, the shape #1122 warns about.
1077    ///
1078    /// [`is_braced_literal_slot`]: Self::is_braced_literal_slot
1079    /// [`is_braced_script_word`]: Self::is_braced_script_word
1080    /// [`is_switch_arm_body`]: Self::is_switch_arm_body
1081    /// [`is_value_braced_word`]: Self::is_value_braced_word
1082    ///
1083    /// [`get_op_type`]: Self::get_op_type
1084    #[must_use]
1085    fn braced_word_op_type<'a>(
1086        node: &Node<'a>,
1087        code: &[u8],
1088        ancestors: Ancestors<'a, '_>,
1089        kinds: &BracedWordKinds,
1090    ) -> TokenRole {
1091        let base = Self::get_op_type(node, ancestors);
1092        // Only the opener can change answer, and the test has to say so
1093        // rather than infer it from the parent kind. A braced word's
1094        // operator children are the `{` *and* every `;` separating two
1095        // of its commands — `_terminator` is a hidden rule, so the
1096        // separator is inlined as a direct child of the `braced_word`
1097        // and both dialects list `SEMI` as an operator. The closer is
1098        // unclassified and a `\n` terminator is not an operator, so
1099        // those two are the whole set.
1100        if node.kind_id() != kinds.open_brace || !matches!(base, TokenRole::Operator) {
1101            return base;
1102        }
1103        let quotes_a_literal = ancestors.iter(node).next().is_some_and(|(parent, above)| {
1104            parent.kind_id() == kinds.script
1105                && !Self::is_braced_script_word(&parent, code, above, kinds)
1106        });
1107        if quotes_a_literal {
1108            TokenRole::Unknown
1109        } else {
1110            base
1111        }
1112    }
1113
1114    /// The grammar's name for the operator kind `id`, used to render a
1115    /// Halstead operator; empty for languages that do not report them.
1116    #[must_use]
1117    fn get_operator_id_as_str(_id: u16) -> &'static str {
1118        ""
1119    }
1120}
1121
1122mod bash;
1123mod c;
1124mod ccomment;
1125mod cpp;
1126mod csharp;
1127mod elixir;
1128mod go;
1129mod groovy;
1130mod irules;
1131mod java;
1132mod javascript;
1133mod kotlin;
1134mod lua;
1135mod mozcpp;
1136mod mozjs;
1137mod objc;
1138mod perl;
1139mod php;
1140mod preproc;
1141mod python;
1142mod ruby;
1143mod rust;
1144mod tcl;
1145mod tsx;
1146mod typescript;
1147
1148#[cfg(test)]
1149mod node_text_tests {
1150    use super::node_text;
1151    use crate::langs::RustParser;
1152    use crate::traits::ParserTrait;
1153    use std::path::PathBuf;
1154
1155    /// A node whose span lies inside the buffer it was parsed from
1156    /// yields its exact source text.
1157    #[cfg(feature = "rust")]
1158    #[test]
1159    fn in_bounds_span_returns_text() {
1160        let src = "fn x() {}";
1161        let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
1162        let root = parser.root();
1163        assert_eq!(node_text(parser.code(), &root), Some(src));
1164    }
1165
1166    /// Reslicing a node against a *shorter* buffer (the stale-span hazard
1167    /// the guard exists for) must degrade to `None`, not panic. A direct
1168    /// `&code[start..end]` would panic here — this is the revert check.
1169    #[cfg(feature = "rust")]
1170    #[test]
1171    fn out_of_bounds_span_returns_none_not_panic() {
1172        let src = "fn x() {}";
1173        let parser = RustParser::new(src.as_bytes().to_vec(), &PathBuf::from("t.rs"), None);
1174        let root = parser.root();
1175        assert!(root.end_byte() > 2);
1176        let truncated = &src.as_bytes()[..2];
1177        assert_eq!(node_text(truncated, &root), None);
1178    }
1179
1180    /// The UTF-8 guard is the *other* failure mode, and unlike the range
1181    /// guard it is reachable without violating the same-parse
1182    /// precondition: `Ast::parse` accepts arbitrary bytes. A span whose
1183    /// bytes are not valid UTF-8 must yield `None`, not a panic and not
1184    /// lossy replacement characters.
1185    #[cfg(feature = "rust")]
1186    #[test]
1187    fn non_utf8_span_returns_none() {
1188        let mut src = b"fn ".to_vec();
1189        src.extend_from_slice(&[0xF0, 0x9F]);
1190        src.extend_from_slice(b"() {}");
1191        let parser = RustParser::new(src.clone(), &PathBuf::from("t.rs"), None);
1192        let root = parser.root();
1193        assert_eq!(root.end_byte(), src.len());
1194        assert_eq!(node_text(parser.code(), &root), None);
1195    }
1196}
1197
1198#[cfg(test)]
1199mod ancestor_tests {
1200    use super::Getter;
1201    use crate::node::{Ancestors, Node};
1202    use crate::test_support::for_each_node_with_chain;
1203    use crate::traits::LanguageInfo;
1204
1205    /// `get_func_space_name` must name a space the same whether it reads
1206    /// the walker's ancestor chain or climbs with `Node::parent`.
1207    ///
1208    /// Two grammars consult an ancestor here, for different reasons:
1209    /// the JS family names an anonymous `function` / arrow from the
1210    /// `pair` or `variable_declarator` holding it, and Elixir skips
1211    /// naming a `def` that sits inside a `quote` template. #1088 moved
1212    /// both onto the chain.
1213    #[cfg(any(
1214        feature = "elixir",
1215        feature = "javascript",
1216        feature = "mozjs",
1217        feature = "typescript",
1218    ))]
1219    fn assert_name_parity<L: LanguageInfo + Getter>(
1220        label: &str,
1221        code: &[u8],
1222        expect_named: &[&str],
1223    ) {
1224        let mut seen: Vec<&str> = Vec::new();
1225        let visited = for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
1226            let known = L::get_func_space_name(node, code, Ancestors::known(chain));
1227            let climbing = L::get_func_space_name(node, code, Ancestors::unknown());
1228            assert_eq!(
1229                known,
1230                climbing,
1231                "{label}: name of {} at row {} disagrees",
1232                node.kind(),
1233                node.start_row()
1234            );
1235            if let Some(name) = known
1236                && expect_named.contains(&name)
1237                && !seen.contains(&name)
1238            {
1239                seen.push(name);
1240            }
1241        });
1242        assert!(visited > 20, "{label}: fixture is too small to prove much");
1243        for name in expect_named {
1244            assert!(
1245                seen.contains(name),
1246                "{label}: no node resolved to {name:?}, so the fixture no longer \
1247                 exercises the ancestor-derived naming it was added for"
1248            );
1249        }
1250    }
1251
1252    #[cfg(all(
1253        feature = "elixir",
1254        feature = "javascript",
1255        feature = "mozjs",
1256        feature = "typescript",
1257    ))]
1258    #[test]
1259    fn func_space_name_agrees_between_known_and_climbing() {
1260        // `outer` and `keyed` are only reachable through the parent:
1261        // the function expressions themselves carry no `name` field.
1262        //
1263        // All four JS-family grammars are exercised, not just
1264        // JavaScript: their `get_func_space_name` impls are separate
1265        // copies of the same body against four distinct `kind_id`
1266        // enums, so a `Pair` / `VariableDeclarator` id that drifted in
1267        // one grammar would be invisible here if only one were checked.
1268        let js_source =
1269            b"var outer = function () { return 1; };\nvar o = { keyed: function () { return 2; } };\n";
1270        assert_name_parity::<crate::langs::JavascriptCode>(
1271            "javascript",
1272            js_source,
1273            &["outer", "keyed"],
1274        );
1275        assert_name_parity::<crate::langs::MozjsCode>("mozjs", js_source, &["outer", "keyed"]);
1276        assert_name_parity::<crate::langs::TypescriptCode>(
1277            "typescript",
1278            js_source,
1279            &["outer", "keyed"],
1280        );
1281        assert_name_parity::<crate::langs::TsxCode>("tsx", js_source, &["outer", "keyed"]);
1282        // `multi` is named from its `Call` head; the `def a` inside the
1283        // `quote` template is not a definition, so it falls through to
1284        // the field-less default.
1285        assert_name_parity::<crate::langs::ElixirCode>(
1286            "elixir",
1287            b"defmodule Foo do\n  defmacro multi do\n    quote do\n      def a, do: 1\n    end\n  end\nend\n",
1288            &["Foo", "multi"],
1289        );
1290    }
1291}
1292
1293/// The `is_braced_literal_slot` / `is_switch_arm_body` guards that no
1294/// Tcl-family walk can reach, and the grammar shape each rests on.
1295#[cfg(test)]
1296#[cfg(any(feature = "tcl", feature = "irules"))]
1297mod braced_slot_tests {
1298    use super::{BracedWordKinds, Getter};
1299    // Gated with the rows that name it: `Tcl` is read only by the
1300    // `feature = "tcl"` helpers below, so the module's wider
1301    // `any(tcl, irules)` gate would leave this unused — and `ci.yml` sets
1302    // `RUSTFLAGS: "-D warnings"`, which turns that into a hard failure on
1303    // an `irules`-without-`tcl` build.
1304    #[cfg(feature = "tcl")]
1305    use crate::Tcl;
1306    use crate::node::{Ancestors, Node};
1307    use crate::test_support::for_each_node_with_chain;
1308    use crate::traits::LanguageInfo;
1309
1310    /// No node shallower than an argument can fill a value slot.
1311    ///
1312    /// A value slot is an argument, and both grammars hang an argument at
1313    /// least two levels below the root — under a `word_list` under a
1314    /// `command`, or under a construct's own node. A statement-position
1315    /// braced word is no exception: `{a b}` alone on a line is a `command`
1316    /// wrapping the braced word, never the braced word itself. So the root
1317    /// and its direct children are exactly the depths
1318    /// `is_braced_literal_slot`'s two chain exits answer for, and no walk
1319    /// reaches either.
1320    ///
1321    /// They are still the method's contract — it is `pub`, takes any node,
1322    /// and must not read a slot into something with no argument list above
1323    /// it. Flipping either exit to `true` makes this fail.
1324    fn assert_no_shallow_value_slot<L: LanguageInfo + Getter>(
1325        label: &str,
1326        code: &[u8],
1327        kinds: &BracedWordKinds,
1328    ) {
1329        let (mut roots, mut children) = (0, 0);
1330        for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
1331            if chain.len() > 1 {
1332                return;
1333            }
1334            for ancestors in [Ancestors::known(chain), Ancestors::unknown()] {
1335                assert!(
1336                    !L::is_braced_literal_slot(node, code, ancestors, kinds),
1337                    "{label}: a {} at depth {} filled a value slot",
1338                    node.kind(),
1339                    chain.len()
1340                );
1341            }
1342            if chain.is_empty() {
1343                roots += 1;
1344            } else {
1345                children += 1;
1346            }
1347        });
1348        assert_eq!(roots, 1, "{label}: exactly one root");
1349        assert!(
1350            children > 1,
1351            "{label}: the fixture must put several nodes directly under the \
1352             root, or the depth-1 exit goes unexercised"
1353        );
1354    }
1355
1356    #[test]
1357    #[cfg(feature = "tcl")]
1358    fn tcl_no_node_above_an_argument_fills_a_value_slot() {
1359        assert_no_shallow_value_slot::<crate::langs::TclCode>(
1360            "tcl",
1361            b"{a b}\nproc {my proc} {} {}\nnamespace export {c d}\n",
1362            &crate::lang_helpers::tcl::BRACED_WORD_KINDS,
1363        );
1364    }
1365
1366    #[test]
1367    #[cfg(feature = "irules")]
1368    fn irules_no_node_above_an_argument_fills_a_value_slot() {
1369        assert_no_shallow_value_slot::<crate::langs::IrulesCode>(
1370            "irules",
1371            b"{a b}\nproc {my proc} {} {}\nnamespace export {c d}\n",
1372            &crate::lang_helpers::irules::BRACED_WORD_KINDS,
1373        );
1374    }
1375
1376    /// Every braced word of `code` that fills a value slot, in source
1377    /// order. Tcl only: `is_braced_literal_slot` is a provided method, so
1378    /// both dialects run the identical body over their own kind table.
1379    ///
1380    /// Walks with [`Ancestors::unknown`] rather than through
1381    /// `for_each_node_with_chain`, whose fixtures must parse cleanly so a
1382    /// walk cannot cover error recovery by accident — which is exactly
1383    /// what the callers below are about. The two spellings answer
1384    /// identically here; `assert_no_shallow_value_slot` above asserts
1385    /// that on every node it visits.
1386    #[cfg(feature = "tcl")]
1387    fn value_slot_texts(code: &[u8]) -> Vec<String> {
1388        let tree = crate::node::Tree::new::<crate::langs::TclCode>(code);
1389        tree.get_root()
1390            .preorder()
1391            .filter(|node| {
1392                <crate::langs::TclCode as Getter>::is_braced_literal_slot(
1393                    node,
1394                    code,
1395                    Ancestors::unknown(),
1396                    &crate::lang_helpers::tcl::BRACED_WORD_KINDS,
1397                )
1398            })
1399            .map(|node| node.utf8_text(code).unwrap_or_default().to_owned())
1400            .collect()
1401    }
1402
1403    /// A parse error nested inside one argument's *script* leaves its
1404    /// sibling *value* slots alone (#1381 review).
1405    ///
1406    /// `Node::has_error` is transitive, so asking it of the command
1407    /// withdrew the rescue from every argument: `trap {pat} {v} {puts ]}`
1408    /// lost both value slots, and the dump then rendered each as a
1409    /// fabricated `command` named after its own text. What decides
1410    /// whether the positions are readable is the argument list's own slot
1411    /// sequence, and an error *inside* an argument moves no sibling's
1412    /// index. Verified by reverting the guard to `owner.has_error()`:
1413    /// the error row then comes back empty and this fails.
1414    ///
1415    /// Asserted as the *same* list for both fixtures rather than as a
1416    /// non-empty one for the error row. The well-formed row is what pins
1417    /// that the fixture still spells two value slots at all, so trimming
1418    /// one out fails here instead of quietly reducing the claim to
1419    /// "nothing changed".
1420    #[test]
1421    #[cfg(feature = "tcl")]
1422    fn an_error_inside_a_script_argument_keeps_the_sibling_value_slots() {
1423        let well_formed = value_slot_texts(b"trap {pat} {v} {puts j}\n");
1424        assert_eq!(
1425            well_formed,
1426            ["{pat}", "{v}"],
1427            "the fixture must spell two value slots before an error is added"
1428        );
1429        assert_eq!(
1430            value_slot_texts(b"trap {pat} {v} {puts ]}\n"),
1431            well_formed,
1432            "an error inside the script argument moves no sibling's index"
1433        );
1434    }
1435
1436    /// The other half of the same rule: an `ERROR` token occupying a slot
1437    /// of the argument list *does* shift every argument after it, so the
1438    /// construct-wide answer is the right one there.
1439    ///
1440    /// `on code varList script` reads its slots by position, and the
1441    /// stray `]` takes one of its own — so `{code}` is no longer
1442    /// followed by the variable list the signature puts there and the
1443    /// layout cannot be trusted. Without this row the guard could be
1444    /// deleted outright and the test above would still pass.
1445    ///
1446    /// The fixture keeps the clause's three arguments deliberately: the
1447    /// `]` replaces the variable list rather than joining it, so
1448    /// `ScriptSlots::LastOf`'s arity rule still admits the list and
1449    /// `argument_slots_are_readable` is the only guard that can reject
1450    /// it. A four-argument spelling would be rejected twice over and
1451    /// this row would stop naming which guard it tests (#1382 review).
1452    ///
1453    /// The well-formed row is what keeps the empty one from passing
1454    /// vacuously: `on` reaching the slot table at all is what makes the
1455    /// error row's emptiness mean something, and dropping that row from
1456    /// `SCRIPT_TAKING_COMMANDS` empties both. The siblings either side
1457    /// of this test anchor the same way.
1458    #[test]
1459    #[cfg(feature = "tcl")]
1460    fn an_error_occupying_a_slot_withdraws_the_layout() {
1461        assert_eq!(
1462            value_slot_texts(b"on {code} {v} {puts j}\n"),
1463            ["{code}", "{v}"],
1464            "the fixture must spell two value slots before the error \
1465             takes one of their places"
1466        );
1467        assert!(
1468            value_slot_texts(b"on {code} ] {puts j}\n").is_empty(),
1469            "an ERROR token in the argument list makes every position \
1470             unreadable"
1471        );
1472    }
1473
1474    /// The third way a layout stops being readable: the grammar groups
1475    /// a *following* clause into this one, so the list is longer than
1476    /// the signature admits (#1382 review).
1477    ///
1478    /// Tcl models neither `on` nor `trap` outside a `try`, so a
1479    /// top-level `try … trap … finally` parses the whole tail as one
1480    /// generic `trap` command — `p v b finally c`. Reading "the script
1481    /// is the last argument" off that names the `finally` body and
1482    /// calls the handler body a value, which withdraws the `{}` of a
1483    /// real block and flattens it out of the dump. Both rows run the
1484    /// same clause, so the second cannot pass by the fixture having
1485    /// stopped spelling a value slot at all.
1486    #[test]
1487    #[cfg(feature = "tcl")]
1488    fn a_handler_clause_of_the_wrong_arity_withdraws_the_layout() {
1489        assert_eq!(
1490            value_slot_texts(b"try {puts a} trap {p} {v} {puts b}\n"),
1491            ["{p}", "{v}"],
1492            "a three-argument `trap` clause spells two value slots"
1493        );
1494        assert!(
1495            value_slot_texts(b"try {puts a} trap {p} {v} {puts b} finally {puts c}\n").is_empty(),
1496            "a trailing `finally` joins the same generic `trap` command, \
1497             so no position in its argument list is trustworthy"
1498        );
1499    }
1500
1501    /// A command with no argument list has no `switch`-arm body position.
1502    ///
1503    /// `is_value_braced_word` only ever asks about a word it has already
1504    /// placed inside the command's `arguments`, so that field is always
1505    /// present on the walk's path. It is absent for an argument-less
1506    /// command — `switch` on a line by itself is a `command` carrying only
1507    /// its `name` — and the method has to answer for one, because a
1508    /// bodiless command has no even-indexed slot to be in.
1509    ///
1510    /// The second half is the same method's positive answer on a real arm
1511    /// list, so the first cannot be satisfied by a constant.
1512    #[test]
1513    #[cfg(feature = "tcl")]
1514    fn a_command_without_arguments_has_no_switch_arm_body() {
1515        use crate::node::Tree;
1516
1517        let code = b"switch\nswitch $v { p {puts a} }\n";
1518        let tree = Tree::new::<crate::langs::TclCode>(code);
1519        let root = tree.get_root();
1520        let bare = root.child(0).expect("the bare `switch` command");
1521        let name = bare
1522            .child_by_field_name("name")
1523            .expect("even a bare command names itself");
1524        assert!(
1525            bare.child_by_field_name("arguments").is_none(),
1526            "the fixture's first command must carry no argument list"
1527        );
1528        assert!(
1529            !<crate::langs::TclCode as Getter>::is_switch_arm_body(&name, &bare),
1530            "a command with no argument list has no body position"
1531        );
1532
1533        let arm = find_arm_named_p(&root, code).expect("the arm command named `p`");
1534        let body = arm
1535            .child_by_field_name("arguments")
1536            .and_then(|args| args.child(0))
1537            .expect("the arm's body");
1538        assert!(
1539            <crate::langs::TclCode as Getter>::is_switch_arm_body(&body, &arm),
1540            "the arm's sole argument is its body, or the assertion above is \
1541             satisfied by a constant"
1542        );
1543    }
1544
1545    /// The `command` a Tcl `switch` arm list spells its first pattern as.
1546    #[cfg(feature = "tcl")]
1547    fn find_arm_named_p<'a>(node: &Node<'a>, code: &[u8]) -> Option<Node<'a>> {
1548        let named_p = node.kind_id() == Tcl::Command as u16
1549            && node
1550                .child_by_field_name("name")
1551                .is_some_and(|name| name.utf8_text(code) == Some("p"));
1552        if named_p {
1553            return Some(*node);
1554        }
1555        node.children()
1556            .find_map(|child| find_arm_named_p(&child, code))
1557    }
1558
1559    /// Only a `command` and a `namespace` own a `word_list`, which is what
1560    /// makes `is_braced_literal_slot`'s third owner arm unreachable.
1561    ///
1562    /// Checked against both pinned grammars' `node-types.json` when that
1563    /// arm was written; this is the executable half, so a grammar bump
1564    /// handing a `word_list` to a third construct fails here rather than
1565    /// silently routing it to the no-layout answer.
1566    fn assert_word_list_owners<L: LanguageInfo>(label: &str, code: &[u8], kinds: &BracedWordKinds) {
1567        let mut seen = 0;
1568        for_each_node_with_chain::<L>(code, |node: &Node<'_>, chain| {
1569            if node.kind_id() != kinds.word_list {
1570                return;
1571            }
1572            let owner = chain.last().expect("a word_list is never the root");
1573            assert!(
1574                [kinds.command, kinds.namespace].contains(&owner.kind_id()),
1575                "{label}: a word_list owned by {}",
1576                owner.kind()
1577            );
1578            seen += 1;
1579        });
1580        assert!(seen > 2, "{label}: fixture reached only {seen} word_lists");
1581    }
1582
1583    /// Both owners plus the modelled constructs either grammar spells, so
1584    /// a new owner among them is what the assertion catches.
1585    const WORD_LIST_OWNER_FIXTURE: &[u8] = b"proc p {x} { puts $x }\n\
1586        namespace eval ns { puts hi }\n\
1587        namespace export {a b}\n\
1588        if {$x} { puts a } else { puts b }\n\
1589        while {$x} { puts a }\n\
1590        foreach i {1 2} { puts $i }\n\
1591        catch { puts a } msg\n\
1592        eval {puts hi}\n\
1593        switch $v { p {puts a} }\n\
1594        lappend l {c d}\n";
1595
1596    #[test]
1597    #[cfg(feature = "tcl")]
1598    fn tcl_a_word_list_is_owned_only_by_a_command_or_a_namespace() {
1599        assert_word_list_owners::<crate::langs::TclCode>(
1600            "tcl",
1601            WORD_LIST_OWNER_FIXTURE,
1602            &crate::lang_helpers::tcl::BRACED_WORD_KINDS,
1603        );
1604    }
1605
1606    #[test]
1607    #[cfg(feature = "irules")]
1608    fn irules_a_word_list_is_owned_only_by_a_command_or_a_namespace() {
1609        assert_word_list_owners::<crate::langs::IrulesCode>(
1610            "irules",
1611            WORD_LIST_OWNER_FIXTURE,
1612            &crate::lang_helpers::irules::BRACED_WORD_KINDS,
1613        );
1614    }
1615}