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