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
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}