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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
//! `Getter` implementation for Groovy.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
// `method_type_parameters` is the method-level spelling of
// `type_parameters` (`<T> T id(T x)`). `LT2` is the `token.immediate`
// `<` that `type_parameters` and `type_arguments` open with; the parser
// reports it as `LT`, so it is listed defensively, here and in the
// operator arm, and its absence is pinned by
// `groovy_generic_opener_alias_never_reaches_kind_id`.
const GENERIC_ANGLES: GenericAngleKinds = GenericAngleKinds {
lists: &[
Groovy::TypeArguments as u16,
Groovy::TypeParameters as u16,
Groovy::MethodTypeParameters as u16,
],
openers: &[Groovy::LT as u16, Groovy::LT2 as u16],
closers: &[Groovy::GT as u16],
is_misparse: never_misparsed,
};
/// Whether a contextual keyword leaf (`trait`, `record`, `sealed`,
/// `permits`) sits directly under the construct it introduces.
///
/// Valid Groovy may use any of the four as a name. Inside a class the
/// grammar parses that name as an `identifier`, but a script-level
/// statement that opens with one (`record = 3`, `String record`,
/// `void record() {}`, `sealed.call()`) fails into an `ERROR` holding
/// the keyword leaf. Billing the leaf there would make a variable an
/// operator (#1552, #1565). The pinned grammar also misreads a *nested*
/// `sealed class` as a field typed `sealed`, so that `sealed` bills as
/// a type-name operand.
fn contextual_keyword_owns<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
use Groovy::*;
ancestors.parent(node).is_some_and(|parent| {
matches!(
(node.kind_id().into(), parent.kind_id().into()),
(Trait, TraitDeclaration)
| (Record, RecordDeclaration)
| (Permits, PermitsClause)
| (
Sealed,
ClassDeclaration | InterfaceDeclaration | TraitDeclaration
)
)
})
}
/// Java's eight primitive type keywords and `void`, which Groovy
/// reserves as Java does.
const PRIMITIVE_TYPES: &[&str] = &[
"boolean", "byte", "char", "double", "float", "int", "long", "short", "void",
];
/// Whether `node` spells a primitive type, which bills as a text-keyed
/// operator as Java's primitive keywords do (#1568).
///
/// The pinned grammar has no primitive node: a declared type is a
/// `type_identifier` (`int x = 1`, `void m()`), and a class literal's
/// receiver (`int.class`) or an uninitialised local (`int t`, which
/// parses as two expression statements) is a plain `identifier`. Groovy
/// reserves these keywords, so a name can spell one only as a member
/// selector after a `.`-family operator (`x.int`, `x?.int`, `x::int`) or
/// as a map or named-argument label (`[int: 1]`, `f(int: 1)`); those
/// stay operands.
fn spells_primitive_type<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
use Groovy::*;
if !matches!(node.kind_id().into(), Identifier | TypeIdentifier)
|| !node_text(code, node).is_some_and(|text| PRIMITIVE_TYPES.contains(&text))
{
return false;
}
ancestors.parent(node).is_none_or(|parent| {
let is_first_child = parent.child(0).is_some_and(|first| first.id() == node.id());
match parent.kind_id().into() {
FieldAccess
| SafeNavigationExpression
| SafeChainDotExpression
| SpreadDotExpression
| DirectFieldAccessExpression
| MethodPointerExpression
| MethodReferenceExpression => is_first_child,
MapEntry | NamedArgument => !is_first_child,
_ => true,
}
})
}
impl Getter for GroovyCode {
/// Names the space, synthesising one for constructs that carry no
/// name token (#1184).
///
/// `get_func_space_name` returns `Option<&'a str>` borrowed from
/// `code`, so the only name available for a nameless construct is a
/// `&'static str` — a per-property spelling like `<get-foo>` would
/// need a signature change. Angle brackets follow the existing
/// `<anonymous>` convention and cannot collide with a real
/// identifier in any of these grammars.
///
/// Sibling collisions are accepted, exactly as multiple
/// `<anonymous>` siblings already are: two properties each with a
/// getter, or two `static { }` blocks in one class, produce two
/// spaces with the same name. Nothing enforces name uniqueness
/// among siblings, and inventing an index would make the name
/// unstable under an unrelated edit.
///
/// `<static-init>` rather than the JVM's `<clinit>`: the same
/// construct exists in JavaScript, where `<clinit>` would mean
/// nothing, and one spelling across languages is worth more here
/// than JVM precision.
fn get_func_space_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
if node.kind_id() == Groovy::StaticInitializer as u16 {
return Some("<static-init>");
}
crate::getter::default_func_space_name(node, code, ancestors)
}
fn get_space_kind(node: &Node) -> SpaceKind {
use Groovy::{
AnnotationTypeDeclaration, ClassDeclaration, Closure, ConstructorDeclaration,
EnumDeclaration, InterfaceDeclaration, MethodDeclaration, RecordDeclaration,
SourceFile, TraitDeclaration,
};
// Mirrors `impl Getter for JavaCode::get_space_kind` for class/
// method shapes (issue #280, lesson 11). `Closure` tags as
// `Function` because Groovy closures are first-class callable
// bodies, the same way Java's `LambdaExpression` is tagged.
// The new dekobon grammar models `TraitDeclaration` as a
// distinct node (the prior amaanq grammar mis-parsed `trait`
// as `juxt_function_call` + `closure` — see #247); it gets
// `Interface` because Groovy traits are interfaces with default
// method bodies.
// Groovy anonymous classes (`new Runnable() { ... }`) get no
// Class space here, unlike Java (#463): the pinned dekobon
// grammar does not attach the body to the
// `object_creation_expression`. It parses `new Runnable()` as a
// bare `object_creation_expression` and the following `{ ... }`
// as a separate `closure`, so the members already land in that
// closure's `Function` space rather than being mis-attributed to
// the enclosing method. This is an upstream-grammar limitation,
// not a wrapper bug; adding an `ObjectCreationExpression` +
// `class_body` arm here (as Java does) would be permanently
// inert under this grammar. Revisit if/when the grammar is
// bumped to model anonymous-class bodies as `class_body`.
match node.kind_id().into() {
ClassDeclaration | EnumDeclaration | RecordDeclaration => SpaceKind::Class,
InterfaceDeclaration | TraitDeclaration | AnnotationTypeDeclaration => {
SpaceKind::Interface
}
// `Groovy::` qualified: this module imports an explicit list
// rather than glob-importing, so a bare name here would parse
// as a binding and match everything.
MethodDeclaration | ConstructorDeclaration | Closure | Groovy::StaticInitializer => {
SpaceKind::Function
}
SourceFile => SpaceKind::Unit,
_ => SpaceKind::Unknown,
}
}
fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
use Groovy::*;
if GENERIC_ANGLES.is_closer(node, ancestors) {
return TokenRole::Unknown;
}
// Mirrors `JavaCode`'s classification: modifiers are billed
// (#1565) and module keywords (`Package`, `Import`, …) are
// excluded. The grammar has no `modifiers` wrapper — each
// modifier leaf sits directly under its declaration — so each
// bills once. The type-declaration keywords
// (`class`, `interface`, `trait`, `enum`, `record`) are billed,
// as Java's are since #1552: no `*Declaration` wrapper is
// classified, so the keyword leaf is the only node that can
// bill one. The dekobon Groovy grammar (#246, #247)
// emits a distinct named node for every Groovy-specific
// operator (Elvis `?:`, safe-nav `?.`, identity `===`/`!==`,
// regex `=~`/`==~`, spaceship `<=>`, exclusive ranges
// `..<` / `<..` / `<..<`, `as` coercion, etc.); their leaf
// tokens are listed here as operators so Halstead counts the
// tokens directly rather than the wrapping expression node.
// `NumberLiteral` is the new grammar's consolidated numeric
// literal — the prior grammar split numbers by radix
// (Hex/Octal/Binary/Decimal Integer/Float).
match node.kind_id().into() {
// Slashy-string delimiter punctuation. A slashy string
// (`/xyz/`) is a `StringLiteral` whose closing delimiter is
// a `SLASH` — the kind id real division uses — so
// `def b = /xyz/` reported a `/` operator with no division
// in the source (#1314, the Groovy sibling of Elixir #1256
// and Ruby/Perl #1312). Only the *closer* is a child: the
// grammar folds the opening `/` into the literal's own
// span, so this fabricated one `/` per literal rather than
// Ruby's two. The `StringLiteral` is already the operand
// (below), so the delimiter is suppressed exactly when its
// parent is that node — the compound-leaf guard of
// grammar-dispatch section 5.
//
// Parent, not ancestor, and here that distinction is
// *observable*: a GString interpolation inside a slashy
// string (`/x${a / b}y/`) puts a real division under a
// `StringLiteral` ancestor, and an ancestor-scanning guard
// would swallow it. Pinned by
// `groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped`.
//
// The other string forms need no arm: `'plain'` is a
// childless leaf, `"dq"` closes with `"` (134), and
// dollar-slashy `$/…/$` closes with `/$` (144) — none of
// which this match classifies. `~/ab.c/` is a
// `UnaryExpression` wrapping the same `StringLiteral`, so
// its `~` still counts.
SLASH
if ancestors.parent_has_kind(node, StringLiteral as u16) =>
{
TokenRole::Unknown
}
// `super` is an operator only as the bound of a `wildcard`
// (`List<? super T>`), where it denotes no value and mirrors
// `? extends T`'s `extends` — which the arm below bills as an
// operator. Everywhere else it names a receiver a `.`, `?.`
// or `::` acts on, so it is an operand. The parent alone
// separates the two, as #1380 settled for Java's
// identically-shaped arm.
//
// Forward compatibility, not a live fix: the pinned dekobon
// grammar spells `'super'` in the `wildcard` production and
// nowhere else, so every reference (`super(1)`, `super.h()`,
// `A.super.h()`, `super::h`, `super?.h()`) is already a plain
// `identifier` and an operand. Before the gate that agreement
// was a grammar accident — an ungated `Super` would bill a
// reference as an operator the moment a bump routed one here.
//
// The operand branch is consequently unreachable under the
// pin and deliberately untested: only error recovery on
// invalid Groovy (`List<? super>`) reaches it, and pinning
// that shape would make the grammar's present
// over-permissiveness the contract (grammar-dispatch section
// 6). `groovy_wildcard_super_bound_stays_an_operator` carries
// the node census that fails by name if a bump makes it
// reachable (#1419).
Super if ancestors.parent_has_kind(node, Wildcard as u16) => TokenRole::Operator,
Super => TokenRole::Operand,
// `trait`, `record`, `sealed` and `permits` are contextual
// keywords, so each bills only under the construct it
// introduces (`contextual_keyword_owns`); elsewhere it falls
// through to `Unknown`.
Trait | Record | Sealed | Permits if contextual_keyword_owns(node, ancestors) => {
TokenRole::Operator
}
// `import static`'s `static` bills nothing, as `import` does
// not; Java makes the same call.
Static if ancestors.parent_has_kind(node, ImportDeclaration as u16) => {
TokenRole::Unknown
}
// Control-flow + keyword operators (mirrors Java's set,
// minus tokens that no longer exist in the dekobon grammar
// — `This`, `VoidType`, `Throws2`).
If | Else | Switch | Case | Try | Catch | Throw | Throws | For | While | Continue
| Break | Do | Finally | New | Return | Default | Abstract | Assert | Instanceof
| Extends | Final | Implements | Transient | Synchronized | Def | In | As
// The rest of the modifiers (#1565). `non-sealed` cannot be
// spelled as a name; `non-sealed` as a subtraction parses as
// `non`, `-` and an identifier `sealed`.
| Public | Private | Protected | Static | Native | Strictfp | Volatile
| NonDASHsealed
// Type-declaration keywords (#1552). Unlike Java's, `class`
// needs no class-literal guard: `String.class` parses as a
// `field_access` whose `class` is a plain `identifier`.
// `trait` and `record` are the gated arms above.
| Class | Interface | Enum
// Separators / brackets.
| SEMI | COMMA | COLONCOLON | DOT | DASHGT | LBRACE | LBRACK | LPAREN
// Java-compatible operators (arithmetic, bitwise, comparison, assignment).
| EQ | LT | LT2 | GT | BANG | TILDE | QMARK | COLON | EQEQ | LTEQ | GTEQ | BANGEQ
| AMPAMP | PIPEPIPE | PLUSPLUS | DASHDASH | PLUS | DASH | STAR | SLASH | AMP
| PIPE | CARET | PERCENT | LTLT | GTGT | GTGTGT | PLUSEQ | DASHEQ | STAREQ
| SLASHEQ | AMPEQ | PIPEEQ | CARETEQ | PERCENTEQ | LTLTEQ | GTGTEQ | GTGTGTEQ
| STARSTAR | STARSTAREQ
// Groovy-specific operator tokens added by the dekobon
// grammar (closes #247): ranges `..` / `..<` / `<..` /
// `<..<`, Elvis `?:` and Elvis-assign `?=`, safe-nav `?.`,
// safe-chain `??.`, spread-dot `*.`, method-pointer `.&`,
// direct-field `.@`, safe-index `?[`, identity `===` /
// `!==`, spaceship `<=>`, regex `=~` / `==~`, logical
// implication `==>`, and spread-map `*:`.
| DOTDOT | DOTDOTLT | LTDOTDOT | LTDOTDOTLT | QMARKCOLON | QMARKEQ | QMARKDOT
| QMARKQMARKDOT | STARDOT | DOTAMP | DOTAT | QMARKLBRACK | EQEQEQ | BANGEQEQ
| LTEQGT | EQTILDE | EQEQTILDE | EQEQGT | STARCOLON => TokenRole::Operator,
// `QualifiedName` (a `package` / `import` path) and
// `QualifiedType` (`java.util.Map` in type position) were
// listed here until #1263, and `QualifiedName` really did
// double-count: `package com.example` billed `com`,
// `example` AND `com.example`. `QualifiedType` never fired,
// because the runtime emits the *alias* `QualifiedType2`
// (kind_id 228) that this arm did not name — a latent
// lesson-2 miss whose only effect was to make the wrong
// classification unobservable for that half. Both are gone
// rather than completed: a qualified path is its identifier
// leaves plus the `.` operator, as in every other language
// here, and as PHP settled the same call in #1293.
//
// Completing the alias list instead is the double count,
// not the fix. Both halves are pinned against that by
// `groovy_qualified_name_counts_leaves_not_the_composite_1263`
// and `groovy_qualified_type_counts_leaves_not_the_composite_1352`.
// Neither wrapper is ever a name's sole carrier
// (grammar-dispatch section 6): `import java` still nests an
// `identifier`, and a single-segment type is a bare
// `type_identifier` the grammar does not wrap at all.
Identifier | TypeIdentifier | NullLiteral | True | False | NumberLiteral => {
TokenRole::Operand
}
// A Groovy GString interpolates inner expressions whose
// operands are walked and counted separately, so the
// wrapping literal must yield Unknown to avoid double-
// counting (issue #454, same mechanism as Kotlin #191 /
// PHP #184 / Elixir #180, generalized in #420). The dekobon
// grammar emits two interpolation child kinds: the braced
// long form `${expr}` (`gstring_brace_interpolation`) and
// the short `$name` / `$obj.field` form
// (`gstring_dollar_interpolation`). A plain non-interpolated
// literal has neither child and stays a single operand.
StringLiteral => Self::string_operand_type(
node,
&[
GstringBraceInterpolation as u16,
GstringDollarInterpolation as u16,
],
),
_ => TokenRole::Unknown,
}
}
// Only the bytes tell `int` from `Integer`, so the byte-less
// `get_op_type` above still answers `Operand` for a primitive; the
// walk calls this spelling (grammar-dispatch section 7).
fn get_op_type_with_code<'a>(
node: &Node<'a>,
code: &[u8],
ancestors: Ancestors<'a, '_>,
) -> TokenRole {
if spells_primitive_type(node, code, ancestors) {
return TokenRole::Operator;
}
Self::get_op_type(node, ancestors)
}
fn get_operator_spelling<'a>(
node: &Node<'a>,
ancestors: Ancestors<'a, '_>,
) -> Option<&'static str> {
GENERIC_ANGLES.opener_spelling(node, ancestors)
}
fn get_operator_id_as_str(id: u16) -> &'static str {
let typ = id.into();
match typ {
Groovy::LPAREN => "()",
Groovy::LBRACK => "[]",
Groovy::LBRACE => "{}",
_ => typ.into(),
}
}
}