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}