Skip to main content

normalize_languages/
kotlin.rs

1//! Kotlin language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::java::{find_gradle_cache, find_maven_repository, get_java_version};
5use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
6use std::path::{Path, PathBuf};
7use tree_sitter::Node;
8
9/// Kotlin language support.
10pub struct Kotlin;
11
12impl Language for Kotlin {
13    fn name(&self) -> &'static str {
14        "Kotlin"
15    }
16    fn extensions(&self) -> &'static [&'static str] {
17        &["kt", "kts"]
18    }
19    fn grammar_name(&self) -> &'static str {
20        "kotlin"
21    }
22
23    fn has_symbols(&self) -> bool {
24        true
25    }
26
27    fn container_kinds(&self) -> &'static [&'static str] {
28        &["class_declaration", "object_declaration", "enum_class_body"]
29    }
30
31    fn function_kinds(&self) -> &'static [&'static str] {
32        &[
33            "function_declaration",
34            "anonymous_function",
35            "lambda_literal",
36        ]
37    }
38
39    fn type_kinds(&self) -> &'static [&'static str] {
40        &["class_declaration", "object_declaration", "type_alias"]
41    }
42
43    fn import_kinds(&self) -> &'static [&'static str] {
44        &["import_header"]
45    }
46
47    fn public_symbol_kinds(&self) -> &'static [&'static str] {
48        &[
49            "class_declaration",
50            "object_declaration",
51            "function_declaration",
52        ]
53    }
54
55    fn visibility_mechanism(&self) -> VisibilityMechanism {
56        VisibilityMechanism::AccessModifier
57    }
58
59    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
60        if self.get_visibility(node, content) != Visibility::Public {
61            return Vec::new();
62        }
63
64        let name = match self.node_name(node, content) {
65            Some(n) => n.to_string(),
66            None => return Vec::new(),
67        };
68
69        let kind = match node.kind() {
70            "class_declaration" => SymbolKind::Class,
71            "object_declaration" => SymbolKind::Class, // object is a singleton class
72            "function_declaration" => SymbolKind::Function,
73            _ => return Vec::new(),
74        };
75
76        vec![Export {
77            name,
78            kind,
79            line: node.start_position().row + 1,
80        }]
81    }
82
83    fn scope_creating_kinds(&self) -> &'static [&'static str] {
84        &[
85            "for_statement",
86            "while_statement",
87            "do_while_statement",
88            "try_expression",
89            "catch_block",
90            "when_expression",
91            "lambda_literal",
92        ]
93    }
94
95    fn control_flow_kinds(&self) -> &'static [&'static str] {
96        &[
97            "if_expression",
98            "for_statement",
99            "while_statement",
100            "do_while_statement",
101            "when_expression",
102            "try_expression",
103            "jump_expression",
104        ]
105    }
106
107    fn complexity_nodes(&self) -> &'static [&'static str] {
108        &[
109            "if_expression",
110            "for_statement",
111            "while_statement",
112            "do_while_statement",
113            "when_entry",
114            "catch_block",
115            "elvis_expression",
116            "conjunction_expression",
117            "disjunction_expression",
118        ]
119    }
120
121    fn nesting_nodes(&self) -> &'static [&'static str] {
122        &[
123            "if_expression",
124            "for_statement",
125            "while_statement",
126            "do_while_statement",
127            "when_expression",
128            "try_expression",
129            "function_declaration",
130            "class_declaration",
131        ]
132    }
133
134    fn signature_suffix(&self) -> &'static str {
135        " {}"
136    }
137
138    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
139        let name = self.node_name(node, content)?;
140        let params = node
141            .child_by_field_name("value_parameters")
142            .or_else(|| node.child_by_field_name("parameters"))
143            .map(|p| content[p.byte_range()].to_string())
144            .unwrap_or_else(|| "()".to_string());
145
146        let return_type = node
147            .child_by_field_name("type")
148            .map(|t| format!(": {}", content[t.byte_range()].trim()))
149            .unwrap_or_default();
150
151        // Check for override modifier
152        let is_override = if let Some(mods) = node.child_by_field_name("modifiers") {
153            let mut cursor = mods.walk();
154            let children: Vec<_> = mods.children(&mut cursor).collect();
155            children.iter().any(|child| {
156                child.kind() == "member_modifier"
157                    && child.child(0).map(|c| c.kind()) == Some("override")
158            })
159        } else {
160            false
161        };
162
163        Some(Symbol {
164            name: name.to_string(),
165            kind: SymbolKind::Function,
166            signature: format!("fun {}{}{}", name, params, return_type),
167            docstring: None,
168            attributes: Vec::new(),
169            start_line: node.start_position().row + 1,
170            end_line: node.end_position().row + 1,
171            visibility: self.get_visibility(node, content),
172            children: Vec::new(),
173            is_interface_impl: is_override,
174            implements: Vec::new(),
175        })
176    }
177
178    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
179        let name = self.node_name(node, content)?;
180        let (kind, keyword) = match node.kind() {
181            "object_declaration" => (SymbolKind::Class, "object"),
182            _ => (SymbolKind::Class, "class"),
183        };
184
185        Some(Symbol {
186            name: name.to_string(),
187            kind,
188            signature: format!("{} {}", keyword, name),
189            docstring: None,
190            attributes: Vec::new(),
191            start_line: node.start_position().row + 1,
192            end_line: node.end_position().row + 1,
193            visibility: self.get_visibility(node, content),
194            children: Vec::new(),
195            is_interface_impl: false,
196            implements: Vec::new(),
197        })
198    }
199
200    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
201        if node.kind() == "type_alias" {
202            let name = self.node_name(node, content)?;
203            let target = node
204                .child_by_field_name("type")
205                .map(|t| content[t.byte_range()].to_string())
206                .unwrap_or_default();
207            return Some(Symbol {
208                name: name.to_string(),
209                kind: SymbolKind::Type,
210                signature: format!("typealias {} = {}", name, target),
211                docstring: None,
212                attributes: Vec::new(),
213                start_line: node.start_position().row + 1,
214                end_line: node.end_position().row + 1,
215                visibility: self.get_visibility(node, content),
216                children: Vec::new(),
217                is_interface_impl: false,
218                implements: Vec::new(),
219            });
220        }
221        self.extract_container(node, content)
222    }
223
224    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
225        // Look for KDoc comment before the node
226        let mut prev = node.prev_sibling();
227        while let Some(sibling) = prev {
228            match sibling.kind() {
229                "multiline_comment" => {
230                    let text = &content[sibling.byte_range()];
231                    if text.starts_with("/**") {
232                        // Strip /** and */ and leading *
233                        let lines: Vec<&str> = text
234                            .strip_prefix("/**")
235                            .unwrap_or(text)
236                            .strip_suffix("*/")
237                            .unwrap_or(text)
238                            .lines()
239                            .map(|l| l.trim().strip_prefix("*").unwrap_or(l).trim())
240                            .filter(|l| !l.is_empty())
241                            .collect();
242                        if !lines.is_empty() {
243                            return Some(lines.join(" "));
244                        }
245                    }
246                    return None;
247                }
248                "line_comment" => {
249                    // Skip single-line comments
250                }
251                _ => return None,
252            }
253            prev = sibling.prev_sibling();
254        }
255        None
256    }
257
258    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
259        Vec::new()
260    }
261
262    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
263        if node.kind() != "import_header" {
264            return Vec::new();
265        }
266
267        let line = node.start_position().row + 1;
268
269        // Get the import identifier
270        let mut cursor = node.walk();
271        for child in node.children(&mut cursor) {
272            if child.kind() == "identifier" || child.kind() == "user_type" {
273                let module = content[child.byte_range()].to_string();
274                let is_wildcard = content[node.byte_range()].contains(".*");
275                return vec![Import {
276                    module,
277                    names: Vec::new(),
278                    alias: None,
279                    is_wildcard,
280                    is_relative: false,
281                    line,
282                }];
283            }
284        }
285
286        Vec::new()
287    }
288
289    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
290        // Kotlin: import pkg.Class or import pkg.*
291        if import.is_wildcard {
292            format!("import {}.*", import.module)
293        } else {
294            format!("import {}", import.module)
295        }
296    }
297
298    fn is_public(&self, node: &Node, content: &str) -> bool {
299        self.get_visibility(node, content) == Visibility::Public
300    }
301
302    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
303        let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
304        if has_test_attr {
305            return true;
306        }
307        match symbol.kind {
308            crate::SymbolKind::Class => {
309                symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
310            }
311            _ => false,
312        }
313    }
314
315    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
316        None
317    }
318
319    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
320        node.child_by_field_name("class_body")
321            .or_else(|| node.child_by_field_name("body"))
322    }
323
324    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
325        false
326    }
327
328    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
329        // Try "name" field first (most declarations)
330        if let Some(name_node) = node.child_by_field_name("name") {
331            return Some(&content[name_node.byte_range()]);
332        }
333        // For type alias, the name might be a simple_identifier
334        let mut cursor = node.walk();
335        for child in node.children(&mut cursor) {
336            if child.kind() == "simple_identifier" {
337                return Some(&content[child.byte_range()]);
338            }
339        }
340        None
341    }
342
343    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
344        let ext = path.extension()?.to_str()?;
345        if ext != "kt" && ext != "kts" {
346            return None;
347        }
348        // Kotlin: com/foo/Bar.kt -> com.foo.Bar
349        let path_str = path.to_str()?;
350        // Remove common source prefixes
351        let rel = path_str
352            .strip_prefix("src/main/kotlin/")
353            .or_else(|| path_str.strip_prefix("src/main/java/"))
354            .or_else(|| path_str.strip_prefix("src/"))
355            .unwrap_or(path_str);
356        let without_ext = rel
357            .strip_suffix(".kt")
358            .or_else(|| rel.strip_suffix(".kts"))?;
359        Some(without_ext.replace('/', "."))
360    }
361
362    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
363        let path = module.replace('.', "/");
364        vec![
365            format!("src/main/kotlin/{}.kt", path),
366            format!("src/main/java/{}.kt", path), // Kotlin can live in java dirs
367            format!("src/{}.kt", path),
368        ]
369    }
370
371    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
372        import_name.starts_with("kotlin.")
373            || import_name.starts_with("kotlinx.")
374            || import_name.starts_with("java.")
375            || import_name.starts_with("javax.")
376    }
377
378    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
379        // Kotlin stdlib is bundled with the compiler/runtime
380        None
381    }
382
383    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
384        let mut cursor = node.walk();
385        for child in node.children(&mut cursor) {
386            if child.kind() == "modifiers" {
387                let mods = &content[child.byte_range()];
388                if mods.contains("private") {
389                    return Visibility::Private;
390                }
391                if mods.contains("protected") {
392                    return Visibility::Protected;
393                }
394                if mods.contains("internal") {
395                    return Visibility::Protected;
396                } // internal ≈ protected for our purposes
397                if mods.contains("public") {
398                    return Visibility::Public;
399                }
400            }
401            // Also check visibility_modifier directly
402            if child.kind() == "visibility_modifier" {
403                let vis = &content[child.byte_range()];
404                if vis == "private" {
405                    return Visibility::Private;
406                }
407                if vis == "protected" {
408                    return Visibility::Protected;
409                }
410                if vis == "internal" {
411                    return Visibility::Protected;
412                }
413                if vis == "public" {
414                    return Visibility::Public;
415                }
416            }
417        }
418        // Kotlin default is public (unlike Java's package-private)
419        Visibility::Public
420    }
421
422    // === Import Resolution ===
423
424    fn lang_key(&self) -> &'static str {
425        "kotlin"
426    }
427
428    fn resolve_local_import(
429        &self,
430        import: &str,
431        current_file: &Path,
432        project_root: &Path,
433    ) -> Option<PathBuf> {
434        let path_part = import.replace('.', "/");
435
436        // Common Kotlin source directories
437        let source_dirs = [
438            "src/main/kotlin",
439            "src/main/java", // Kotlin can live alongside Java
440            "src/kotlin",
441            "src",
442            "app/src/main/kotlin", // Android
443            "app/src/main/java",
444        ];
445
446        for src_dir in &source_dirs {
447            // Try .kt first, then .java (Kotlin can import Java)
448            for ext in &["kt", "java"] {
449                let source_path = project_root
450                    .join(src_dir)
451                    .join(format!("{}.{}", path_part, ext));
452                if source_path.is_file() {
453                    return Some(source_path);
454                }
455            }
456        }
457
458        // Also try relative to current file's package structure
459        let mut current = current_file.parent()?;
460        while current != project_root {
461            for ext in &["kt", "java"] {
462                let potential = current.join(format!("{}.{}", path_part, ext));
463                if potential.is_file() {
464                    return Some(potential);
465                }
466            }
467            current = current.parent()?;
468        }
469
470        None
471    }
472
473    fn resolve_external_import(
474        &self,
475        import_name: &str,
476        project_root: &Path,
477    ) -> Option<ResolvedPackage> {
478        // Kotlin uses Maven/Gradle like Java
479        // Reuse Java's resolution (they share the same cache)
480        crate::java::Java.resolve_external_import(import_name, project_root)
481    }
482
483    fn get_version(&self, _project_root: &Path) -> Option<String> {
484        // Use Java version as proxy (Kotlin runs on JVM)
485        get_java_version()
486    }
487
488    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
489        find_maven_repository().or_else(find_gradle_cache)
490    }
491
492    fn indexable_extensions(&self) -> &'static [&'static str] {
493        &["kt", "kts"]
494    }
495
496    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
497        // Reuse Java's package sources (shared Maven/Gradle cache)
498        crate::java::Java.package_sources(_project_root)
499    }
500
501    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
502        use crate::traits::{has_extension, skip_dotfiles};
503        if skip_dotfiles(name) {
504            return true;
505        }
506        if is_dir && (name == "META-INF" || name == "test" || name == "tests") {
507            return true;
508        }
509        !is_dir && !has_extension(name, self.indexable_extensions())
510    }
511
512    fn discover_packages(&self, source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
513        // Reuse Java's package discovery
514        crate::java::Java.discover_packages(source)
515    }
516
517    fn package_module_name(&self, entry_name: &str) -> String {
518        entry_name
519            .strip_suffix(".kt")
520            .or_else(|| entry_name.strip_suffix(".kts"))
521            .unwrap_or(entry_name)
522            .to_string()
523    }
524
525    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
526        if path.is_file() {
527            return Some(path.to_path_buf());
528        }
529        // For JAR files, return the JAR itself
530        if path.extension().map(|e| e == "jar").unwrap_or(false) {
531            return Some(path.to_path_buf());
532        }
533        None
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540    use crate::validate_unused_kinds_audit;
541
542    /// Documents node kinds that exist in the Kotlin grammar but aren't used in trait methods.
543    /// Run `cross_check_node_kinds` in registry.rs to see all potentially useful kinds.
544    #[test]
545    fn unused_node_kinds_audit() {
546        #[rustfmt::skip]
547        let documented_unused: &[&str] = &[
548            // STRUCTURAL
549            "annotated_lambda",        // @Ann { }
550            "class_body",              // class body
551            "class_modifier",          // class modifiers
552            "class_parameter",         // class param
553            "constructor_delegation_call", // this(), super()
554            "constructor_invocation",  // constructor call
555            "control_structure_body",  // control body
556            "delegation_specifier",    // delegation
557            "enum_entry",              // enum value
558            "function_body",           // function body
559            "function_modifier",       // fun modifiers
560            "function_type_parameters",// (T) -> U params
561            "function_value_parameters", // fun params
562            "identifier",              // too common
563            "import_alias",            // import as
564            "import_list",             // imports
565            "inheritance_modifier",    // open, final
566            "interpolated_expression", // ${expr}
567            "interpolated_identifier", // $id
568            "lambda_parameters",       // lambda params
569            "member_modifier",         // member modifiers
570            "modifiers",               // modifiers
571            "multi_variable_declaration", // val (a, b)
572            "parameter_modifier",      // param modifiers
573            "parameter_modifiers",     // param modifiers list
574            "parameter_with_optional_type", // optional type param
575            "platform_modifier",       // expect, actual
576            "primary_constructor",     // primary constructor
577            "property_declaration",    // property
578            "property_modifier",       // property modifiers
579            "reification_modifier",    // reified
580            "secondary_constructor",   // secondary constructor
581            "simple_identifier",       // simple id
582            "statements",              // statement list
583            "visibility_modifier",     // public, private
584
585            // EXPRESSION
586            "additive_expression",     // a + b
587            "as_expression",           // x as T
588            "call_expression",         // foo()
589            "check_expression",        // is, !is
590            "comparison_expression",   // a < b
591            "directly_assignable_expression", // assignable
592            "equality_expression",     // a == b
593            "indexing_expression",     // arr[i]
594            "infix_expression",        // a infix b
595            "multiplicative_expression", // a * b
596            "navigation_expression",   // a.b
597            "parenthesized_expression",// (expr)
598            "postfix_expression",      // x++
599            "prefix_expression",       // ++x
600            "range_expression",        // 0..10
601            "spread_expression",       // *arr
602            "super_expression",        // super
603            "this_expression",         // this
604            "wildcard_import",         // import.*
605
606            // TYPE
607            "function_type",           // (T) -> U
608            "not_nullable_type",       // T & Any
609            "nullable_type",           // T?
610            "parenthesized_type",      // (T)
611            "parenthesized_user_type", // (UserType)
612            "receiver_type",           // T.
613            "type_arguments",          // <T, U>
614            "type_constraint",         // T : Bound
615            "type_constraints",        // where clause
616            "type_identifier",         // type name
617            "type_modifiers",          // type modifiers
618            "type_parameter",          // T
619            "type_parameter_modifiers",// type param mods
620            "type_parameters",         // <T, U>
621            "type_projection",         // out T, in T
622            "type_projection_modifiers", // projection mods
623            "type_test",               // is T
624            "user_type",               // user-defined type
625            "variance_modifier",       // in, out
626
627            // OTHER
628            "finally_block",           // finally
629            "variable_declaration",    // var/val decl
630        ];
631
632        validate_unused_kinds_audit(&Kotlin, documented_unused)
633            .expect("Kotlin unused node kinds audit failed");
634    }
635}