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