Skip to main content

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
125// Whether `node`, a modifier keyword leaf, sits in a modifier list the
126// grammar built (#1558), rather than in the recovery of a soft-keyword
127// *name* it failed to parse. Two recoveries strand the leaf:
128//
129// - an `ERROR` holds it, usually still wrapped in its `*_modifier`
130//   node (`open = !open` at statement start, `val suspend = 1`), so the
131//   first ancestor above the wrappers is the `ERROR`;
132// - a modifier list is followed by an inserted, zero-width name
133//   (`fun f(vararg: Int)`, `class C(open: Int)`).
134//
135// Kinds are compared by name: `*_modifier` covers every wrapper, and
136// `user_type` is where recovery can hang `type_modifiers`. The inserted
137// name is found by width, not `is_missing()` (grammar-dispatch).
138fn kotlin_modifier_was_written_as_one<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
139    let host = ancestors
140        .iter(node)
141        .map(|(ancestor, _)| ancestor)
142        .find(|ancestor| {
143            let kind = ancestor.kind();
144            !(kind.ends_with("_modifier") || kind == "type_modifiers" || kind == "user_type")
145        });
146    host.is_some_and(|host| !host.is_error()) && !kotlin_next_token_is_inserted(node, ancestors)
147}
148
149// Whether the first token after `node`, comments aside, is zero-width:
150// one error recovery inserted where the source has none. A comment
151// between the two is a sibling extra, so `vararg /*c*/: Int` must step
152// over it to reach the inserted name, as `vararg: Int` does. A node
153// never opens with an extra, which the parser hangs on the level above,
154// so the descent takes each first child as it is.
155//
156// Every parameter's modifier asks, and the climb reaches
157// `function_value_parameters`, so the sibling is found without a scan
158// from the list's start: that made a long parameter list quadratic.
159fn kotlin_next_token_is_inserted<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> bool {
160    std::iter::once((*node, ancestors))
161        .chain(ancestors.iter(node))
162        .find_map(|(level, level_ancestors)| {
163            level_ancestors
164                .siblings_after(&level)
165                .find(|sibling| !sibling.as_tree_sitter().is_extra())
166        })
167        .is_some_and(|mut next| {
168            while let Some(first) = next.child(0) {
169                next = first;
170            }
171            next.start_byte() == next.end_byte()
172        })
173}
174
175// A qualified `super<A>` (a `super_expression`) encloses the supertype
176// it names (#1567).
177const GENERIC_ANGLES: GenericAngleKinds = GenericAngleKinds {
178    lists: &[
179        Kotlin::TypeArguments as u16,
180        Kotlin::TypeParameters as u16,
181        Kotlin::SuperExpression as u16,
182    ],
183    openers: &[Kotlin::LT as u16],
184    closers: &[Kotlin::GT as u16],
185    is_misparse: never_misparsed,
186};
187
188impl Getter for KotlinCode {
189    /// Names the space, synthesising one for constructs that carry no
190    /// name token (#1184).
191    ///
192    /// `get_func_space_name` returns `Option<&'a str>` borrowed from
193    /// `code`, so the only name available for a nameless construct is a
194    /// `&'static str` — a per-property spelling like `<get-foo>` would
195    /// need a signature change. Angle brackets follow the existing
196    /// `<anonymous>` convention and cannot collide with a real
197    /// identifier in any of these grammars.
198    ///
199    /// Sibling collisions are accepted, exactly as multiple
200    /// `<anonymous>` siblings already are: two properties each with a
201    /// getter, or two `static { }` blocks in one class, produce two
202    /// spaces with the same name. Nothing enforces name uniqueness
203    /// among siblings, and inventing an index would make the name
204    /// unstable under an unrelated edit.
205    ///
206    /// `<get>` / `<set>` are Kotlin's own spelling of the accessor
207    /// keywords; `<init>` is what the JVM calls the constructor an
208    /// `init` block compiles into.
209    fn get_func_space_name<'a, 'tree>(
210        node: &Node<'tree>,
211        code: &'a [u8],
212        ancestors: Ancestors<'tree, '_>,
213    ) -> Option<&'a str> {
214        match node.kind_id().into() {
215            Kotlin::Getter => Some("<get>"),
216            Kotlin::Setter => Some("<set>"),
217            Kotlin::AnonymousInitializer => Some("<init>"),
218            _ => crate::getter::default_func_space_name(node, code, ancestors),
219        }
220    }
221
222    fn get_space_kind(node: &Node) -> SpaceKind {
223        use Kotlin::*;
224
225        match node.kind_id().into() {
226            // The Kotlin grammar models classes and interfaces under a single
227            // `class_declaration` node with the discriminating keyword
228            // (`class` vs `interface`) appearing as a direct child token.
229            // Distinguishing them at the space-kind level lets the OOP
230            // metrics (`wmc`, `npa`, `npm`) attribute counts to the right
231            // bucket without re-inspecting the AST.
232            ClassDeclaration => {
233                if node.first_child(|id| id == Interface).is_some() {
234                    SpaceKind::Interface
235                } else {
236                    SpaceKind::Class
237                }
238            }
239            // `object MyObject { ... }` is a singleton; treat it as a class
240            // for OOP metric purposes (it has properties, methods, init
241            // blocks, exactly like a class).
242            //
243            // `companion object { ... }` is the anonymous (or named)
244            // singleton nested in a class. Its body is a `class_body`
245            // holding methods and properties, exactly like a named
246            // object, so it opens its own Class space rather than
247            // folding its members into the enclosing class (#431). The
248            // default `get_func_space_name` reads the optional `name`
249            // field, yielding the declared name for `companion object
250            // Named` and `<anonymous>` for the nameless form.
251            //
252            // `object : T { ... }` (`object_literal`) is the anonymous
253            // object expression — Kotlin's analogue of a Java anonymous
254            // class. The grammar models its body as a `class_body` of
255            // methods and properties, identical in shape to a named
256            // object, so it opens its own Class space rather than folding
257            // its members into the enclosing function (#463). It is always
258            // nameless, so the default `get_func_space_name` yields
259            // `<anonymous>`.
260            ObjectDeclaration | CompanionObject | ObjectLiteral => SpaceKind::Class,
261            FunctionDeclaration | SecondaryConstructor | LambdaLiteral | AnonymousFunction => {
262                SpaceKind::Function
263            }
264            // Property accessors and `init { … }` (#1184). Fully
265            // qualified deliberately: the enclosing `use Kotlin::*` would
266            // otherwise put the `Getter` *variant* and the `Getter`
267            // *trait* (in scope from `use super::*`) in the same
268            // namespace. `src/getter/ruby.rs` hit the same collision and
269            // solved it with `use Ruby as R;`.
270            Kotlin::Getter | Kotlin::Setter | Kotlin::AnonymousInitializer => SpaceKind::Function,
271            SourceFile => SpaceKind::Unit,
272            _ => SpaceKind::Unknown,
273        }
274    }
275
276    fn get_op_type<'a>(node: &Node<'a>, ancestors: Ancestors<'a, '_>) -> TokenRole {
277        use Kotlin::*;
278
279        if GENERIC_ANGLES.is_closer(node, ancestors) {
280            return TokenRole::Unknown;
281        }
282
283        // Modifier keywords, billed as C# bills all of its own (#1558;
284        // Java and Groovy bill only some, #1565). Each is the keyword
285        // leaf; its `*_modifier` / `modifiers` wrappers stay unlisted, so
286        // one modifier bills once (grammar-dispatch section 5). Every
287        // keyword has exactly one kind, so `kind_id` keys it without
288        // splitting it across the two `n1` maps. `Annotation2` is the
289        // `annotation` of `annotation class`; `Annotation` is an `@N` use,
290        // which is no keyword. `in` already bills as a variance modifier
291        // through `In` below, so `out` joins it. `reified` has only the
292        // `ReificationModifier` kind, which `KotlinCode::is_primitive`
293        // keys by its text. An early return rather than an arm, because
294        // the match below is one rustfmt declines to format.
295        //
296        // Each word is a soft keyword that valid Kotlin may use as a name,
297        // and where kotlin-ng fails to parse that name the leaf survives
298        // in its recovery (`open = !open` at statement start,
299        // `val suspend = 1`, a parameter named `vararg`). Such a leaf is
300        // no modifier and bills nothing; see
301        // `kotlin_modifier_was_written_as_one`. `companion` is left out
302        // of that gate: it never parses as a name, and gating it would
303        // only drop a real `companion object` from a class body that
304        // failed to parse.
305        let kind: Kotlin = node.kind_id().into();
306        if matches!(
307            kind,
308            Enum | Sealed
309                | Annotation2
310                | Data
311                | Inner
312                | Value
313                | Public
314                | Private
315                | Protected
316                | Internal
317                | Abstract
318                | Final
319                | Open
320                | Override
321                | Lateinit
322                | Const
323                | Tailrec
324                | Kotlin::Operator
325                | Infix
326                | Inline
327                | External
328                | Suspend
329                | Vararg
330                | Noinline
331                | Crossinline
332                | ReificationModifier
333                | Out
334                | Expect
335                | Actual
336        ) {
337            return if kotlin_modifier_was_written_as_one(node, ancestors) {
338                TokenRole::Operator
339            } else {
340                TokenRole::Unknown
341            };
342        }
343
344        // The accessor keywords, billed as C# bills its `get` / `set`
345        // (#1564), but only directly under their `getter` / `setter`,
346        // which stay unlisted. The same leaves are the `@get:` / `@set:`
347        // use-site targets, which bill nothing, like `@field:` and
348        // `@property:`; and where kotlin-ng misreads a call `get(1)` after
349        // a local `val f = 6`, an `ERROR` holds the leaf. Used as a name
350        // otherwise (`val get = 1`, `x.set(v)`, `fun get()`) the word
351        // parses as an identifier. The grammar also reads a local
352        // `val f = 6` followed by `set = 2` as a setter, which bills
353        // `set`. `field` in an accessor body is an identifier, so it is an
354        // operand like any other variable. An early return for the same
355        // reason as the modifiers'.
356        let accessor = match kind {
357            Get => Some(Kotlin::Getter),
358            Set => Some(Setter),
359            _ => None,
360        };
361        if let Some(accessor) = accessor {
362            return if ancestors.parent_has_kind(node, accessor as u16) {
363                TokenRole::Operator
364            } else {
365                TokenRole::Unknown
366            };
367        }
368
369        match kind {
370            // Operator: control flow keywords
371            If | Else | When | For | While | Do | Try | Catch | Finally | Throw | Return
372            | ReturnAT
373            // Operator: other keywords. `interface` is the declaration
374            // keyword leaf beside `class` and `object` (#1556); its
375            // `class_declaration` wrapper stays unlisted. `companion` is
376            // the one modifier billed here; see the early return above.
377            // `typealias`, `init`, `where` and `by` bill as C# bills
378            // `where` and TypeScript `type` (#1564), each the leaf of its
379            // `type_alias` / `anonymous_initializer` / `type_constraints`
380            // / `property_delegate` or `explicit_delegation` wrapper.
381            // `typealias` is a hard keyword; the other three, used as
382            // names (`val by = 1`, `where(x)`, `init.f()`), parse as
383            // identifiers, so the leaf exists only in its construct.
384            | Class | Interface | Fun | Object | Val | Var | In | Is | As | AsQMARK | BANGis
385            | BANGin | Constructor | Companion | Typealias | Init | Where | By
386            // Operator: brackets, separators, terminators
387            | SEMI | COMMA | COLONCOLON | DOT | LBRACE | LBRACK | LPAREN
388            // Operator: assignment and arithmetic
389            | EQ | PLUS | DASH | STAR | SLASH | PERCENT
390            | PLUSEQ | DASHEQ | STAREQ | SLASHEQ | PERCENTEQ
391            | PLUSPLUS | DASHDASH
392            // Operator: comparison and equality
393            | LT | GT | LTEQ | GTEQ | EQEQ | EQEQEQ | BANGEQ | BANGEQEQ
394            // Operator: logical and misc
395            | AMPAMP | PIPEPIPE | BANG | BANGBANG
396            | QMARK | QMARKCOLON | QMARKDOT
397            | DOTDOT | DOTDOTLT | DASHGT | COLON => TokenRole::Operator,
398            // Operands: identifiers and literals, plus the self- and
399            // super-reference leaves (#1380). Four leaf kinds, not two:
400            // the label-qualified spellings `this@Outer` / `super@Inner`
401            // are `this@` / `super@` tokens, distinct kind_ids that
402            // carry no `this` / `super` leaf of their own — the same
403            // shape as `return@`, already listed among the operators
404            // above.
405            //
406            // The leaves are the keepers, not the `this_expression` /
407            // `super_expression` wrappers (grammar-dispatch section 6):
408            // a `constructor_delegation_call` (`constructor() : this(0)`,
409            // `: super(x)`) emits a bare leaf with no wrapper at all, so
410            // billing the wrapper would score constructor delegation
411            // zero. Classifying both would double-count, and the
412            // wrapper's span swallows the label identifier that is
413            // already billed on its own (section 5).
414            Identifier | NumberLiteral | FloatLiteral | CharacterLiteral | Label | This
415            | Super | ThisAT | SuperAT => TokenRole::Operand,
416            // Regression #191: a Kotlin string template wraps an
417            // `Interpolation` child (the long `"${expr}"` form) whose
418            // inner expressions are walked and counted separately, so
419            // the wrapping literal yields Unknown to avoid double-
420            // counting (same pattern as #180 Bash/Elixir, #184 PHP).
421            // The short `"Hi $name"` form has no `Interpolation` node —
422            // it needs the source bytes to distinguish `$name` from
423            // literal `$5`, so it is handled in `get_op_type_with_code`;
424            // this byte-less arm sees only the long form. Both single-
425            // line and multi-line (triple-quoted) literals interpolate.
426            StringLiteral | MultilineStringLiteral => {
427                Self::string_operand_type(node, &[Interpolation as u16])
428            }
429            _ => TokenRole::Unknown,
430        }
431    }
432
433    fn get_op_type_with_code<'a>(
434        node: &Node<'a>,
435        code: &[u8],
436        ancestors: Ancestors<'a, '_>,
437    ) -> TokenRole {
438        use Kotlin::*;
439
440        match node.kind_id().into() {
441            // The kotlin-ng grammar emits no structured node for the
442            // short string template `"Hi $name"` — the `$` sigil and the
443            // variable name arrive as two adjacent `string_content`
444            // tokens (issue #454). When the name token is a clean
445            // identifier, recover it as the operand the long `${name}`
446            // form would have produced, so both template forms count the
447            // inner variable and suppress the wrapping literal (the
448            // wrapper arm below). The grammar does not tokenise at the
449            // identifier boundary for mid-segment forms (`"$x is "`), so
450            // those are left as literal text rather than emitting a
451            // partial-literal operand.
452            StringContent | StringContent2 | StringContent3
453                if kotlin_is_short_interp_name(node, ancestors.previous_sibling(node), code) =>
454            {
455                TokenRole::Operand
456            }
457            // The wrapping literal is Unknown when it interpolates in
458            // either form (long `${expr}` or short `$name`); the inner
459            // expression / recovered variable is the only operand. A
460            // plain literal has no interpolation and stays one operand.
461            StringLiteral | MultilineStringLiteral => {
462                if kotlin_string_has_interp(node, code) {
463                    TokenRole::Unknown
464                } else {
465                    TokenRole::Operand
466                }
467            }
468            _ => Self::get_op_type(node, ancestors),
469        }
470    }
471
472    fn get_operand_id<'a>(
473        node: &Node<'a>,
474        code: &'a [u8],
475        ancestors: Ancestors<'a, '_>,
476    ) -> &'a [u8] {
477        // Narrow a recovered short-interpolation name token to its
478        // leading identifier prefix so `"$a $b"` keys operand `"a"`,
479        // not `"a "`, matching the long `${a}` form (#454).
480        if kotlin_is_short_interp_name(node, ancestors.previous_sibling(node), code) {
481            kotlin_short_interp_operand_bytes(node, code)
482        } else {
483            &code[node.start_byte()..node.end_byte()]
484        }
485    }
486
487    fn get_operator_spelling<'a>(
488        node: &Node<'a>,
489        ancestors: Ancestors<'a, '_>,
490    ) -> Option<&'static str> {
491        GENERIC_ANGLES.opener_spelling(node, ancestors)
492    }
493
494    get_operator!(Kotlin);
495}