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