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