big_code_analysis_ast/getter/groovy.rs
1//! `Getter` implementation for Groovy.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6// `method_type_parameters` is the method-level spelling of
7// `type_parameters` (`<T> T id(T x)`). `LT2` is the `token.immediate`
8// `<` that `type_parameters` and `type_arguments` open with; the parser
9// reports it as `LT`, so it is listed defensively, here and in the
10// operator arm, and its absence is pinned by
11// `groovy_generic_opener_alias_never_reaches_kind_id`.
12const GENERIC_ANGLES: GenericAngleKinds = GenericAngleKinds {
13 lists: &[
14 Groovy::TypeArguments as u16,
15 Groovy::TypeParameters as u16,
16 Groovy::MethodTypeParameters as u16,
17 ],
18 openers: &[Groovy::LT as u16, Groovy::LT2 as u16],
19 closers: &[Groovy::GT as u16],
20 is_misparse: never_misparsed,
21};
22
23/// Whether a contextual keyword leaf (`trait`, `record`, `sealed`,
24/// `permits`) sits directly under the construct it introduces.
25///
26/// Valid Groovy may use any of the four as a name. Inside a class the
27/// grammar parses that name as an `identifier`, but a script-level
28/// statement that opens with one (`record = 3`, `String record`,
29/// `void record() {}`, `sealed.call()`) fails into an `ERROR` holding
30/// the keyword leaf. Billing the leaf there would make a variable an
31/// operator (#1552, #1565). The pinned grammar also misreads a *nested*
32/// `sealed class` as a field typed `sealed`, so that `sealed` bills as
33/// a type-name operand.
34fn contextual_keyword_owns<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
35 use Groovy::*;
36
37 ancestors.parent(node).is_some_and(|parent| {
38 matches!(
39 (node.kind_id().into(), parent.kind_id().into()),
40 (Trait, TraitDeclaration)
41 | (Record, RecordDeclaration)
42 | (Permits, PermitsClause)
43 | (
44 Sealed,
45 ClassDeclaration | InterfaceDeclaration | TraitDeclaration
46 )
47 )
48 })
49}
50
51/// Java's eight primitive type keywords and `void`, which Groovy
52/// reserves as Java does.
53const PRIMITIVE_TYPES: &[&str] = &[
54 "boolean", "byte", "char", "double", "float", "int", "long", "short", "void",
55];
56
57/// Whether `node` spells a primitive type, which bills as a text-keyed
58/// operator as Java's primitive keywords do (#1568).
59///
60/// The pinned grammar has no primitive node: a declared type is a
61/// `type_identifier` (`int x = 1`, `void m()`), and a class literal's
62/// receiver (`int.class`) or an uninitialised local (`int t`, which
63/// parses as two expression statements) is a plain `identifier`. Groovy
64/// reserves these keywords, so a name can spell one only as a member
65/// selector after a `.`-family operator (`x.int`, `x?.int`, `x::int`) or
66/// as a map or named-argument label (`[int: 1]`, `f(int: 1)`); those
67/// stay operands.
68fn spells_primitive_type<'a>(node: &Node<'a>, code: &[u8], ancestors: Ancestors<'a, '_>) -> bool {
69 use Groovy::*;
70
71 if !matches!(node.kind_id().into(), Identifier | TypeIdentifier)
72 || !node_text(code, node).is_some_and(|text| PRIMITIVE_TYPES.contains(&text))
73 {
74 return false;
75 }
76 ancestors.parent(node).is_none_or(|parent| {
77 let is_first_child = parent.child(0).is_some_and(|first| first.id() == node.id());
78 match parent.kind_id().into() {
79 FieldAccess
80 | SafeNavigationExpression
81 | SafeChainDotExpression
82 | SpreadDotExpression
83 | DirectFieldAccessExpression
84 | MethodPointerExpression
85 | MethodReferenceExpression => is_first_child,
86 MapEntry | NamedArgument => !is_first_child,
87 _ => true,
88 }
89 })
90}
91
92impl Getter for GroovyCode {
93 /// Names the space, synthesising one for constructs that carry no
94 /// name token (#1184).
95 ///
96 /// `get_func_space_name` returns `Option<&'a str>` borrowed from
97 /// `code`, so the only name available for a nameless construct is a
98 /// `&'static str` — a per-property spelling like `<get-foo>` would
99 /// need a signature change. Angle brackets follow the existing
100 /// `<anonymous>` convention and cannot collide with a real
101 /// identifier in any of these grammars.
102 ///
103 /// Sibling collisions are accepted, exactly as multiple
104 /// `<anonymous>` siblings already are: two properties each with a
105 /// getter, or two `static { }` blocks in one class, produce two
106 /// spaces with the same name. Nothing enforces name uniqueness
107 /// among siblings, and inventing an index would make the name
108 /// unstable under an unrelated edit.
109 ///
110 /// `<static-init>` rather than the JVM's `<clinit>`: the same
111 /// construct exists in JavaScript, where `<clinit>` would mean
112 /// nothing, and one spelling across languages is worth more here
113 /// than JVM precision.
114 fn get_func_space_name<'a, 'tree>(
115 node: &Node<'tree>,
116 code: &'a [u8],
117 ancestors: Ancestors<'tree, '_>,
118 ) -> Option<&'a str> {
119 if node.kind_id() == Groovy::StaticInitializer as u16 {
120 return Some("<static-init>");
121 }
122 crate::getter::default_func_space_name(node, code, ancestors)
123 }
124
125 fn get_space_kind(node: &Node) -> SpaceKind {
126 use Groovy::{
127 AnnotationTypeDeclaration, ClassDeclaration, Closure, ConstructorDeclaration,
128 EnumDeclaration, InterfaceDeclaration, MethodDeclaration, RecordDeclaration,
129 SourceFile, TraitDeclaration,
130 };
131
132 // Mirrors `impl Getter for JavaCode::get_space_kind` for class/
133 // method shapes (issue #280, lesson 11). `Closure` tags as
134 // `Function` because Groovy closures are first-class callable
135 // bodies, the same way Java's `LambdaExpression` is tagged.
136 // The new dekobon grammar models `TraitDeclaration` as a
137 // distinct node (the prior amaanq grammar mis-parsed `trait`
138 // as `juxt_function_call` + `closure` — see #247); it gets
139 // `Interface` because Groovy traits are interfaces with default
140 // method bodies.
141 // Groovy anonymous classes (`new Runnable() { ... }`) get no
142 // Class space here, unlike Java (#463): the pinned dekobon
143 // grammar does not attach the body to the
144 // `object_creation_expression`. It parses `new Runnable()` as a
145 // bare `object_creation_expression` and the following `{ ... }`
146 // as a separate `closure`, so the members already land in that
147 // closure's `Function` space rather than being mis-attributed to
148 // the enclosing method. This is an upstream-grammar limitation,
149 // not a wrapper bug; adding an `ObjectCreationExpression` +
150 // `class_body` arm here (as Java does) would be permanently
151 // inert under this grammar. Revisit if/when the grammar is
152 // bumped to model anonymous-class bodies as `class_body`.
153 match node.kind_id().into() {
154 ClassDeclaration | EnumDeclaration | RecordDeclaration => SpaceKind::Class,
155 InterfaceDeclaration | TraitDeclaration | AnnotationTypeDeclaration => {
156 SpaceKind::Interface
157 }
158 // `Groovy::` qualified: this module imports an explicit list
159 // rather than glob-importing, so a bare name here would parse
160 // as a binding and match everything.
161 MethodDeclaration | ConstructorDeclaration | Closure | Groovy::StaticInitializer => {
162 SpaceKind::Function
163 }
164 SourceFile => SpaceKind::Unit,
165 _ => SpaceKind::Unknown,
166 }
167 }
168
169 fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
170 use Groovy::*;
171
172 if GENERIC_ANGLES.is_closer(node, ancestors) {
173 return TokenRole::Unknown;
174 }
175 // Mirrors `JavaCode`'s classification: modifiers are billed
176 // (#1565) and module keywords (`Package`, `Import`, …) are
177 // excluded. The grammar has no `modifiers` wrapper — each
178 // modifier leaf sits directly under its declaration — so each
179 // bills once. The type-declaration keywords
180 // (`class`, `interface`, `trait`, `enum`, `record`) are billed,
181 // as Java's are since #1552: no `*Declaration` wrapper is
182 // classified, so the keyword leaf is the only node that can
183 // bill one. The dekobon Groovy grammar (#246, #247)
184 // emits a distinct named node for every Groovy-specific
185 // operator (Elvis `?:`, safe-nav `?.`, identity `===`/`!==`,
186 // regex `=~`/`==~`, spaceship `<=>`, exclusive ranges
187 // `..<` / `<..` / `<..<`, `as` coercion, etc.); their leaf
188 // tokens are listed here as operators so Halstead counts the
189 // tokens directly rather than the wrapping expression node.
190 // `NumberLiteral` is the new grammar's consolidated numeric
191 // literal — the prior grammar split numbers by radix
192 // (Hex/Octal/Binary/Decimal Integer/Float).
193 match node.kind_id().into() {
194 // Slashy-string delimiter punctuation. A slashy string
195 // (`/xyz/`) is a `StringLiteral` whose closing delimiter is
196 // a `SLASH` — the kind id real division uses — so
197 // `def b = /xyz/` reported a `/` operator with no division
198 // in the source (#1314, the Groovy sibling of Elixir #1256
199 // and Ruby/Perl #1312). Only the *closer* is a child: the
200 // grammar folds the opening `/` into the literal's own
201 // span, so this fabricated one `/` per literal rather than
202 // Ruby's two. The `StringLiteral` is already the operand
203 // (below), so the delimiter is suppressed exactly when its
204 // parent is that node — the compound-leaf guard of
205 // grammar-dispatch section 5.
206 //
207 // Parent, not ancestor, and here that distinction is
208 // *observable*: a GString interpolation inside a slashy
209 // string (`/x${a / b}y/`) puts a real division under a
210 // `StringLiteral` ancestor, and an ancestor-scanning guard
211 // would swallow it. Pinned by
212 // `groovy_slashy_guard_is_parent_scoped_not_ancestor_scoped`.
213 //
214 // The other string forms need no arm: `'plain'` is a
215 // childless leaf, `"dq"` closes with `"` (134), and
216 // dollar-slashy `$/…/$` closes with `/$` (144) — none of
217 // which this match classifies. `~/ab.c/` is a
218 // `UnaryExpression` wrapping the same `StringLiteral`, so
219 // its `~` still counts.
220 SLASH
221 if ancestors.parent_has_kind(node, StringLiteral as u16) =>
222 {
223 TokenRole::Unknown
224 }
225 // `super` is an operator only as the bound of a `wildcard`
226 // (`List<? super T>`), where it denotes no value and mirrors
227 // `? extends T`'s `extends` — which the arm below bills as an
228 // operator. Everywhere else it names a receiver a `.`, `?.`
229 // or `::` acts on, so it is an operand. The parent alone
230 // separates the two, as #1380 settled for Java's
231 // identically-shaped arm.
232 //
233 // Forward compatibility, not a live fix: the pinned dekobon
234 // grammar spells `'super'` in the `wildcard` production and
235 // nowhere else, so every reference (`super(1)`, `super.h()`,
236 // `A.super.h()`, `super::h`, `super?.h()`) is already a plain
237 // `identifier` and an operand. Before the gate that agreement
238 // was a grammar accident — an ungated `Super` would bill a
239 // reference as an operator the moment a bump routed one here.
240 //
241 // The operand branch is consequently unreachable under the
242 // pin and deliberately untested: only error recovery on
243 // invalid Groovy (`List<? super>`) reaches it, and pinning
244 // that shape would make the grammar's present
245 // over-permissiveness the contract (grammar-dispatch section
246 // 6). `groovy_wildcard_super_bound_stays_an_operator` carries
247 // the node census that fails by name if a bump makes it
248 // reachable (#1419).
249 Super if ancestors.parent_has_kind(node, Wildcard as u16) => TokenRole::Operator,
250 Super => TokenRole::Operand,
251
252 // `trait`, `record`, `sealed` and `permits` are contextual
253 // keywords, so each bills only under the construct it
254 // introduces (`contextual_keyword_owns`); elsewhere it falls
255 // through to `Unknown`.
256 Trait | Record | Sealed | Permits if contextual_keyword_owns(node, ancestors) => {
257 TokenRole::Operator
258 }
259 // `import static`'s `static` bills nothing, as `import` does
260 // not; Java makes the same call.
261 Static if ancestors.parent_has_kind(node, ImportDeclaration as u16) => {
262 TokenRole::Unknown
263 }
264
265 // Control-flow + keyword operators (mirrors Java's set,
266 // minus tokens that no longer exist in the dekobon grammar
267 // — `This`, `VoidType`, `Throws2`).
268 If | Else | Switch | Case | Try | Catch | Throw | Throws | For | While | Continue
269 | Break | Do | Finally | New | Return | Default | Abstract | Assert | Instanceof
270 | Extends | Final | Implements | Transient | Synchronized | Def | In | As
271 // The rest of the modifiers (#1565). `non-sealed` cannot be
272 // spelled as a name; `non-sealed` as a subtraction parses as
273 // `non`, `-` and an identifier `sealed`.
274 | Public | Private | Protected | Static | Native | Strictfp | Volatile
275 | NonDASHsealed
276 // Type-declaration keywords (#1552). Unlike Java's, `class`
277 // needs no class-literal guard: `String.class` parses as a
278 // `field_access` whose `class` is a plain `identifier`.
279 // `trait` and `record` are the gated arms above.
280 | Class | Interface | Enum
281 // Separators / brackets.
282 | SEMI | COMMA | COLONCOLON | DOT | DASHGT | LBRACE | LBRACK | LPAREN
283 // Java-compatible operators (arithmetic, bitwise, comparison, assignment).
284 | EQ | LT | LT2 | GT | BANG | TILDE | QMARK | COLON | EQEQ | LTEQ | GTEQ | BANGEQ
285 | AMPAMP | PIPEPIPE | PLUSPLUS | DASHDASH | PLUS | DASH | STAR | SLASH | AMP
286 | PIPE | CARET | PERCENT | LTLT | GTGT | GTGTGT | PLUSEQ | DASHEQ | STAREQ
287 | SLASHEQ | AMPEQ | PIPEEQ | CARETEQ | PERCENTEQ | LTLTEQ | GTGTEQ | GTGTGTEQ
288 | STARSTAR | STARSTAREQ
289 // Groovy-specific operator tokens added by the dekobon
290 // grammar (closes #247): ranges `..` / `..<` / `<..` /
291 // `<..<`, Elvis `?:` and Elvis-assign `?=`, safe-nav `?.`,
292 // safe-chain `??.`, spread-dot `*.`, method-pointer `.&`,
293 // direct-field `.@`, safe-index `?[`, identity `===` /
294 // `!==`, spaceship `<=>`, regex `=~` / `==~`, logical
295 // implication `==>`, and spread-map `*:`.
296 | DOTDOT | DOTDOTLT | LTDOTDOT | LTDOTDOTLT | QMARKCOLON | QMARKEQ | QMARKDOT
297 | QMARKQMARKDOT | STARDOT | DOTAMP | DOTAT | QMARKLBRACK | EQEQEQ | BANGEQEQ
298 | LTEQGT | EQTILDE | EQEQTILDE | EQEQGT | STARCOLON => TokenRole::Operator,
299
300 // `QualifiedName` (a `package` / `import` path) and
301 // `QualifiedType` (`java.util.Map` in type position) were
302 // listed here until #1263, and `QualifiedName` really did
303 // double-count: `package com.example` billed `com`,
304 // `example` AND `com.example`. `QualifiedType` never fired,
305 // because the runtime emits the *alias* `QualifiedType2`
306 // (kind_id 228) that this arm did not name — a latent
307 // lesson-2 miss whose only effect was to make the wrong
308 // classification unobservable for that half. Both are gone
309 // rather than completed: a qualified path is its identifier
310 // leaves plus the `.` operator, as in every other language
311 // here, and as PHP settled the same call in #1293.
312 //
313 // Completing the alias list instead is the double count,
314 // not the fix. Both halves are pinned against that by
315 // `groovy_qualified_name_counts_leaves_not_the_composite_1263`
316 // and `groovy_qualified_type_counts_leaves_not_the_composite_1352`.
317 // Neither wrapper is ever a name's sole carrier
318 // (grammar-dispatch section 6): `import java` still nests an
319 // `identifier`, and a single-segment type is a bare
320 // `type_identifier` the grammar does not wrap at all.
321 Identifier | TypeIdentifier | NullLiteral | True | False | NumberLiteral => {
322 TokenRole::Operand
323 }
324
325 // A Groovy GString interpolates inner expressions whose
326 // operands are walked and counted separately, so the
327 // wrapping literal must yield Unknown to avoid double-
328 // counting (issue #454, same mechanism as Kotlin #191 /
329 // PHP #184 / Elixir #180, generalized in #420). The dekobon
330 // grammar emits two interpolation child kinds: the braced
331 // long form `${expr}` (`gstring_brace_interpolation`) and
332 // the short `$name` / `$obj.field` form
333 // (`gstring_dollar_interpolation`). A plain non-interpolated
334 // literal has neither child and stays a single operand.
335 StringLiteral => Self::string_operand_type(
336 node,
337 &[
338 GstringBraceInterpolation as u16,
339 GstringDollarInterpolation as u16,
340 ],
341 ),
342
343 _ => TokenRole::Unknown,
344 }
345 }
346
347 // Only the bytes tell `int` from `Integer`, so the byte-less
348 // `get_op_type` above still answers `Operand` for a primitive; the
349 // walk calls this spelling (grammar-dispatch section 7).
350 fn get_op_type_with_code<'a>(
351 node: &Node<'a>,
352 code: &[u8],
353 ancestors: Ancestors<'a, '_>,
354 ) -> TokenRole {
355 if spells_primitive_type(node, code, ancestors) {
356 return TokenRole::Operator;
357 }
358 Self::get_op_type(node, ancestors)
359 }
360
361 fn get_operator_spelling<'a>(
362 node: &Node<'a>,
363 ancestors: Ancestors<'a, '_>,
364 ) -> Option<&'static str> {
365 GENERIC_ANGLES.opener_spelling(node, ancestors)
366 }
367
368 fn get_operator_id_as_str(id: u16) -> &'static str {
369 let typ = id.into();
370 match typ {
371 Groovy::LPAREN => "()",
372 Groovy::LBRACK => "[]",
373 Groovy::LBRACE => "{}",
374 _ => typ.into(),
375 }
376 }
377}