use std::path::{Path, PathBuf};
use tree_sitter::Language;
use astmap_core::SymbolKind;
use super::{
detect_jvm_source_roots, find_enclosing_symbol, node_text, parse_with_tree_sitter,
symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol, ExtractedTypeRef,
LanguageParser, LanguageResolver, LanguageSupport, ParseResult,
};
pub(super) fn lang() -> LanguageSupport {
LanguageSupport {
name: "kotlin",
extensions: &["kt", "kts"],
parser: &KotlinParser,
resolver_factory: |root| Box::new(KotlinResolver::new(root)),
config_files: &["build.gradle.kts", "build.gradle"],
sibling_fn: |_| None, }
}
pub(crate) struct KotlinParser;
impl LanguageParser for KotlinParser {
fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
parse_kotlin_file(source)
}
fn language_name(&self) -> &str {
"kotlin"
}
}
fn kotlin_language() -> Language {
tree_sitter_kotlin_ng::LANGUAGE.into()
}
fn parse_kotlin_file(source: &str) -> ParseResult {
parse_with_tree_sitter(
source,
&kotlin_language(),
|root, src, symbols, imports| extract_from_node(root, src, symbols, imports, None),
extract_calls,
extract_type_refs,
)
}
fn extract_from_node(
node: tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
parent_index: Option<usize>,
) {
match node.kind() {
"class_declaration" => {
if let Some(sym) = extract_class_declaration(&node, source, parent_index) {
let idx = symbols.len();
symbols.push(sym);
recurse_into_class_body(&node, source, symbols, imports, idx);
return;
}
}
"object_declaration" => {
if let Some(sym) = extract_object_declaration(&node, source, parent_index) {
let idx = symbols.len();
symbols.push(sym);
if let Some(body) = find_child_by_kind(&node, "class_body") {
recurse_children(&body, source, symbols, imports, Some(idx));
}
return;
}
}
"companion_object" => {
let sym = extract_companion_object(&node, source, parent_index);
let idx = symbols.len();
symbols.push(sym);
if let Some(body) = find_child_by_kind(&node, "class_body") {
recurse_children(&body, source, symbols, imports, Some(idx));
}
return;
}
"function_declaration" => {
if let Some(sym) = extract_function(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"property_declaration" => {
if let Some(sym) = extract_property(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"type_alias" => {
if let Some(sym) = extract_type_alias(&node, source) {
symbols.push(sym);
return;
}
}
"import" => {
if let Some(imp) = extract_import(&node, source) {
imports.push(imp);
}
return;
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
extract_from_node(child, source, symbols, imports, parent_index);
}
}
}
fn find_child_by_kind<'a>(
node: &'a tree_sitter::Node<'a>,
kind: &str,
) -> Option<tree_sitter::Node<'a>> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == kind {
return Some(child);
}
}
}
None
}
fn recurse_children(
body: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
parent_index: Option<usize>,
) {
for i in 0..body.child_count() {
if let Some(child) = body.child(i as u32) {
extract_from_node(child, source, symbols, imports, parent_index);
}
}
}
fn recurse_into_class_body(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
class_idx: usize,
) {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
match child.kind() {
"class_body" => {
recurse_children(&child, source, symbols, imports, Some(class_idx));
return;
}
"enum_class_body" => {
for j in 0..child.child_count() {
if let Some(entry) = child.child(j as u32) {
if entry.kind() == "enum_entry" {
if let Some(sym) =
extract_enum_entry(&entry, source, Some(class_idx))
{
symbols.push(sym);
}
} else {
extract_from_node(entry, source, symbols, imports, Some(class_idx));
}
}
}
return;
}
_ => {}
}
}
}
}
fn classify_class_declaration(node: &tree_sitter::Node, source: &str) -> SymbolKind {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "enum_class_body" {
return SymbolKind::Enum;
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "interface" {
return SymbolKind::Interface;
}
}
}
if let Some(modifiers) = find_child_by_kind(node, "modifiers") {
let mods_text = node_text(&modifiers, source);
if mods_text.contains("annotation") {
return SymbolKind::Annotation;
}
if mods_text.contains("enum") {
return SymbolKind::Enum;
}
}
SymbolKind::Class
}
fn extract_class_declaration(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let kind = classify_class_declaration(node, source);
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_object_declaration(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
Some(symbol_from_node(
name,
SymbolKind::Class,
node,
source,
parent_index,
))
}
fn extract_companion_object(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> ExtractedSymbol {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "Companion".to_string());
symbol_from_node(name, SymbolKind::Module, node, source, parent_index)
}
fn extract_function(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name")?;
let name = node_text(&name_node, source);
let kind = if parent_index.is_some() {
SymbolKind::Method
} else {
SymbolKind::Function
};
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn extract_property(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name = find_property_name(node, source)?;
let kind = if parent_index.is_none() {
if has_val_keyword(node) {
SymbolKind::Const
} else {
SymbolKind::Variable
}
} else {
SymbolKind::Variable
};
Some(symbol_from_node(name, kind, node, source, parent_index))
}
fn find_property_name(node: &tree_sitter::Node, source: &str) -> Option<String> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "variable_declaration" {
for j in 0..child.child_count() {
if let Some(id) = child.child(j as u32) {
if id.kind() == "identifier" || id.kind() == "simple_identifier" {
return Some(node_text(&id, source));
}
}
}
}
}
}
None
}
fn has_val_keyword(node: &tree_sitter::Node) -> bool {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "val" {
return true;
}
if child.kind() == "var" {
return false;
}
}
}
false
}
fn extract_type_alias(node: &tree_sitter::Node, source: &str) -> Option<ExtractedSymbol> {
let name = node
.child_by_field_name("type")
.map(|n| node_text(&n, source))
.or_else(|| {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "identifier" {
return Some(node_text(&child, source));
}
}
}
None
})?;
Some(symbol_from_node(
name,
SymbolKind::TypeAlias,
node,
source,
None,
))
}
fn extract_enum_entry(
node: &tree_sitter::Node,
source: &str,
parent_index: Option<usize>,
) -> Option<ExtractedSymbol> {
let name = find_first_identifier(node, source)?;
Some(symbol_from_node(
name,
SymbolKind::Const,
node,
source,
parent_index,
))
}
fn find_first_identifier(node: &tree_sitter::Node, source: &str) -> Option<String> {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "identifier" || child.kind() == "simple_identifier" {
return Some(node_text(&child, source));
}
}
}
None
}
fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
let mut qualified_path: Option<String> = None;
let mut is_wildcard = false;
let mut alias: Option<String> = None;
let mut saw_as = false;
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
match child.kind() {
"qualified_identifier" => {
qualified_path = Some(node_text(&child, source));
}
"*" => {
is_wildcard = true;
}
"as" => {
saw_as = true;
}
"identifier" if saw_as => {
alias = Some(node_text(&child, source));
}
_ => {}
}
}
}
let dot_path = qualified_path?;
let slash_path = dot_path.replace('.', "/");
if is_wildcard {
Some(ExtractedImport {
path: format!("{}::*", slash_path),
imported_symbols: vec![],
})
} else if let Some(alias_name) = alias {
Some(ExtractedImport {
path: slash_path,
imported_symbols: vec![alias_name],
})
} else {
let last_segment = dot_path.rsplit('.').next().unwrap_or(&dot_path).to_string();
Some(ExtractedImport {
path: slash_path,
imported_symbols: vec![last_segment],
})
}
}
fn extract_calls(
root: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
) -> Vec<ExtractedCall> {
let mut calls = Vec::new();
collect_calls(root, source, symbols, &mut calls);
calls
}
fn collect_calls(
node: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
calls: &mut Vec<ExtractedCall>,
) {
if node.kind() == "call_expression" {
if let Some(callee_node) = node.child(0) {
let callee_name = extract_callee_name(&callee_node, source);
if !is_kotlin_call_builtin(&callee_name) {
let call_line = node.start_position().row + 1;
if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
calls.push(ExtractedCall {
caller_name: caller.to_string(),
callee_name,
line: call_line,
});
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
collect_calls(child, source, symbols, calls);
}
}
}
fn extract_callee_name(node: &tree_sitter::Node, source: &str) -> String {
match node.kind() {
"navigation_expression" => {
let count = node.child_count();
if count > 0 {
if let Some(last) = node.child((count - 1) as u32) {
let text = node_text(&last, source);
return text.trim_start_matches('.').to_string();
}
}
let full = node_text(node, source);
full.rsplit('.').next().unwrap_or(&full).to_string()
}
"identifier" | "simple_identifier" => node_text(node, source),
_ => {
let full = node_text(node, source);
full.rsplit('.').next().unwrap_or(&full).to_string()
}
}
}
fn is_kotlin_call_builtin(name: &str) -> bool {
matches!(
name,
"println"
| "print"
| "require"
| "requireNotNull"
| "check"
| "checkNotNull"
| "error"
| "assert"
| "TODO"
| "run"
| "let"
| "also"
| "apply"
| "with"
| "lazy"
| "repeat"
| "buildString"
| "buildList"
| "buildSet"
| "buildMap"
| "listOf"
| "setOf"
| "mapOf"
| "mutableListOf"
| "mutableSetOf"
| "mutableMapOf"
| "arrayOf"
| "intArrayOf"
| "longArrayOf"
| "emptyList"
| "emptySet"
| "emptyMap"
| "listOfNotNull"
| "to"
| "arrayOfNulls"
| "hashMapOf"
| "hashSetOf"
| "linkedMapOf"
| "linkedSetOf"
| "sortedMapOf"
| "sortedSetOf"
)
}
fn extract_type_refs(
root: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
) -> Vec<ExtractedTypeRef> {
let mut refs = Vec::new();
walk_kotlin_type_refs(root, source, symbols, &mut refs);
refs
}
fn walk_kotlin_type_refs(
node: tree_sitter::Node,
source: &str,
symbols: &[ExtractedSymbol],
refs: &mut Vec<ExtractedTypeRef>,
) {
if node.kind() == "user_type" {
if let Some(id) = node.child(0) {
if id.kind() == "identifier"
|| id.kind() == "type_identifier"
|| id.kind() == "simple_identifier"
{
let type_name = node_text(&id, source);
let line = node.start_position().row + 1;
if !is_kotlin_builtin(&type_name) {
if let Some(from_sym) = find_enclosing_symbol(symbols, line) {
refs.push(ExtractedTypeRef {
from_symbol: from_sym.to_string(),
to_type: type_name,
line,
});
}
}
}
}
return; }
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
walk_kotlin_type_refs(child, source, symbols, refs);
}
}
}
fn is_kotlin_builtin(name: &str) -> bool {
matches!(
name,
"Int"
| "Long"
| "Float"
| "Double"
| "Boolean"
| "Char"
| "Byte"
| "Short"
| "String"
| "Unit"
| "Nothing"
| "Any"
| "Array"
| "List"
| "Set"
| "Map"
| "MutableList"
| "MutableSet"
| "MutableMap"
| "Pair"
| "Triple"
| "Sequence"
| "Iterable"
| "Comparable"
| "Throwable"
| "Exception"
| "RuntimeException"
| "Error"
| "Annotation"
| "Deprecated"
| "Suppress"
| "JvmStatic"
| "JvmField"
| "JvmOverloads"
)
}
pub(crate) struct KotlinResolver {
source_roots: Vec<PathBuf>,
}
impl KotlinResolver {
fn new(root: &Path) -> Self {
Self {
source_roots: detect_jvm_source_roots(root),
}
}
}
impl LanguageResolver for KotlinResolver {
fn resolve_import(
&self,
import_path: &str,
_source_file: &str,
project_root: &Path,
) -> Option<PathBuf> {
let effective_path = import_path.strip_suffix("::*").unwrap_or(import_path);
let project_root_buf = project_root.to_path_buf();
for root in self
.source_roots
.iter()
.chain(std::iter::once(&project_root_buf))
{
let kt_path = root.join(format!("{}.kt", effective_path));
if kt_path.exists() {
return kt_path.canonicalize().ok();
}
let kts_path = root.join(format!("{}.kts", effective_path));
if kts_path.exists() {
return kts_path.canonicalize().ok();
}
let java_path = root.join(format!("{}.java", effective_path));
if java_path.exists() {
return java_path.canonicalize().ok();
}
let dir = root.join(effective_path);
if dir.is_dir() {
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "kt") {
return path.canonicalize().ok();
}
}
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use astmap_core::SymbolKind;
fn parse(source: &str) -> ParseResult {
KotlinParser.parse(source, &PathBuf::from("test.kt"))
}
#[test]
fn test_parse_class() {
let result = parse("class Foo {}");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Foo");
assert_eq!(result.symbols[0].kind, SymbolKind::Class);
}
#[test]
fn test_parse_data_class() {
let result = parse("data class User(val name: String, val age: Int)");
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "User");
}
#[test]
fn test_parse_sealed_class() {
let result = parse("sealed class Result {}");
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "Result");
}
#[test]
fn test_parse_enum_class() {
let result = parse(
r#"enum class Color {
RED,
GREEN,
BLUE
}"#,
);
assert!(!result.has_errors);
let enums: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Enum)
.collect();
assert_eq!(enums.len(), 1);
assert_eq!(enums[0].name, "Color");
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert!(
consts.len() >= 3,
"expected 3 enum entries, got {:?}",
consts
);
}
#[test]
fn test_parse_annotation_class() {
let result = parse("annotation class MyAnnotation");
assert!(!result.has_errors);
let annotations: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Annotation)
.collect();
assert_eq!(annotations.len(), 1);
assert_eq!(annotations[0].name, "MyAnnotation");
}
#[test]
fn test_parse_interface() {
let result = parse("interface Drawable {\n fun draw()\n}");
assert!(!result.has_errors);
let interfaces: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Interface)
.collect();
assert_eq!(interfaces.len(), 1);
assert_eq!(interfaces[0].name, "Drawable");
}
#[test]
fn test_parse_sealed_interface() {
let result = parse("sealed interface Event {}");
assert!(!result.has_errors);
let interfaces: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Interface)
.collect();
assert_eq!(interfaces.len(), 1);
assert_eq!(interfaces[0].name, "Event");
}
#[test]
fn test_parse_value_class() {
let result = parse("@JvmInline\nvalue class Password(val value: String)");
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "Password");
}
#[test]
fn test_parse_inner_class() {
let result = parse(
r#"class Outer {
inner class Inner {}
}"#,
);
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert!(classes.len() >= 2, "expected Outer and Inner classes");
let inner = result.symbols.iter().find(|s| s.name == "Inner").unwrap();
assert_eq!(inner.kind, SymbolKind::Class);
assert!(inner.parent_index.is_some(), "Inner should have parent");
}
#[test]
fn test_parse_object() {
let result = parse("object Singleton {}");
assert!(!result.has_errors);
let objs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.name == "Singleton")
.collect();
assert_eq!(objs.len(), 1);
assert_eq!(objs[0].kind, SymbolKind::Class);
}
#[test]
fn test_parse_companion_object_default_name() {
let result = parse(
r#"class Foo {
companion object {
fun create(): Foo = Foo()
}
}"#,
);
let companions: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Module)
.collect();
assert_eq!(companions.len(), 1);
assert_eq!(companions[0].name, "Companion");
assert!(companions[0].parent_index.is_some());
}
#[test]
fn test_parse_companion_object_named() {
let result = parse(
r#"class Bar {
companion object Factory {
fun create(): Bar = Bar()
}
}"#,
);
let companions: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Module)
.collect();
assert_eq!(companions.len(), 1);
assert_eq!(companions[0].name, "Factory");
}
#[test]
fn test_parse_top_level_function() {
let result = parse("fun hello() {}");
assert!(!result.has_errors);
let funcs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "hello");
assert!(funcs[0].parent_index.is_none());
}
#[test]
fn test_parse_method_in_class() {
let result = parse(
r#"class Greeter {
fun greet(name: String): String {
return "Hello, $name"
}
}"#,
);
assert!(!result.has_errors);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "greet");
assert!(methods[0].parent_index.is_some());
}
#[test]
fn test_parse_extension_function() {
let result = parse("fun String.addExclamation(): String = this + \"!\"");
assert!(!result.has_errors);
let funcs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "addExclamation");
}
#[test]
fn test_parse_top_level_val_as_const() {
let result = parse("val MAX_SIZE = 100");
assert!(!result.has_errors);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert_eq!(consts.len(), 1);
assert_eq!(consts[0].name, "MAX_SIZE");
}
#[test]
fn test_parse_top_level_var_as_variable() {
let result = parse("var counter = 0");
assert!(!result.has_errors);
let vars: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Variable)
.collect();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name, "counter");
}
#[test]
fn test_parse_class_property_as_variable() {
let result = parse(
r#"class Foo {
val name: String = "test"
}"#,
);
assert!(!result.has_errors);
let vars: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Variable)
.collect();
assert_eq!(vars.len(), 1);
assert_eq!(vars[0].name, "name");
assert!(vars[0].parent_index.is_some());
}
#[test]
fn test_parse_type_alias() {
let result = parse("typealias StringList = List<String>");
assert!(!result.has_errors);
let aliases: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::TypeAlias)
.collect();
assert_eq!(aliases.len(), 1);
assert_eq!(aliases[0].name, "StringList");
}
#[test]
fn test_parse_enum_entries_as_const() {
let result = parse(
r#"enum class Direction {
NORTH,
SOUTH,
EAST,
WEST
}"#,
);
assert!(!result.has_errors);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert!(consts.len() >= 4, "expected 4 enum entries");
let names: Vec<&str> = consts.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"NORTH"));
assert!(names.contains(&"SOUTH"));
assert!(names.contains(&"EAST"));
assert!(names.contains(&"WEST"));
for c in &consts {
assert!(
c.parent_index.is_some(),
"enum entry {} should have parent",
c.name
);
}
}
#[test]
fn test_nested_class_parent_index() {
let result = parse(
r#"class Outer {
class Nested {
fun inner() {}
}
fun outer() {}
}"#,
);
assert!(!result.has_errors);
let outer = result.symbols.iter().find(|s| s.name == "Outer").unwrap();
assert!(outer.parent_index.is_none());
let nested = result.symbols.iter().find(|s| s.name == "Nested").unwrap();
assert_eq!(nested.parent_index, Some(0));
let inner_fn = result.symbols.iter().find(|s| s.name == "inner").unwrap();
assert_eq!(inner_fn.kind, SymbolKind::Method);
let outer_fn = result.symbols.iter().find(|s| s.name == "outer").unwrap();
assert_eq!(outer_fn.kind, SymbolKind::Method);
assert_eq!(outer_fn.parent_index, Some(0));
}
#[test]
fn test_import_regular() {
let result = parse("import com.example.Foo\n");
assert!(!result.has_errors);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].path, "com/example/Foo");
assert_eq!(result.imports[0].imported_symbols, vec!["Foo"]);
}
#[test]
fn test_import_wildcard() {
let result = parse("import com.example.*\n");
assert!(!result.has_errors);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].path, "com/example::*");
assert!(result.imports[0].imported_symbols.is_empty());
}
#[test]
fn test_import_alias() {
let result = parse("import com.example.Foo as Bar\n");
assert!(!result.has_errors);
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].path, "com/example/Foo");
assert_eq!(result.imports[0].imported_symbols, vec!["Bar"]);
}
#[test]
fn test_multiple_imports() {
let result = parse(
r#"import com.example.Foo
import com.example.Bar
import org.other.*
"#,
);
assert!(!result.has_errors);
assert!(
result.imports.len() >= 3,
"expected at least 3 imports, got {}",
result.imports.len()
);
}
#[test]
fn test_call_simple() {
let result = parse(
r#"fun main() {
doWork()
}"#,
);
assert!(!result.has_errors);
let calls: Vec<_> = result
.calls
.iter()
.filter(|c| c.callee_name == "doWork")
.collect();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].caller_name, "main");
}
#[test]
fn test_call_navigation() {
let result = parse(
r#"fun process() {
service.execute()
}"#,
);
assert!(!result.has_errors);
let calls: Vec<_> = result
.calls
.iter()
.filter(|c| c.callee_name == "execute")
.collect();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].caller_name, "process");
}
#[test]
fn test_call_builtin_filtered() {
let result = parse(
r#"fun main() {
println("hello")
listOf(1, 2, 3)
}"#,
);
assert!(!result.has_errors);
let non_builtins: Vec<_> = result
.calls
.iter()
.filter(|c| c.callee_name == "println" || c.callee_name == "listOf")
.collect();
assert_eq!(non_builtins.len(), 0);
}
#[test]
fn test_type_ref_parameter() {
let result = parse("fun process(input: CustomType): ResultType { return ResultType() }");
assert!(!result.has_errors);
let type_names: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
type_names.contains(&"CustomType"),
"should reference CustomType, got {:?}",
type_names
);
assert!(
type_names.contains(&"ResultType"),
"should reference ResultType, got {:?}",
type_names
);
}
#[test]
fn test_type_ref_builtin_filtered() {
let result = parse("fun count(items: List<String>): Int { return items.size }");
assert!(!result.has_errors);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
!refs.contains(&"Int"),
"Int should be filtered, got {:?}",
refs
);
assert!(
!refs.contains(&"String"),
"String should be filtered, got {:?}",
refs
);
assert!(
!refs.contains(&"List"),
"List should be filtered, got {:?}",
refs
);
}
#[test]
fn test_type_ref_class_property() {
let result = parse(
r#"class Container {
val service: MyService = MyService()
}"#,
);
assert!(!result.has_errors);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(
refs.contains(&"MyService"),
"should reference MyService, got {:?}",
refs
);
}
#[test]
fn test_classify_class_kinds() {
let result = parse(
r#"class Regular {}
data class Data(val x: Int)
sealed class Sealed {}
interface Iface {
fun draw()
}
sealed interface SealedIface {}
annotation class Anno
enum class E { A, B }
"#,
);
assert!(!result.has_errors);
let regular = result.symbols.iter().find(|s| s.name == "Regular").unwrap();
assert_eq!(regular.kind, SymbolKind::Class);
let data = result.symbols.iter().find(|s| s.name == "Data").unwrap();
assert_eq!(data.kind, SymbolKind::Class);
let sealed = result.symbols.iter().find(|s| s.name == "Sealed").unwrap();
assert_eq!(sealed.kind, SymbolKind::Class);
let iface = result.symbols.iter().find(|s| s.name == "Iface").unwrap();
assert_eq!(iface.kind, SymbolKind::Interface);
let sealed_iface = result
.symbols
.iter()
.find(|s| s.name == "SealedIface")
.unwrap();
assert_eq!(sealed_iface.kind, SymbolKind::Interface);
let anno = result.symbols.iter().find(|s| s.name == "Anno").unwrap();
assert_eq!(anno.kind, SymbolKind::Annotation);
let e = result.symbols.iter().find(|s| s.name == "E").unwrap();
assert_eq!(e.kind, SymbolKind::Enum);
}
#[test]
fn test_resolver_kt_file() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/kotlin/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("Foo.kt"), "class Foo {}").unwrap();
let resolver = KotlinResolver::new(tmp.path());
let result = resolver.resolve_import("com/example/Foo", "Main.kt", tmp.path());
assert!(result.is_some(), "should resolve com/example/Foo.kt");
}
#[test]
fn test_resolver_wildcard_import() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/kotlin/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("Foo.kt"), "class Foo {}").unwrap();
let resolver = KotlinResolver::new(tmp.path());
let result = resolver.resolve_import("com/example/Foo::*", "Main.kt", tmp.path());
assert!(result.is_some(), "should resolve wildcard import");
}
#[test]
fn test_resolver_kts_file() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/kotlin/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("build.kts"), "// build script").unwrap();
let resolver = KotlinResolver::new(tmp.path());
let result = resolver.resolve_import("com/example/build", "Main.kt", tmp.path());
assert!(result.is_some(), "should resolve .kts file");
}
#[test]
fn test_resolver_java_cross_language() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/kotlin/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("Legacy.java"), "class Legacy {}").unwrap();
let resolver = KotlinResolver::new(tmp.path());
let result = resolver.resolve_import("com/example/Legacy", "Main.kt", tmp.path());
assert!(
result.is_some(),
"should resolve .java file for cross-language interop"
);
}
#[test]
fn test_resolver_external_not_found() {
let tmp = tempfile::tempdir().unwrap();
let resolver = KotlinResolver::new(tmp.path());
let result = resolver.resolve_import("org/external/Lib", "Main.kt", tmp.path());
assert!(result.is_none(), "external imports should return None");
}
#[test]
fn test_detect_jvm_source_roots() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src/main/kotlin");
std::fs::create_dir_all(&src).unwrap();
let test = tmp.path().join("src/test/kotlin");
std::fs::create_dir_all(&test).unwrap();
let roots = detect_jvm_source_roots(tmp.path());
assert!(
roots.len() >= 2,
"should detect src/main/kotlin and src/test/kotlin"
);
}
#[test]
fn test_detect_jvm_source_roots_fallback() {
let tmp = tempfile::tempdir().unwrap();
let roots = detect_jvm_source_roots(tmp.path());
assert_eq!(roots.len(), 1);
assert_eq!(roots[0], tmp.path().to_path_buf());
}
#[test]
fn test_empty_file() {
let result = parse("");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 0);
}
#[test]
fn test_malformed_file() {
let result = parse("fun broken( {");
assert!(result.has_errors);
}
#[test]
fn test_object_with_methods() {
let result = parse(
r#"object Utils {
fun helper(): String = "help"
val VERSION = "1.0"
}"#,
);
assert!(!result.has_errors);
let obj = result.symbols.iter().find(|s| s.name == "Utils").unwrap();
assert_eq!(obj.kind, SymbolKind::Class);
let method = result.symbols.iter().find(|s| s.name == "helper").unwrap();
assert_eq!(method.kind, SymbolKind::Method);
assert!(method.parent_index.is_some());
}
#[test]
fn test_abstract_class() {
let result = parse(
r#"abstract class Shape {
abstract fun area(): Double
}"#,
);
assert!(!result.has_errors);
let shape = result.symbols.iter().find(|s| s.name == "Shape").unwrap();
assert_eq!(shape.kind, SymbolKind::Class);
}
#[test]
fn test_private_top_level_function() {
let result = parse("private fun helper() {}");
assert!(!result.has_errors);
let funcs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "helper");
}
#[test]
fn test_const_val_property() {
let result = parse("const val MAX = 100");
assert!(!result.has_errors);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert_eq!(consts.len(), 1);
assert_eq!(consts[0].name, "MAX");
}
#[test]
fn test_generic_class() {
let result = parse("class Box<T>(val value: T) {}");
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 1);
assert_eq!(classes[0].name, "Box");
}
#[test]
fn test_suspend_function() {
let result = parse("suspend fun fetchData(): String { return \"data\" }");
assert!(!result.has_errors);
let funcs: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "fetchData");
}
#[test]
fn test_interface_with_methods() {
let result = parse(
r#"interface Repository {
fun findAll(): List<String>
fun save(item: String)
}"#,
);
assert!(!result.has_errors);
let iface = result
.symbols
.iter()
.find(|s| s.name == "Repository")
.unwrap();
assert_eq!(iface.kind, SymbolKind::Interface);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 2);
}
#[test]
fn test_enum_with_methods() {
let result = parse(
r#"enum class Planet {
EARTH,
MARS;
fun description(): String = name
}"#,
);
assert!(!result.has_errors);
let e = result.symbols.iter().find(|s| s.name == "Planet").unwrap();
assert_eq!(e.kind, SymbolKind::Enum);
let entries: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert!(entries.len() >= 2);
}
#[test]
fn test_multiple_classes_in_file() {
let result = parse(
r#"class Alpha {}
class Beta {}
class Gamma {}
"#,
);
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 3);
}
#[test]
fn test_open_class() {
let result = parse("open class Base {}");
assert!(!result.has_errors);
let base = result.symbols.iter().find(|s| s.name == "Base").unwrap();
assert_eq!(base.kind, SymbolKind::Class);
}
}