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}