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}