Skip to main content

big_code_analysis_ast/getter/
tcl.rs

1//! `Getter` implementation for Tcl.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6use crate::lang_helpers::tcl::BRACED_WORD_KINDS;
7
8impl Getter for TclCode {
9    fn get_space_kind(node: &Node) -> SpaceKind {
10        match node.kind_id().into() {
11            Tcl::Procedure => SpaceKind::Function,
12            Tcl::SourceFile => SpaceKind::Unit,
13            _ => SpaceKind::Unknown,
14        }
15    }
16
17    fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
18        match node.kind_id().into() {
19            // The braced-word rule (#1354 / #1317), whose two halves
20            // `Getter::is_subsumed_braced_word` states once for both
21            // dialects. This arm is only its Tcl instantiation, plus
22            // the derivation and the caveats.
23            //
24            // A braced *word* is one literal value: Tcl evaluates
25            // nothing between the braces, so `{$x}` is the
26            // two-character string `$x` and not a variable reference.
27            // The grammar models the interior structurally all the
28            // same — `braced_word_simple` (89) admits `simple_word`,
29            // `escaped_character`, `quoted_word`,
30            // `variable_substitution`, `command_substitution` and a
31            // nested `braced_word_simple` — so the word and every part
32            // of it were billed together: `set x {literal here}` scored
33            // n2 4 / N2 4 for one value, and `set a {braced word}`
34            // reported four operands where its synonym
35            // `set a "quoted word"` reported two.
36            //
37            // The wrapper keeps the operand and its children are
38            // suppressed — the opposite polarity from `QuotedWord`
39            // below, and for the reason that decides it: a quoted word
40            // *does* interpolate, so its `$var` children are real
41            // references the walk has to count, while a braced word's
42            // are letters. Billing the whole word is also what keeps
43            // the two spellings of one literal scoring alike, the
44            // spelling insensitivity #695, #1312 and #1314 each
45            // restored elsewhere — and it is the contract the book
46            // already documents: "a Tcl braced word … contributes *one
47            // operand* — the literal" (`metrics.md`, Halstead).
48            //
49            // The value half is keyed on the parent alone rather than
50            // on a list of child kinds (the #1355 shape): the invariant
51            // is positional — a wrapper's span already contains its
52            // children's — and an enumerated list would be a coverage
53            // claim to re-derive on every grammar bump.
54            // `tcl_braced_word_bills_its_content_once_1354` witnesses all six
55            // child kinds and fails on a seventh.
56            //
57            // It subsumes #1314's narrower `LBRACE` guard: that `{` is
58            // one more child of the word, and reporting it as a `{}`
59            // operator fabricated a block the source does not contain.
60            // A *script* is a `BracedWord` (88) — verified across
61            // `proc`, `if`, `catch`, `eval`, `after` and `uplevel` — an
62            // `if`/`while` condition is an `Expr` (97), and a `proc`
63            // parameter list is `Arguments` (94). All three keep their
64            // braces as operators, which is what makes keying the value
65            // half on `BracedWordSimple` safe. (#1318 later carved one
66            // exception out of that list from the other side: a
67            // *defaulted parameter's* value is a `BracedWord` under an
68            // `Argument` (93), and a default is data, so it loses its
69            // brace — the enclosing `Arguments` list keeps its own.)
70            //
71            // Two limits of the value half, both deliberate and both
72            // pinned rather than left implied.
73            //
74            // It is *parent*-scoped, so "Tcl evaluates nothing between
75            // the braces" holds for what sits directly inside the word
76            // and not for what sits deeper: the grammar parses a `[…]`
77            // command substitution and a `"…"` quoted word inside a
78            // braced word, and the operands *they* contain still count
79            // (`set a {x "$q" v}` bills `$q`). Widening to an ancestor
80            // scan would also swallow the `{` of every `Expr` and
81            // `BracedWord` nested under a value word, which is the
82            // fabrication #1314 removed in the other direction —
83            // `tcl_braced_word_guard_is_parent_scoped_not_ancestor_scoped`
84            // pins the operators, `tcl_braced_word_bills_its_content_once_1354`
85            // the surviving operands. The residual is the same one
86            // #1314 recorded: the grammar models a substitution Tcl
87            // will not perform, and what the classifier sees is what
88            // the metric reports.
89            //
90            // And the grammar emits `BracedWordSimple` only in the
91            // value slot of the commands it special-cases, so
92            // `lappend x {a b}` parses its literal as a `BracedWord`
93            // script and its `{` kept fabricating a block. Guarding
94            // that kind here too would drop every real block, so #1318
95            // closed it out-of-band instead, by the enclosing command's
96            // leading word (grammar-dispatch §9) — in
97            // `get_op_type_with_code` below, because the recognition
98            // needs the source bytes. It touches the brace and nothing
99            // else, so every operand this arm decides is unchanged.
100            //
101            // Cross-walked against the sibling predicates
102            // (grammar-dispatch §7): `Checker::is_string` and
103            // `Alterator::alterate` both list `BracedWord` beside the two
104            // literal forms, and #1381 narrows both for a script body —
105            // `Checker::is_string_with_code` and
106            // `Alterator::keeps_children` ask
107            // `Getter::is_braced_script_word` — so `bca find --type
108            // string` reports `lappend x {a b}`'s `{a b}` and not a
109            // `proc` body, agreeing with this arm. Since #1382 the two
110            // also agree on the value slots of a script-taking command
111            // (`proc {my proc}`, `after {100} {…}`,
112            // `namespace export {…}`): `get_op_type_with_code` asks the
113            // same `Getter::is_braced_script_word` and no longer bills
114            // their `{` as a block. Two disagreements remain, each
115            // recorded where it lives: an interpolating `QuotedWord` is
116            // `Unknown` here and a string there, which predates #1354;
117            // and an empty or comment-only script body is an operand
118            // here and not a string there.
119            //
120            // `Checker::is_call` needs no such follow-up. It calls
121            // every `Command` a call, including the ones inside a value
122            // braced word — and so does this arm, which still bills
123            // their operands. The two agree precisely because #1318
124            // withdrew the brace and not the contents; a rule that
125            // suppressed the contents would have put the getter and
126            // every branching metric on opposite sides of the same
127            // bytes.
128            _ if Self::is_subsumed_braced_word(node, ancestors, &BRACED_WORD_KINDS) => {
129                TokenRole::Unknown
130            }
131            // Anonymous keyword tokens (control-flow and declaration keywords).
132            Tcl::Proc
133            | Tcl::If2
134            | Tcl::Elseif2
135            | Tcl::Else2
136            | Tcl::While2
137            | Tcl::Foreach2
138            | Tcl::Set2
139            | Tcl::Global2
140            | Tcl::Namespace2
141            | Tcl::Try2
142            | Tcl::Catch2
143            | Tcl::Finally2
144            | Tcl::Regexp2
145            | Tcl::Expr2
146            // String comparison operators.
147            | Tcl::Eq
148            | Tcl::Ne
149            | Tcl::In
150            | Tcl::Ni
151            // Structural punctuation. Only the *opening* delimiter is an
152            // operator (the pair folds to one glyph in
153            // `get_operator_id_as_str`); counting the matching closer too
154            // (former `RBRACE`/`RBRACK`/`RPAREN` arms) double-counted every
155            // balanced pair, inflating n1/N1 (#695).
156            // `LPAREN2` is defensive — the runtime collapses it to `LPAREN`
157            // before `kind_id()`, so it never fires (#768; see the Cpp note).
158            | Tcl::LBRACE
159            | Tcl::LBRACK
160            | Tcl::LPAREN
161            | Tcl::LPAREN2
162            | Tcl::SEMI
163            | Tcl::COLON
164            | Tcl::COLONCOLON
165            | Tcl::COLONCOLON2
166            // Arithmetic / exponent operators.
167            | Tcl::PLUS
168            | Tcl::DASH
169            | Tcl::STAR
170            | Tcl::SLASH
171            | Tcl::PERCENT
172            | Tcl::STARSTAR
173            // Bitwise operators.
174            | Tcl::AMP
175            | Tcl::PIPE
176            | Tcl::CARET
177            | Tcl::TILDE
178            | Tcl::LTLT
179            | Tcl::GTGT
180            // Comparison operators.
181            | Tcl::EQEQ
182            | Tcl::BANGEQ
183            | Tcl::LT
184            | Tcl::GT
185            | Tcl::LTEQ
186            | Tcl::GTEQ
187            // Logical operators.
188            | Tcl::BANG
189            | Tcl::AMPAMP
190            | Tcl::PIPEPIPE
191            // Ternary conditional operator.
192            | Tcl::QMARK => TokenRole::Operator,
193
194            // The anonymous `id` token (`Id2`) is the kind the parser
195            // actually emits, in *both* of its positions: as a standalone
196            // `set` target (`set s …` → `s`, a real operand) and as the
197            // inner leaf of every `variable_substitution` (`$s` →
198            // `(variable_substitution (id "s"))`), whose wrapper is already
199            // the operand for the reference. A blanket `Id2` exclusion
200            // therefore dropped every `set` target from n2/N2 (#1294); the
201            // correct guard is a parent check, the same shape as iRules'
202            // `Id` arm. The named `Id` never surfaces at the pinned grammar
203            // (drift marker: `tcl_named_id_variant_is_unreachable`) and is
204            // listed defensively so a grammar bump that starts emitting it
205            // classifies it identically.
206            Tcl::Id | Tcl::Id2 => {
207                if ancestors.parent_has_kind(node, Tcl::VariableSubstitution as u16) {
208                    TokenRole::Unknown
209                } else {
210                    TokenRole::Operand
211                }
212            }
213
214            // Operands: identifiers and literals.
215            //
216            // `BracedWord` reaches this arm only when the guard above
217            // let it through, which since #1354 means only when it
218            // holds no command. A script kind holding commands —
219            // a `proc` body, an `if` body, a `catch` or `eval` script —
220            // was billed as one operand spanning its entire text
221            // *beside* the commands the walk descends into, which made
222            // `n2` grow with the size and uniqueness of the code rather
223            // than with its vocabulary; nested bodies compounded it,
224            // one vocabulary entry per block, each containing the next.
225            //
226            // Gated rather than deleted (grammar-dispatch §6): the same
227            // kind is the value slot of every command the grammar does
228            // not special-case, so `lappend l {}` is an empty list
229            // whose brace pair is its only carrier, and deleting the
230            // kind scored it zero where its `lappend l ""` synonym
231            // scores one. An empty or comment-only `proc` body is
232            // spelled identically and so also counts one — #1318 can
233            // now tell the two roles apart, but it revises only the
234            // brace, so nothing here changes.
235            Tcl::SimpleWord
236            | Tcl::Number
237            | Tcl::BracedWord
238            | Tcl::BracedWordSimple
239            | Tcl::VariableSubstitution => TokenRole::Operand,
240
241            // Double-quoted strings count as a single operand when inert
242            // (`"hello world"`). When they carry a `$var` or `[cmd]`
243            // interpolation child, the inner `variable_substitution` /
244            // `command_substitution` nodes are walked separately and
245            // contribute their own operands; counting the wrapping
246            // `QuotedWord` too would double-count `N2` (issue #277, same
247            // pattern as #180/#183/#184 for Bash/C#/PHP).
248            Tcl::QuotedWord => Self::string_operand_type(
249                node,
250                &[
251                    Tcl::VariableSubstitution as u16,
252                    Tcl::CommandSubstitution as u16,
253                ],
254            ),
255
256            _ => TokenRole::Unknown,
257        }
258    }
259
260    // The half of the braced-word rule that needs the source bytes
261    // (#1318). `braced_word` serves two roles — the block of
262    // `eval {…}` and the literal of `lappend x {a b}` — and the
263    // grammar spells both the same, so only the enclosing command's
264    // leading word tells them apart (grammar-dispatch §9). This is the
265    // spelling the walk calls (`compute_halstead` →
266    // `get_op_type_with_code`), so the revision reaches every count;
267    // `get_op_type` above stays the byte-less answer for the two
268    // callers that have no source to offer.
269    fn get_op_type_with_code<'a>(
270        node: &Node<'a>,
271        code: &[u8],
272        ancestors: Ancestors<'a, '_>,
273    ) -> TokenRole {
274        Self::braced_word_op_type(node, code, ancestors, &BRACED_WORD_KINDS)
275    }
276
277    get_operator!(Tcl);
278}