big_code_analysis_ast/getter/kotlin.rs
1//! `Getter` implementation for Kotlin.
2#![allow(clippy::wildcard_imports, clippy::enum_glob_use)]
3
4use super::*;
5
6/// Returns the byte length of the leading Kotlin identifier in `text`
7/// — a Unicode letter or `_` start followed by letters / digits / `_`
8/// — or `0` if `text` does not start with an identifier.
9///
10/// The short string template `$name` interpolates a *simple
11/// identifier*, but the kotlin-ng grammar does not tokenise at the
12/// identifier boundary: it splits the literal only at each `$`, so the
13/// token after `$` absorbs any trailing inter-segment text into its own
14/// byte range. For `"$a $b"` the token after the first `$` is `"a "`
15/// (with the trailing space), and for `"$x is "` it is `"x is "`. Taking
16/// the maximal leading-identifier prefix recovers the interpolated
17/// variable (`a`, `x`) and treats the remainder as literal text, so the
18/// short form matches the long `${a}` / `${x}` form (#454). A token with
19/// no identifier prefix (`"price: $5"` → `"5"`, digit start) yields `0`
20/// and stays literal.
21fn kotlin_leading_identifier_len(text: &str) -> usize {
22 let mut chars = text.char_indices();
23 let Some((_, first)) = chars.next() else {
24 return 0;
25 };
26 if first != '_' && !first.is_alphabetic() {
27 return 0;
28 }
29 // The leading identifier ends at the first byte that is neither `_`
30 // nor alphanumeric; everything from there on is inter-segment
31 // literal text the grammar glued onto this `$name` token.
32 let mut end = first.len_utf8();
33 for (idx, c) in chars {
34 if c == '_' || c.is_alphanumeric() {
35 end = idx + c.len_utf8();
36 } else {
37 break;
38 }
39 }
40 end
41}
42
43/// Returns whether the `string_content` token `node` is (or begins
44/// with) the variable-name part of a Kotlin short string template
45/// (`$name`). The kotlin-ng grammar emits no structured node for this
46/// form — the `$` sigil and the variable name arrive as two adjacent
47/// `string_content` tokens (verified by AST dump, #454). The name token
48/// is recognised by a preceding `string_content` sibling spelled
49/// exactly `$` plus its own text starting with an identifier. Because
50/// the grammar glues trailing inter-segment text onto the name token
51/// (`"$a $b"` → `"a "`, `"$x is "` → `"x is "`), a non-empty leading
52/// identifier prefix is sufficient; the prefix is the recovered operand
53/// and the remainder is literal text. Only a token with no identifier
54/// prefix (`"price: $5"` → `"5"`) stays literal.
55fn kotlin_is_short_interp_name(node: &Node, previous: Option<Node<'_>>, code: &[u8]) -> bool {
56 use Kotlin::{StringContent, StringContent2, StringContent3};
57
58 // The grammar emits three aliased `string_content` kind ids
59 // (single-line uses `StringContent3`, others `StringContent` /
60 // `StringContent2`); all render as the text `"string_content"`.
61 const STRING_CONTENT_KINDS: [u16; 3] = [
62 StringContent as u16,
63 StringContent2 as u16,
64 StringContent3 as u16,
65 ];
66
67 if !STRING_CONTENT_KINDS.contains(&node.kind_id()) {
68 return false;
69 }
70 let Some(prev) = previous else {
71 return false;
72 };
73 if !STRING_CONTENT_KINDS.contains(&prev.kind_id()) || prev.utf8_text(code) != Some("$") {
74 return false;
75 }
76 node.utf8_text(code)
77 .is_some_and(|text| kotlin_leading_identifier_len(text) > 0)
78}
79
80/// Returns the byte slice keying the Halstead operand for a Kotlin
81/// short-interpolation name token (`$name`). The grammar glues trailing
82/// inter-segment text onto the name token, so the raw node bytes for
83/// `"$a $b"` are `"a "` — keying that would record a distinct `"a "`
84/// operand and break parity with the long `${a}` form. Narrow the slice
85/// to the leading identifier prefix so the operand is keyed as the bare
86/// identifier (`"a"`), matching the long form (#454).
87fn kotlin_short_interp_operand_bytes<'a>(node: &Node, code: &'a [u8]) -> &'a [u8] {
88 let start = node.start_byte();
89 let end = node.end_byte();
90 let full = &code[start..end];
91 match std::str::from_utf8(full) {
92 Ok(text) => {
93 let prefix_len = kotlin_leading_identifier_len(text);
94 if prefix_len > 0 {
95 &full[..prefix_len]
96 } else {
97 full
98 }
99 }
100 Err(_) => full,
101 }
102}
103
104/// Returns whether a Kotlin string literal `node` carries any
105/// interpolation: the long `${expr}` form (a structured `Interpolation`
106/// child) or the short `$name` form (adjacent `$` + identifier
107/// `string_content` tokens). The wrapping literal yields Unknown in
108/// either case so the inner expression is the only counted operand
109/// (#454).
110fn kotlin_string_has_interp(node: &Node, code: &[u8]) -> bool {
111 use Kotlin::Interpolation;
112
113 // The scan already holds each child's predecessor, so the short-form
114 // test gets it for free rather than through the `O(depth)`
115 // `Node::previous_sibling` (#1096).
116 let mut previous = None;
117 node.children().any(|child| {
118 let found = child.kind_id() == Interpolation as u16
119 || kotlin_is_short_interp_name(&child, previous, code);
120 previous = Some(child);
121 found
122 })
123}
124
125impl Getter for KotlinCode {
126 /// Names the space, synthesising one for constructs that carry no
127 /// name token (#1184).
128 ///
129 /// `get_func_space_name` returns `Option<&'a str>` borrowed from
130 /// `code`, so the only name available for a nameless construct is a
131 /// `&'static str` — a per-property spelling like `<get-foo>` would
132 /// need a signature change. Angle brackets follow the existing
133 /// `<anonymous>` convention and cannot collide with a real
134 /// identifier in any of these grammars.
135 ///
136 /// Sibling collisions are accepted, exactly as multiple
137 /// `<anonymous>` siblings already are: two properties each with a
138 /// getter, or two `static { }` blocks in one class, produce two
139 /// spaces with the same name. Nothing enforces name uniqueness
140 /// among siblings, and inventing an index would make the name
141 /// unstable under an unrelated edit.
142 ///
143 /// `<get>` / `<set>` are Kotlin's own spelling of the accessor
144 /// keywords; `<init>` is what the JVM calls the constructor an
145 /// `init` block compiles into.
146 fn get_func_space_name<'a, 'tree>(
147 node: &Node<'tree>,
148 code: &'a [u8],
149 ancestors: Ancestors<'tree, '_>,
150 ) -> Option<&'a str> {
151 match node.kind_id().into() {
152 Kotlin::Getter => Some("<get>"),
153 Kotlin::Setter => Some("<set>"),
154 Kotlin::AnonymousInitializer => Some("<init>"),
155 _ => crate::getter::default_func_space_name(node, code, ancestors),
156 }
157 }
158
159 fn get_space_kind(node: &Node) -> SpaceKind {
160 use Kotlin::*;
161
162 match node.kind_id().into() {
163 // The Kotlin grammar models classes and interfaces under a single
164 // `class_declaration` node with the discriminating keyword
165 // (`class` vs `interface`) appearing as a direct child token.
166 // Distinguishing them at the space-kind level lets the OOP
167 // metrics (`wmc`, `npa`, `npm`) attribute counts to the right
168 // bucket without re-inspecting the AST.
169 ClassDeclaration => {
170 if node.first_child(|id| id == Interface).is_some() {
171 SpaceKind::Interface
172 } else {
173 SpaceKind::Class
174 }
175 }
176 // `object MyObject { ... }` is a singleton; treat it as a class
177 // for OOP metric purposes (it has properties, methods, init
178 // blocks, exactly like a class).
179 //
180 // `companion object { ... }` is the anonymous (or named)
181 // singleton nested in a class. Its body is a `class_body`
182 // holding methods and properties, exactly like a named
183 // object, so it opens its own Class space rather than
184 // folding its members into the enclosing class (#431). The
185 // default `get_func_space_name` reads the optional `name`
186 // field, yielding the declared name for `companion object
187 // Named` and `<anonymous>` for the nameless form.
188 //
189 // `object : T { ... }` (`object_literal`) is the anonymous
190 // object expression — Kotlin's analogue of a Java anonymous
191 // class. The grammar models its body as a `class_body` of
192 // methods and properties, identical in shape to a named
193 // object, so it opens its own Class space rather than folding
194 // its members into the enclosing function (#463). It is always
195 // nameless, so the default `get_func_space_name` yields
196 // `<anonymous>`.
197 ObjectDeclaration | CompanionObject | ObjectLiteral => SpaceKind::Class,
198 FunctionDeclaration | SecondaryConstructor | LambdaLiteral | AnonymousFunction => {
199 SpaceKind::Function
200 }
201 // Property accessors and `init { … }` (#1184). Fully
202 // qualified deliberately: the enclosing `use Kotlin::*` would
203 // otherwise put the `Getter` *variant* and the `Getter`
204 // *trait* (in scope from `use super::*`) in the same
205 // namespace. `src/getter/ruby.rs` hit the same collision and
206 // solved it with `use Ruby as R;`.
207 Kotlin::Getter | Kotlin::Setter | Kotlin::AnonymousInitializer => SpaceKind::Function,
208 SourceFile => SpaceKind::Unit,
209 _ => SpaceKind::Unknown,
210 }
211 }
212
213 fn get_op_type<'a>(node: &Node<'a>, _ancestors: Ancestors<'a, '_>) -> TokenRole {
214 use Kotlin::*;
215
216 match node.kind_id().into() {
217 // Operator: control flow keywords
218 If | Else | When | For | While | Do | Try | Catch | Finally | Throw | Return
219 | ReturnAT
220 // Operator: other keywords
221 | Class | Fun | Object | Val | Var | In | Is | As | AsQMARK | BANGis | BANGin
222 | Constructor
223 // Operator: brackets, separators, terminators
224 | SEMI | COMMA | COLONCOLON | DOT | LBRACE | LBRACK | LPAREN
225 // Operator: assignment and arithmetic
226 | EQ | PLUS | DASH | STAR | SLASH | PERCENT
227 | PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ
228 | PLUSPLUS | DASHDASH
229 // Operator: comparison and equality
230 | LT | GT | LTEQ | GTEQ | EQEQ | EQEQEQ | BANGEQ | BANGEQEQ
231 // Operator: logical and misc
232 | AMPAMP | PIPEPIPE | BANG | BANGBANG
233 | QMARK | QMARKCOLON | QMARKDOT
234 | DOTDOT | DOTDOTLT | DASHGT | COLON => TokenRole::Operator,
235 // Operands: identifiers and literals, plus the self- and
236 // super-reference leaves (#1380). Four leaf kinds, not two:
237 // the label-qualified spellings `this@Outer` / `super@Inner`
238 // are `this@` / `super@` tokens, distinct kind_ids that
239 // carry no `this` / `super` leaf of their own — the same
240 // shape as `return@`, already listed among the operators
241 // above.
242 //
243 // The leaves are the keepers, not the `this_expression` /
244 // `super_expression` wrappers (grammar-dispatch section 6):
245 // a `constructor_delegation_call` (`constructor() : this(0)`,
246 // `: super(x)`) emits a bare leaf with no wrapper at all, so
247 // billing the wrapper would score constructor delegation
248 // zero. Classifying both would double-count, and the
249 // wrapper's span swallows the label identifier that is
250 // already billed on its own (section 5).
251 Identifier | NumberLiteral | FloatLiteral | CharacterLiteral | Label | This
252 | Super | ThisAT | SuperAT => TokenRole::Operand,
253 // Regression #191: a Kotlin string template wraps an
254 // `Interpolation` child (the long `"${expr}"` form) whose
255 // inner expressions are walked and counted separately, so
256 // the wrapping literal yields Unknown to avoid double-
257 // counting (same pattern as #180 Bash/Elixir, #184 PHP).
258 // The short `"Hi $name"` form has no `Interpolation` node —
259 // it needs the source bytes to distinguish `$name` from
260 // literal `$5`, so it is handled in `get_op_type_with_code`;
261 // this byte-less arm sees only the long form. Both single-
262 // line and multi-line (triple-quoted) literals interpolate.
263 StringLiteral | MultilineStringLiteral => {
264 Self::string_operand_type(node, &[Interpolation as u16])
265 }
266 _ => TokenRole::Unknown,
267 }
268 }
269
270 fn get_op_type_with_code<'a>(
271 node: &Node<'a>,
272 code: &[u8],
273 ancestors: Ancestors<'a, '_>,
274 ) -> TokenRole {
275 use Kotlin::*;
276
277 match node.kind_id().into() {
278 // The kotlin-ng grammar emits no structured node for the
279 // short string template `"Hi $name"` — the `$` sigil and the
280 // variable name arrive as two adjacent `string_content`
281 // tokens (issue #454). When the name token is a clean
282 // identifier, recover it as the operand the long `${name}`
283 // form would have produced, so both template forms count the
284 // inner variable and suppress the wrapping literal (the
285 // wrapper arm below). The grammar does not tokenise at the
286 // identifier boundary for mid-segment forms (`"$x is "`), so
287 // those are left as literal text rather than emitting a
288 // partial-literal operand.
289 StringContent | StringContent2 | StringContent3
290 if kotlin_is_short_interp_name(node, ancestors.previous_sibling(node), code) =>
291 {
292 TokenRole::Operand
293 }
294 // The wrapping literal is Unknown when it interpolates in
295 // either form (long `${expr}` or short `$name`); the inner
296 // expression / recovered variable is the only operand. A
297 // plain literal has no interpolation and stays one operand.
298 StringLiteral | MultilineStringLiteral => {
299 if kotlin_string_has_interp(node, code) {
300 TokenRole::Unknown
301 } else {
302 TokenRole::Operand
303 }
304 }
305 _ => Self::get_op_type(node, ancestors),
306 }
307 }
308
309 fn get_operand_id<'a>(
310 node: &Node<'a>,
311 code: &'a [u8],
312 ancestors: Ancestors<'a, '_>,
313 ) -> &'a [u8] {
314 // Narrow a recovered short-interpolation name token to its
315 // leading identifier prefix so `"$a $b"` keys operand `"a"`,
316 // not `"a "`, matching the long `${a}` form (#454).
317 if kotlin_is_short_interp_name(node, ancestors.previous_sibling(node), code) {
318 kotlin_short_interp_operand_bytes(node, code)
319 } else {
320 &code[node.start_byte()..node.end_byte()]
321 }
322 }
323
324 get_operator!(Kotlin);
325}