Skip to main content

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}