Skip to main content

big_code_analysis_ast/getter/
ruby.rs

1//! `Getter` implementation for Ruby.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6impl Getter for RubyCode {
7    fn get_space_kind(node: &Node) -> SpaceKind {
8        use Ruby as R;
9
10        match node.kind_id().into() {
11            R::Class | R::SingletonClass => SpaceKind::Class,
12            R::Module => SpaceKind::Namespace,
13            R::Method | R::SingletonMethod | R::Lambda | R::Block | R::DoBlock => {
14                SpaceKind::Function
15            }
16            R::Program => SpaceKind::Unit,
17            _ => SpaceKind::Unknown,
18        }
19    }
20
21    fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
22        use Ruby as R;
23
24        match node.kind_id().into() {
25            // Regex delimiter punctuation. tree-sitter-ruby aliases
26            // every regex delimiter spelling to `/`: `bca dump` at the
27            // pinned grammar shows `/…/`, `%r{…}` and `%r|…|` all
28            // emitting `SLASH` (kind 85) for both delimiters — the very
29            // kind id real division uses. Counting them fabricated two
30            // `/` operators per regex literal and made n1/N1 depend on
31            // the author's delimiter choice (#1312, the Ruby sibling of
32            // Elixir #1256). The `Regex` node itself is the operand
33            // (below), so the delimiters are suppressed exactly when
34            // their parent is that node — the compound-leaf guard of
35            // grammar-dispatch §5. Parent, not ancestor: a `/` nested
36            // deeper, such as a division inside `#{…}` interpolation,
37            // has `Binary` as its parent and must still count.
38            // `SLASH2` is the aliased regex-start token; the runtime
39            // `public_symbol_map` collapses it to `SLASH` so it never
40            // reaches `kind_id()` (the `LPAREN2` class of #768, pinned
41            // by `ruby_regex_start_alias_never_reaches_kind_id`). It
42            // appears here and *only* here — it was dropped from the
43            // arithmetic arm below, because a regex delimiter is the
44            // only thing it could ever be. Should a bump surface it
45            // outside a `Regex`, it now falls to `Unknown` rather than
46            // being counted as a division.
47            R::SLASH | R::SLASH2
48                if ancestors.parent_has_kind(node, R::Regex as u16) =>
49            {
50                TokenRole::Unknown
51            }
52            // Subshell delimiter punctuation — the second delimiter
53            // family of the fabrication the arm above removes for
54            // regexes (#1360, following #1312). tree-sitter-ruby
55            // aliases both ends of a subshell literal to a single
56            // backtick token: `bca dump` at the pinned grammar shows
57            // all seven spellings — the backtick pair plus %x with
58            // {}, (), [], <>, || and !! — emitting BQUOTE (kind 103)
59            // for the opener *and* the closer, so a one-command
60            // literal reported a backtick operator that is nowhere in
61            // the source as an operation. The `Subshell` node itself
62            // is the operand (the string-like arm below), so
63            // suppressing its delimiters loses nothing, and the
64            // childless `` `` `` spelling still bills that wrapper.
65            //
66            // Gated rather than dropped from the operator arm, per
67            // grammar-dispatch §6: a backtick is also a legal Ruby
68            // method name — def and call alike — and at this grammar
69            // that marker is a BQUOTE wrapped in a named `operator`
70            // node, so its parent is `Operator` and never `Subshell`.
71            // Deleting the kind would score the method-name marker
72            // zero.
73            //
74            // Parent, not ancestor, and here the difference is
75            // observable: a backtick method inside a subshell's `#{…}`
76            // interpolation still has `Operator` as its parent but the
77            // `Subshell` as a further ancestor, so an ancestor scan
78            // would swallow it. That is the input
79            // `ruby_subshell_guard_is_parent_scoped_not_ancestor_scoped`
80            // exists to separate — unlike the numeral guard below,
81            // whose two spellings no Ruby input can tell apart.
82            //
83            // One kind, where the regex guard above names two: BQUOTE2
84            // is this family's aliased literal-start token, but unlike
85            // SLASH2 it never sat in the operator arm, so the wildcard
86            // already answers `Unknown` for it and listing it here
87            // would read as load-bearing while doing nothing. Its
88            // reachability is pinned by
89            // `ruby_subshell_start_alias_never_reaches_kind_id`
90            // instead.
91            R::BQUOTE if ancestors.parent_has_kind(node, R::Subshell as u16) => {
92                TokenRole::Unknown
93            }
94            // Control-flow keyword tokens. tree-sitter-ruby gives each
95            // keyword its own anonymous numbered variant (e.g. `If2` is
96            // the `if` keyword token; `If` is the named statement node).
97            R::If2 | R::Unless2 | R::While2 | R::Until2 | R::For2 | R::In2 | R::Do2
98            | R::Case2 | R::When2 | R::Elsif2 | R::Else2 | R::Then2
99            | R::Begin2 | R::Ensure2 | R::Rescue2
100            | R::Return3 | R::Yield3 | R::Break3 | R::Next3 | R::Redo2 | R::Retry2
101            // Declaration keywords. `End`/`End2` are the two aliased
102            // visible kinds for the `end` block closer (kind_ids 0 and
103            // 13) that every `def`/`class`/`module`/`begin`/`if`/loop
104            // construct emits; `BEGIN`/`END` are the special `BEGIN { }`
105            // / `END { }` block-form keywords (kinds 4 / 7) and are
106            // distinct from the lowercase `end` closer.
107            | R::Def | R::End | R::End2 | R::Class2 | R::Module2
108            | R::BEGIN | R::END
109            | R::Undef2 | R::Alias2
110            // Logical / definedness keywords
111            | R::And | R::Or | R::Not | R::DefinedQMARK
112            // Structural punctuation acting as operators. `HASHLBRACE`
113            // — the `#{` interpolation opener — was here until #1314
114            // and is deliberately gone: an interpolation opener is
115            // spelling rather than an operation, the interpolated
116            // expression's own operators being counted already. Unlike
117            // PHP's `{`, Ruby's is a token of its own, so it needs no
118            // parent guard; dropping the arm is the whole change, and
119            // it applies to every literal that interpolates — string,
120            // symbol, regex, heredoc, subshell. See the PHP sibling in
121            // `src/getter/php.rs` for the cross-language measurement
122            // this policy rests on. This half is a behaviour change,
123            // not a fabrication fix: Ruby Halstead operator counts drop
124            // for interpolated literals.
125            //
126            // Only the *opening* delimiter counts (the pair folds to
127            // one glyph in `get_operator_id_as_str`); the former closing arms
128            // (`RPAREN`/`RPAREN2`/`RBRACE`/`RBRACK`) double-counted every
129            // balanced pair, inflating n1/N1 (#695). The `LBRACKRBRACK` /
130            // `LBRACKRBRACKEQ` indexer *method names* below (`def [](i)`)
131            // are whole-token operators, not a balanced pair, and stay.
132            // `LPAREN2`/`LBRACK2`/`LBRACK3` are defensive — the runtime
133            // `public_symbol_map` collapses them to `LPAREN`/`LBRACK` before
134            // `kind_id()`, so they never fire (#768; see the Cpp impl note
135            // and `second_alias_opener_collapses_to_base_kind_id`).
136            | R::LPAREN | R::LPAREN2
137            | R::LBRACE | R::LBRACK | R::LBRACK2 | R::LBRACK3
138            | R::COMMA | R::SEMI | R::DOT | R::COLONCOLON | R::COLONCOLON2 | R::AMPDOT
139            | R::COLON | R::COLON2 | R::DASHGT
140            // Method-name operator markers (`def +@`, `def -@`, `def ~@`)
141            // and indexer methods.
142            | R::PLUSAT | R::DASHAT | R::TILDEAT
143            | R::LBRACKRBRACK | R::LBRACKRBRACKEQ
144            // Arithmetic
145            | R::PLUS | R::DASH | R::DASH2 | R::DASH3 | R::DASH4 | R::STAR | R::STAR2 | R::STAR3
146            | R::SLASH | R::PERCENT
147            | R::STARSTAR | R::STARSTAR2 | R::STARSTAR3
148            // Comparison
149            | R::EQEQ | R::BANGEQ | R::EQEQEQ
150            | R::LT | R::GT | R::LTEQ | R::GTEQ | R::LTEQGT
151            | R::EQTILDE | R::BANGTILDE
152            // Logical / unary
153            | R::AMPAMP | R::PIPEPIPE | R::BANG | R::TILDE
154            // Bitwise / shift
155            | R::AMP | R::AMP2 | R::PIPE | R::CARET | R::LTLT | R::LTLT2 | R::GTGT
156            // Assignment
157            | R::EQ | R::EQ2
158            | R::PLUSEQ | R::DASHEQ | R::STAREQ | R::SLASHEQ | R::PERCENTEQ
159            | R::STARSTAREQ | R::AMPEQ | R::AMPAMPEQ | R::PIPEEQ | R::PIPEPIPEEQ
160            | R::CARETEQ | R::LTLTEQ | R::GTGTEQ
161            // Hash arrow, ternary, range
162            | R::EQGT | R::QMARK | R::DOTDOT | R::DOTDOTDOT
163            // Subshell backtick used as method-name marker (def `...)
164            | R::BQUOTE
165                => TokenRole::Operator,
166
167            // String-like literals contribute one operand each when
168            // inert. The wrapper is suppressed exactly when it holds a
169            // child the walk already counts, so its contribution is not
170            // billed twice — the shared spelling of C# #183 / Elixir
171            // #180 (`Interpolation`) and of #1353 (the element kinds).
172            //
173            // `String`, `BareString`, `BareSymbol`, `Subshell`, `Regex`,
174            // `HeredocBody` and `DelimitedSymbol` admit only
175            // `string_content` / `escape_sequence` / `interpolation` as
176            // named children (`heredoc_body` also `heredoc_content` /
177            // `heredoc_end`), of which `Interpolation` alone is
178            // classified elsewhere; the delimiter tokens they also hold
179            // are anonymous and no arm names them.
180            //
181            // The three *element containers* hold classified operands
182            // instead, so before #1353 the guard never fired for them
183            // and the wrapper was billed alongside every element:
184            // `b = %w[x y]` scored n2 4 / N2 4 for three operands, the
185            // spare entry being the wrapper's whole-span text. That made
186            // the vocabulary grow with how the author grouped the
187            // literals — the spelling sensitivity #695 removed for
188            // Python's implicit concatenation and #1312 / #1314 for
189            // delimiter tokens.
190            //
191            //   `chained_string` ⊃ `string`      (`"a" "b"`)
192            //   `string_array`   ⊃ `bare_string` (`%w[…]`, `%W[…]`)
193            //   `symbol_array`   ⊃ `bare_symbol` (`%i[…]`, `%I[…]`)
194            //
195            // Gated rather than deleted, per grammar-dispatch §6:
196            // node-types.json marks the two arrays' children optional
197            // and `%w[]` / `%i[]` really do parse to a wrapper holding
198            // nothing but its delimiter tokens, so dropping the kinds
199            // would score an empty literal zero. (`chained_string` is
200            // `seq($.string, repeat1($.string))` and so carries at
201            // least two elements; the guard therefore always fires and
202            // its membership here is unobservable — listed for symmetry
203            // with its siblings rather than out of need. See
204            // `ruby_element_containers_count_elements_not_the_composite_1353`,
205            // which says so rather than pretending to pin it.)
206            //
207            // One list serves both groups because no kind here can hold
208            // an element kind it is not paired with above, and none of
209            // the containers admits an `Interpolation` as a *direct*
210            // child — `%W[a#{n}]` nests it inside the `bare_string`,
211            // which is guarded by this same arm one level down.
212            //
213            // `BareSymbol` reached this arm in #1353; until then it sat
214            // unguarded in the plain operand arm below, so `%I[a#{n}b]`
215            // billed the element `a#{n}b` *and* the `n` inside it while
216            // the `%W[…]` spelling of the same thing billed only `n`.
217            R::String | R::ChainedString | R::BareString | R::BareSymbol
218            | R::Subshell | R::Regex | R::HeredocBody | R::DelimitedSymbol
219            | R::StringArray | R::SymbolArray => Self::string_operand_type(
220                node,
221                &[
222                    R::Interpolation as u16,
223                    R::String as u16,
224                    R::BareString as u16,
225                    R::BareSymbol as u16,
226                ],
227            ),
228
229            // The numeral inside a suffixed numeric literal. At the
230            // pinned tree-sitter-ruby the two suffix forms are wrappers
231            // over the numeral they suffix, never leaves:
232            //
233            //   rational: seq($._int_or_float, token.immediate('r'))
234            //   complex:  choice(
235            //               seq($._int_or_float, token.immediate('i')),
236            //               seq(alias($._int_or_float, $.rational),
237            //                   token.immediate('ri')))
238            //
239            // so `2i` is a `complex` over an `integer`, and `3ri` a
240            // `complex` over a `rational` over an `integer` — three
241            // levels deep, every one of which the plain operand arm
242            // below billed. `a = 1r; b = 2i; c = 3ri; d = 4` reported
243            // n2 11 / N2 12 for its eight operands (#1359).
244            //
245            // Neither §1 nor §2 adds a kind to name here, and both
246            // facts are pinned in the test rather than trusted:
247            // `_int_or_float` is hidden, so `Ruby::IntOrFloat` exists
248            // in the enum and the parser never emits it, leaving the
249            // wrapper itself as the numeral's parent; and the `ri`
250            // form's `alias(…, $.rational)` reuses kind 308 instead of
251            // minting a suffixed variant, so the middle node is a
252            // `rational` whose span holds no `r` of its own.
253            //
254            // The wrapper is the keeper here, not the leaf. `1`, `1r`,
255            // `1i` and `1ri` are four distinct Ruby constants; billing
256            // the leaf would file all four under the operand text `1`
257            // and erase what makes them different values. Every other
258            // language here spells the suffix inside a single token
259            // (`1u32`, `1L`, `1j`), so keeping the wrapper is what
260            // makes Ruby agree with its siblings. It is the same
261            // resolution the `Nil` / `Nil2` pair takes below, and the
262            // opposite of the one `%w[…]` takes above — there the
263            // wrapper holds operands that stand on their own, here the
264            // three levels are one constant.
265            //
266            // Gated rather than deleted, though nothing regresses to
267            // zero either way (§6): node-types.json marks both
268            // wrappers' lone numeral child `"required": true`, and an
269            // unsuffixed `4` carries no wrapper at all, so the arm
270            // below still owns it. §6's keeper rule — take the node
271            // present for *every* spelling — would pick the leaf, but
272            // that rule exists to stop a childless variant scoring
273            // zero and neither choice can do that here. With the
274            // hazard absent the tiebreak is operand identity, which
275            // only the wrapper's span carries.
276            //
277            // Written on the parent per §5, but unlike the regex guard
278            // above the choice is unobservable: `complex` and
279            // `rational` admit numeral children and nothing else, so no
280            // node can have either as an *ancestor* without having it
281            // as its parent. No Ruby input tells the two spellings
282            // apart, and no test can pin the difference.
283            //
284            // Every kind in both sets is load-bearing. Drop `Rational`
285            // from the guarded set and the middle node of `3ri` is
286            // billed again; drop it from the parent set and the leaf
287            // under a bare `1r` is. Only their *pairing* is
288            // unobservable — a `rational` directly inside a `rational`
289            // is not a shape this grammar admits.
290            R::Integer | R::Float | R::Rational
291                if ancestors
292                    .parent(node)
293                    .is_some_and(|p| matches!(p.kind_id().into(), R::Complex | R::Rational)) =>
294            {
295                TokenRole::Unknown
296            }
297
298            // Operands: identifiers and literals.
299            R::Identifier | R::IdentifierSuffix | R::IdentifierSuffixToken1
300            | R::Constant | R::ConstantSuffix | R::ConstantSuffixToken1
301            | R::InstanceVariable | R::ClassVariable | R::GlobalVariable
302            | R::Integer | R::Float | R::Complex | R::Rational
303            | R::Character | R::SimpleSymbol | R::HashKeySymbol
304            // `Nil2` is the leaf `nil` keyword token; `Nil` (named) wraps
305            // it. Counting both would double-count every `nil` literal —
306            // only the wrapping named node contributes one operand.
307            | R::True | R::False | R::Nil
308            | R::Zelf | R::Super
309            | R::Line | R::File | R::Encoding
310                => TokenRole::Operand,
311
312            _ => TokenRole::Unknown,
313        }
314    }
315
316    get_operator!(Ruby);
317}