Skip to main content

big_code_analysis_ast/getter/
bash.rs

1//! `Getter` implementation for Bash.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6/// Returns whether a Bash string node carries an expansion child the walk
7/// already counts through — `$var`, `${name[…]}`, `$(cmd)`, `$((expr))`.
8/// Only `simple_expansion` is itself an operand; for the other three it is
9/// the leaves beneath them (`ls` in `$(ls)`, `1` in `$((1))`) that carry the
10/// count, and `${#}` carries none at all. Either way the wrapping literal
11/// must not be counted on top (#180).
12#[inline]
13fn bash_string_has_expansion(node: &Node) -> bool {
14    node.children().any(|c| {
15        matches!(
16            c.kind_id().into(),
17            Bash::SimpleExpansion
18                | Bash::Expansion
19                | Bash::CommandSubstitution
20                | Bash::ArithmeticExpansion
21        )
22    })
23}
24
25impl Getter for BashCode {
26    fn get_space_kind(node: &Node) -> SpaceKind {
27        match node.kind_id().into() {
28            Bash::FunctionDefinition => SpaceKind::Function,
29            Bash::Program => SpaceKind::Unit,
30            _ => SpaceKind::Unknown,
31        }
32    }
33
34    fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
35        match node.kind_id().into() {
36            // Control flow and declaration keywords
37            Bash::If | Bash::Then | Bash::Fi | Bash::Elif | Bash::Else
38            | Bash::For | Bash::In | Bash::While | Bash::Until | Bash::Do | Bash::Done
39            | Bash::Case | Bash::Esac
40            | Bash::Function | Bash::Local | Bash::Declare | Bash::Typeset
41            | Bash::Export | Bash::Readonly | Bash::Unset | Bash::Unsetenv
42            // Punctuation acting as operators. Only the *opening*
43            // delimiter counts — `get_operator_id_as_str` folds
44            // `LPAREN`/`LBRACK`/`LBRACE` to the pair glyph, so a balanced
45            // pair is one operator. Counting the matching closer too
46            // (former `RPAREN`/`RBRACE`/`RBRACK`/`RBRACKRBRACK` arms)
47            // double-counted every pair, inflating n1/N1 (#695). `[[`
48            // (`LBRACKLBRACK`) is the test-command opener; its `]]` closer
49            // is dropped for the same reason.
50            | Bash::LPAREN | Bash::LBRACE
51            | Bash::LBRACK | Bash::LBRACKLBRACK
52            | Bash::SEMI | Bash::SEMISEMI | Bash::SEMIAMP | Bash::SEMISEMIAMP
53            | Bash::COMMA | Bash::COLON
54            // Assignment, arithmetic, and comparison operators
55            | Bash::EQ | Bash::PLUSEQ | Bash::DASHEQ | Bash::STAREQ | Bash::SLASHEQ
56            | Bash::PERCENTEQ | Bash::STARSTAREQ | Bash::LTLTEQ | Bash::GTGTEQ
57            | Bash::AMPEQ | Bash::CARETEQ | Bash::PIPEEQ
58            | Bash::PLUS | Bash::DASH | Bash::STAR | Bash::SLASH | Bash::PERCENT | Bash::STARSTAR
59            | Bash::PLUSPLUS | Bash::DASHDASH
60            | Bash::EQEQ | Bash::BANGEQ | Bash::LT | Bash::GT | Bash::LTEQ | Bash::GTEQ
61            | Bash::EQTILDE
62            // Logical and bitwise operators
63            | Bash::AMPAMP | Bash::PIPEPIPE | Bash::PIPE | Bash::PIPEAMP
64            | Bash::AMP | Bash::CARET | Bash::TILDE | Bash::BANG
65            | Bash::LTLT | Bash::GTGT
66            // Test operators (prefix)
67            | Bash::DASHa | Bash::DASHo
68            // Redirection operators
69            | Bash::AMPGT | Bash::GTAMP | Bash::LTAMP | Bash::GTPIPE
70            | Bash::LTAMPDASH | Bash::GTAMPDASH | Bash::LTLTDASH | Bash::AMPGTGT
71            | Bash::LTLTLT
72            // Ternary operator
73            | Bash::QMARK | Bash::QMARK2
74                => TokenRole::Operator,
75
76            // Quoted strings count as one operand when they are inert.
77            // When they contain any `$var`/`${...}`/`$(...)`/`$((...))`
78            // expansion child, those expansions are already walked and
79            // classified as operands; counting the wrapping literal too
80            // would double-count the inner identifiers (issue #180).
81            // `RawString` is single-quoted and `AnsiCString` is a leaf
82            // token, so neither can interpolate, but the check is uniform
83            // across the three string kinds for clarity.
84            //
85            // `translated_string` is deliberately absent (#1358) — see the
86            // note on `command_name` below, which it shares a shape with.
87            Bash::String | Bash::RawString | Bash::AnsiCString => {
88                if bash_string_has_expansion(node) {
89                    TokenRole::Unknown
90                } else {
91                    TokenRole::Operand
92                }
93            }
94
95            // Operands: identifiers, literals, variables. `variable_name`
96            // and `special_variable_name` each surface under multiple
97            // aliased kind_ids (tree-sitter generates one per parse-table
98            // context); every alias must be matched or assignment LHS
99            // identifiers like `name` in `name=value` are silently
100            // unclassified — see lesson 2.
101            //
102            // `command_name` is deliberately absent (#1351). The grammar
103            // gives it exactly one required child — a `_primary_expression`
104            // or a `concatenation` — and it contributes no text of its own,
105            // so classifying the wrapper *and* letting the walk reach the
106            // child counted every command name twice in N2 (`ls bar` scored
107            // N2 3 for two operands). For a `"$cmd"` command name it also
108            // planted a second vocabulary entry alongside the inner `$cmd`.
109            // Deleting rather than gating is safe here because the wrapper
110            // is never childless and the child kinds are the ordinary
111            // operand kinds; the one residue — a brace `expansion` with no
112            // `variable_name` leaf, `${#}` — scores zero in argument
113            // position too, so the wrapper was masking that gap for command
114            // names alone.
115            //
116            // `translated_string` is absent for the same reason (#1358).
117            // `translated_string: $ => seq('$', $.string)` gives it one
118            // required `string` child and no text of its own beyond the
119            // `$`, so `a=$"x"` scored N2 3 for two operands. It also
120            // defeated the #180 expansion guard: the wrapper's own children
121            // are `$` and `string`, so `bash_string_has_expansion` never saw
122            // the `simple_expansion` one level down and `b=$"$y"` counted
123            // both `$"$y"` and `$y`.
124            //
125            // The `string` is the keeper because it is the node present for
126            // *every* spelling. Wherever the grammar omits the wrapper — in
127            // ordinary argument position, `echo $"hi"`, which emits a bare
128            // `$` token and a `string` — the child is still there, so
129            // dropping the wrapper makes each position that *does* carry one
130            // (assignment RHS, `case` subject, command name, `for` list,
131            // array element, redirection target, `[[ ]]` operand, and any
132            // other the grammar admits) agree with argument position rather
133            // than newly disagree (grammar-dispatch §6).
134            //
135            // The residue is `command_name`'s, one level down: `$"${#}"`
136            // scores zero, because its inner `string` wraps an operandless
137            // `expansion`. A plain `"${#}"` already scored zero, so that is
138            // the parity rather than a new gap, and
139            // `bash_translated_string_scores_alike_in_both_positions` pins
140            // both halves of it.
141            //
142            // `Checker::is_string` still matches the wrapper, and that is
143            // not drift: `bca find string` reports nodes, and an
144            // interpolating `"$y"` is likewise a string the operand walk
145            // skips.
146            //
147            // `_concat` (`Bash::Concat`) is absent for a different reason:
148            // it is a hidden zero-width external token the scanner emits
149            // between `concatenation` parts, never a named node, and it
150            // spells no operand even if a future grammar promoted it.
151            // Lesson 34 prefers keeping a hidden-rule arm and pinning it,
152            // but keeping this one would preserve a classification that is
153            // wrong the moment it becomes reachable, so the arm goes and
154            // only the marker stays:
155            // `bash_hidden_concat_token_is_unreachable`.
156            Bash::Word | Bash::Word2 | Bash::Word3 | Bash::Word4
157            | Bash::Number | Bash::Number2 | Bash::NumberToken1 | Bash::NumberToken2
158            | Bash::SimpleExpansion
159                => TokenRole::Operand,
160
161            // `variable_name` / `special_variable_name` are operands when
162            // they stand alone — the `name` in `name=value`, or the inner
163            // leaf of a brace `${name}` `expansion` whose wrapper is *not*
164            // itself an operand. But `$name` (and `$?`, `$1`, …) parses as a
165            // `simple_expansion` wrapping the same leaf, and `SimpleExpansion`
166            // is already an operand above; counting the inner leaf too would
167            // double-count every bare `$var` reference (inflating n2/N2 and
168            // every derived Halstead value). Exclude the leaf exactly in
169            // that position — the same guard Tcl applies via its anonymous
170            // `Id2` exclusion and iRules applies via a parent check on
171            // `variable_substitution`.
172            Bash::VariableName | Bash::VariableName2 | Bash::VariableName3
173            | Bash::SpecialVariableName | Bash::SpecialVariableName2 => {
174                if ancestors.parent_has_kind(node, Bash::SimpleExpansion as u16) {
175                    TokenRole::Unknown
176                } else {
177                    TokenRole::Operand
178                }
179            }
180            _ => TokenRole::Unknown,
181        }
182    }
183
184    get_operator!(Bash);
185}