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}