1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! `Getter` implementation for Groovy.
#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
use super::*;
impl Getter for GroovyCode {
/// Names the space, synthesising one for constructs that carry no
/// name token (#1184).
///
/// `get_func_space_name` returns `Option<&'a str>` borrowed from
/// `code`, so the only name available for a nameless construct is a
/// `&'static str` — a per-property spelling like `<get-foo>` would
/// need a signature change. Angle brackets follow the existing
/// `<anonymous>` convention and cannot collide with a real
/// identifier in any of these grammars.
///
/// Sibling collisions are accepted, exactly as multiple
/// `<anonymous>` siblings already are: two properties each with a
/// getter, or two `static { }` blocks in one class, produce two
/// spaces with the same name. Nothing enforces name uniqueness
/// among siblings, and inventing an index would make the name
/// unstable under an unrelated edit.
///
/// `<static-init>` rather than the JVM's `<clinit>`: the same
/// construct exists in JavaScript, where `<clinit>` would mean
/// nothing, and one spelling across languages is worth more here
/// than JVM precision.
fn get_func_space_name<'a, 'tree>(
node: &Node<'tree>,
code: &'a [u8],
ancestors: Ancestors<'tree, '_>,
) -> Option<&'a str> {
if node.kind_id() == Groovy::StaticInitializer as u16 {
return Some("<static-init>");
}
crate::getter::default_func_space_name(node, code, ancestors)
}
fn get_space_kind(node: &Node) -> SpaceKind {
use Groovy::{
AnnotationTypeDeclaration, ClassDeclaration, Closure, ConstructorDeclaration,
EnumDeclaration, InterfaceDeclaration, MethodDeclaration, RecordDeclaration,
SourceFile, TraitDeclaration,
};
// Mirrors `impl Getter for JavaCode::get_space_kind` for class/
// method shapes (issue #280, lesson 11). `Closure` tags as
// `Function` because Groovy closures are first-class callable
// bodies, the same way Java's `LambdaExpression` is tagged.
// The new dekobon grammar models `TraitDeclaration` as a
// distinct node (the prior amaanq grammar mis-parsed `trait`
// as `juxt_function_call` + `closure` — see #247); it gets
// `Interface` because Groovy traits are interfaces with default
// method bodies.
// Groovy anonymous classes (`new Runnable() { ... }`) get no
// Class space here, unlike Java (#463): the pinned dekobon
// grammar does not attach the body to the
// `object_creation_expression`. It parses `new Runnable()` as a
// bare `object_creation_expression` and the following `{ ... }`
// as a separate `closure`, so the members already land in that
// closure's `Function` space rather than being mis-attributed to
// the enclosing method. This is an upstream-grammar limitation,
// not a wrapper bug; adding an `ObjectCreationExpression` +
// `class_body` arm here (as Java does) would be permanently
// inert under this grammar. Revisit if/when the grammar is
// bumped to model anonymous-class bodies as `class_body`.
match node.kind_id().into() {
ClassDeclaration | EnumDeclaration | RecordDeclaration => SpaceKind::Class,
InterfaceDeclaration | TraitDeclaration | AnnotationTypeDeclaration => {
SpaceKind::Interface
}
// `Groovy::` qualified: this module imports an explicit list
// rather than glob-importing, so a bare name here would parse
// as a binding and match everything.
MethodDeclaration | ConstructorDeclaration | Closure | Groovy::StaticInitializer => {
SpaceKind::Function
}
SourceFile => SpaceKind::Unit,
_ => SpaceKind::Unknown,
}
}
fn get_op_type<'a>(node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> HalsteadType {
use Groovy::*;
// Mirrors `JavaCode`'s minimal classification — modifiers
// (`Public`, `Static`, …), declaration keywords (`Class`,
// `Interface`, …), and module keywords (`Package`, `Import`,
// …) are excluded because they live inside `Modifiers` /
// `*Declaration` wrappers and would over-count if treated as
// separate operators. The dekobon Groovy grammar (#246, #247)
// emits a distinct named node for every Groovy-specific
// operator (Elvis `?:`, safe-nav `?.`, identity `===`/`!==`,
// regex `=~`/`==~`, spaceship `<=>`, exclusive ranges
// `..<` / `<..` / `<..<`, `as` coercion, etc.); their leaf
// tokens are listed here as operators so Halstead counts the
// tokens directly rather than the wrapping expression node.
// `NumberLiteral` is the new grammar's consolidated numeric
// literal — the prior grammar split numbers by radix
// (Hex/Octal/Binary/Decimal Integer/Float).
match node.kind_id().into() {
// Control-flow + keyword operators (mirrors Java's set,
// minus tokens that no longer exist in the dekobon grammar
// — `This`, `VoidType`, `Throws2`).
If | Else | Switch | Case | Try | Catch | Throw | Throws | For | While | Continue
| Break | Do | Finally | New | Return | Default | Abstract | Assert | Instanceof
| Extends | Final | Implements | Transient | Synchronized | Super | Def | In | As
// Separators / brackets.
| SEMI | COMMA | COLONCOLON | DOT | DASHGT | LBRACE | LBRACK | LPAREN
// Java-compatible operators (arithmetic, bitwise, comparison, assignment).
| EQ | LT | GT | BANG | TILDE | QMARK | COLON | EQEQ | LTEQ | GTEQ | BANGEQ
| AMPAMP | PIPEPIPE | PLUSPLUS | DASHDASH | PLUS | DASH | STAR | SLASH | AMP
| PIPE | CARET | PERCENT | LTLT | GTGT | GTGTGT | PLUSEQ | DASHEQ | STAREQ
| SLASHEQ | AMPEQ | PIPEEQ | CARETEQ | PERCENTEQ | LTLTEQ | GTGTEQ | GTGTGTEQ
| STARSTAR | STARSTAREQ
// Groovy-specific operator tokens added by the dekobon
// grammar (closes #247): ranges `..` / `..<` / `<..` /
// `<..<`, Elvis `?:` and Elvis-assign `?=`, safe-nav `?.`,
// safe-chain `??.`, spread-dot `*.`, method-pointer `.&`,
// direct-field `.@`, safe-index `?[`, identity `===` /
// `!==`, spaceship `<=>`, regex `=~` / `==~`, logical
// implication `==>`, and spread-map `*:`.
| DOTDOT | DOTDOTLT | LTDOTDOT | LTDOTDOTLT | QMARKCOLON | QMARKEQ | QMARKDOT
| QMARKQMARKDOT | STARDOT | DOTAMP | DOTAT | QMARKLBRACK | EQEQEQ | BANGEQEQ
| LTEQGT | EQTILDE | EQEQTILDE | EQEQGT | STARCOLON => HalsteadType::Operator,
Identifier | TypeIdentifier | QualifiedName | QualifiedType | NullLiteral | True
| False | NumberLiteral => HalsteadType::Operand,
// A Groovy GString interpolates inner expressions whose
// operands are walked and counted separately, so the
// wrapping literal must yield Unknown to avoid double-
// counting (issue #454, same mechanism as Kotlin #191 /
// PHP #184 / Elixir #180, generalized in #420). The dekobon
// grammar emits two interpolation child kinds: the braced
// long form `${expr}` (`gstring_brace_interpolation`) and
// the short `$name` / `$obj.field` form
// (`gstring_dollar_interpolation`). A plain non-interpolated
// literal has neither child and stays a single operand.
StringLiteral => Self::string_operand_type(
node,
&[
GstringBraceInterpolation as u16,
GstringDollarInterpolation as u16,
],
),
_ => HalsteadType::Unknown,
}
}
fn get_operator_id_as_str(id: u16) -> &'static str {
let typ = id.into();
match typ {
Groovy::LPAREN => "()",
Groovy::LBRACK => "[]",
Groovy::LBRACE => "{}",
_ => typ.into(),
}
}
}