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