use crate::models::{CallIntent, EntityKind, ParsedEntity, ReferenceIntent};
use crate::pipeline::parser::utils::node_text;
use tree_sitter::Node;
pub(crate) fn collect_all_reference_intents_kotlin(
node: Node<'_>,
source: &[u8],
intents: &mut Vec<(ReferenceIntent, usize)>,
) {
let byte_pos = node.start_byte();
let line = node.start_position().row + 1;
match node.kind() {
"call_expression" => {
let call_intents = extract_single_call_intent_kotlin(node, source);
for call in call_intents {
intents.push((
ReferenceIntent::Call {
method: call.method,
receiver: call.receiver,
line,
arg_count: call.arg_count,
},
byte_pos,
));
}
}
"modifiers" => {
let mut annotation_refs = Vec::new();
extract_identifiers_from_annotation(node, source, &mut annotation_refs, line);
for ref_intent in annotation_refs {
intents.push((ref_intent, byte_pos));
}
}
"type_identifier" | "simple_identifier" | "identifier" => {
let type_name = node_text(node, source);
if type_name.chars().next().is_some_and(|c| c.is_uppercase())
&& !is_kotlin_import_identifier(node)
{
intents.push((ReferenceIntent::TypeReference { type_name, line }, byte_pos));
}
}
"import" => {
collect_import_intents_kotlin(node, source, intents, byte_pos, line);
}
_ => {}
}
let mut child = node.child(0);
while let Some(c) = child {
collect_all_reference_intents_kotlin(c, source, intents);
child = c.next_sibling();
}
}
fn is_kotlin_import_identifier(node: Node<'_>) -> bool {
if node.parent().is_some_and(|p| p.kind() == "import") {
return true;
}
let mut current = node.parent();
while let Some(n) = current {
if n.kind() == "import_header" || n.kind() == "import" {
return true;
}
current = n.parent();
}
false
}
fn collect_import_intents_kotlin(
node: Node<'_>,
source: &[u8],
intents: &mut Vec<(ReferenceIntent, usize)>,
byte_pos: usize,
line: usize,
) {
let has_wildcard = node.children(&mut node.walk()).any(|c| c.kind() == "*");
if has_wildcard {
return;
}
let mut imported_name: Option<String> = None;
let mut child = node.child(0);
while let Some(c) = child {
if c.kind() == "as" {
break;
}
if c.kind() == "qualified_identifier" || c.kind() == "identifier" {
let text = node_text(c, source);
if let Some(last_segment) = text.split('.').next_back() {
imported_name = Some(last_segment.to_string());
}
}
child = c.next_sibling();
}
if let Some(name) = imported_name {
if name.chars().next().is_some_and(|c| c.is_uppercase()) {
intents.push((
ReferenceIntent::TypeReference {
type_name: name,
line,
},
byte_pos,
));
} else if !name.is_empty() {
intents.push((
ReferenceIntent::ValueReference {
value_name: name,
line,
},
byte_pos,
));
}
}
}
pub(crate) fn extract_annotation_references(
node: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
) {
let line = node.start_position().row + 1;
if node.kind() == "annotation" {
extract_identifiers_from_annotation(node, source, intents, line);
}
let mut child = node.child(0);
while let Some(c) = child {
extract_annotation_references(c, source, intents);
child = c.next_sibling();
}
}
fn extract_identifiers_from_annotation(
annotation_node: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
line: usize,
) {
let mut child = annotation_node.child(0);
while let Some(c) = child {
match c.kind() {
"simple_identifier" | "type_identifier" | "identifier" => {
let name = node_text(c, source);
if name.chars().next().is_some_and(|ch| ch.is_uppercase()) {
intents.push(ReferenceIntent::TypeReference {
type_name: name,
line,
});
}
}
_ => {
extract_identifiers_from_annotation(c, source, intents, line);
}
}
child = c.next_sibling();
}
}
pub(crate) fn extract_type_references(
node: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
) {
let line = node.start_position().row + 1;
if matches!(node.kind(), "type_identifier" | "user_type" | "identifier") {
let type_name = node_text(node, source);
if type_name.chars().next().is_some_and(|c| c.is_uppercase()) {
intents.push(ReferenceIntent::TypeReference { type_name, line });
}
}
let mut child = node.child(0);
while let Some(c) = child {
extract_type_references(c, source, intents);
child = c.next_sibling();
}
}
pub(crate) fn extract_class_inheritance_kotlin(
class_node: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
) {
let is_interface = {
let text = node_text(class_node, source);
text.split_whitespace()
.find(|t| {
!matches!(
*t,
"public"
| "private"
| "protected"
| "internal"
| "abstract"
| "open"
| "sealed"
| "annotation"
| "final"
| "override"
)
})
.is_some_and(|kw| kw == "interface")
};
let mut child = class_node.child(0);
while let Some(c) = child {
if c.kind() == "delegation_specifiers" {
extract_delegation_specifiers(c, source, intents, is_interface);
return;
}
child = c.next_sibling();
}
}
fn extract_delegation_specifiers(
specifiers_node: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
is_interface: bool,
) {
let mut child = specifiers_node.child(0);
while let Some(c) = child {
if c.kind() == "delegation_specifier" {
extract_single_delegation(c, source, intents, is_interface);
}
child = c.next_sibling();
}
}
fn extract_single_delegation(
specifier: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
is_interface: bool,
) {
let line = specifier.start_position().row + 1;
let type_name = extract_delegation_type_name(specifier, source);
if type_name.is_none() {
return;
}
let type_name = type_name.unwrap();
let has_constructor_invocation = specifier
.child(0)
.is_some_and(|c| c.kind() == "constructor_invocation");
let has_explicit_delegation = specifier
.children(&mut specifier.walk())
.any(|c| c.kind() == "explicit_delegation");
if has_constructor_invocation {
intents.push(ReferenceIntent::Extends {
parent: type_name,
line,
});
} else if has_explicit_delegation || !is_interface {
intents.push(ReferenceIntent::Implements {
interface: type_name,
line,
});
} else {
intents.push(ReferenceIntent::Extends {
parent: type_name,
line,
});
}
}
fn extract_delegation_type_name(specifier: Node<'_>, source: &[u8]) -> Option<String> {
let mut user_type = None;
let mut child = specifier.child(0);
while let Some(c) = child {
match c.kind() {
"user_type" => {
user_type = Some(c);
break;
}
"constructor_invocation" => {
let mut ci_child = c.child(0);
while let Some(cc) = ci_child {
if cc.kind() == "user_type" {
user_type = Some(cc);
break;
}
ci_child = cc.next_sibling();
}
break;
}
"explicit_delegation" => {
let mut ed_child = c.child(0);
while let Some(ec) = ed_child {
if ec.kind() == "user_type" {
user_type = Some(ec);
break;
}
ed_child = ec.next_sibling();
}
break;
}
_ => {}
}
child = c.next_sibling();
}
if let Some(ut) = user_type {
let mut ident_child = ut.child(0);
while let Some(ic) = ident_child {
if matches!(
ic.kind(),
"identifier" | "type_identifier" | "simple_identifier"
) {
return Some(node_text(ic, source));
}
ident_child = ic.next_sibling();
}
}
None
}
pub(crate) fn extract_reference_intents_kotlin(
node: Node<'_>,
source: &[u8],
intents: &mut Vec<ReferenceIntent>,
) {
let mut call_intents = Vec::new();
extract_call_intents_kotlin(node, source, &mut call_intents);
for call in call_intents {
intents.push(ReferenceIntent::Call {
method: call.method,
receiver: call.receiver,
line: call.line,
arg_count: call.arg_count,
});
}
}
pub(crate) fn extract_call_intents_kotlin(
node: Node<'_>,
source: &[u8],
intents: &mut Vec<CallIntent>,
) {
intents.extend(extract_single_call_intent_kotlin(node, source));
let mut child = node.child(0);
while let Some(c) = child {
extract_call_intents_kotlin(c, source, intents);
child = c.next_sibling();
}
}
fn extract_receiver_and_method(
node: Node<'_>,
source: &[u8],
receiver: &mut Option<String>,
method: &mut Option<String>,
) {
if node.kind() == "navigation_expression" {
let count = node.child_count();
if count >= 3 {
let last_child = node.child(count as u32 - 1).unwrap();
let first_child = node.child(0).unwrap();
if matches!(last_child.kind(), "simple_identifier" | "identifier") {
*method = Some(node_text(last_child, source));
}
if first_child.kind() == "navigation_expression" {
*receiver = Some(node_text(first_child, source));
} else if matches!(
first_child.kind(),
"simple_identifier" | "identifier" | "this"
) {
*receiver = Some(node_text(first_child, source));
}
return;
}
}
let mut child = node.child(0);
while let Some(c) = child {
match c.kind() {
"simple_identifier" | "identifier" => {
if receiver.is_none() {
*receiver = Some(node_text(c, source));
} else {
*method = Some(node_text(c, source));
}
}
"this" => {
*receiver = Some("this".to_string());
}
"navigation_suffix" => {
if let Some(nav_child) = c.child(0)
&& matches!(nav_child.kind(), "simple_identifier" | "identifier")
{
*method = Some(node_text(nav_child, source));
}
}
"navigation_expression" | "postfix_expression" => {
extract_receiver_and_method(c, source, receiver, method);
}
_ => {}
}
child = c.next_sibling();
}
}
pub(crate) fn extract_single_call_intent_kotlin(node: Node<'_>, source: &[u8]) -> Vec<CallIntent> {
let mut intents = Vec::new();
if node.kind() == "call_expression" {
let mut method_name: Option<String> = None;
let mut receiver: Option<String> = None;
let line = node.start_position().row + 1;
let mut child = node.child(0);
while let Some(c) = child {
let kind = c.kind();
match kind {
"simple_identifier" | "identifier" => {
method_name = Some(node_text(c, source));
}
"postfix_expression" | "navigation_expression" => {
extract_receiver_and_method(c, source, &mut receiver, &mut method_name);
}
"navigation_suffix" => {
if let Some(nav_child) = c.child(0)
&& matches!(nav_child.kind(), "simple_identifier" | "identifier")
{
method_name = Some(node_text(nav_child, source));
}
}
_ => {}
}
child = c.next_sibling();
}
if let Some(method) = method_name {
intents.push(CallIntent {
method,
receiver,
line,
arg_count: None,
});
}
}
intents
}
#[expect(
clippy::too_many_arguments,
reason = "function is verbose but correct — extraction deferred"
)]
pub(crate) fn extract_anonymous_object_implementations(
root: Node<'_>,
source: &[u8],
file_path: &str,
repo_name: &str,
existing_entities: &[ParsedEntity],
out: &mut Vec<ParsedEntity>,
) {
extract_anonymous_objects_recursive(
root,
source,
file_path,
repo_name,
existing_entities,
out,
&mut 0u32,
);
}
#[expect(
clippy::too_many_arguments,
reason = "function is verbose but correct — extraction deferred"
)]
fn extract_anonymous_objects_recursive(
node: Node<'_>,
source: &[u8],
file_path: &str,
repo_name: &str,
existing_entities: &[ParsedEntity],
out: &mut Vec<ParsedEntity>,
counter: &mut u32,
) {
if node.kind() == "object_literal" {
let has_delegation = node
.children(&mut node.walk())
.any(|c| c.kind() == "delegation_specifiers");
if has_delegation {
let line = node.start_position().row + 1;
let end_line = node.end_position().row + 1;
let enclosing_fqn = find_enclosing_fqn(line, existing_entities);
let name = "<anonymous>".to_string();
let fqn = if let Some(ref enclosing) = enclosing_fqn {
format!("{enclosing}.<anonymous@{line}>")
} else {
format!("<anonymous@{line}>")
};
let mut intents = Vec::new();
let mut child = node.child(0);
while let Some(c) = child {
if c.kind() == "delegation_specifiers" {
extract_delegation_specifiers(c, source, &mut intents, false);
break;
}
child = c.next_sibling();
}
if !intents.is_empty() {
*counter += 1;
let mut entity = ParsedEntity::new(
&name,
EntityKind::KotlinObject,
&fqn,
None,
None,
"kotlin",
file_path,
line,
end_line,
enclosing_fqn,
repo_name,
);
entity.reference_intents = intents;
out.push(entity);
}
}
return;
}
let mut child = node.child(0);
while let Some(c) = child {
extract_anonymous_objects_recursive(
c,
source,
file_path,
repo_name,
existing_entities,
out,
counter,
);
child = c.next_sibling();
}
}
fn find_enclosing_fqn(line: usize, entities: &[ParsedEntity]) -> Option<String> {
let mut best: Option<&ParsedEntity> = None;
for e in entities {
if e.name == "<anonymous>" {
continue; }
if line >= e.start_line && line <= e.end_line {
match best {
None => best = Some(e),
Some(b) => {
let b_range = b.end_line - b.start_line;
let e_range = e.end_line - e.start_line;
if e_range < b_range {
best = Some(e);
}
}
}
}
}
best.map(|e| e.fqn.clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_call_intent_simple_function() {
let code = "fun main() { println(\"Hello\") }";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let mut intents = Vec::new();
extract_call_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
assert!(!tree.root_node().has_error());
}
#[test]
fn test_extract_call_intent_with_receiver() {
let code = "fun main() { obj.method() }";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let mut intents = Vec::new();
extract_call_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
assert!(!tree.root_node().has_error());
}
#[test]
fn test_extract_class_declaration() {
let code = "class MyClass { }";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let root = tree.root_node();
let mut found_class = false;
let mut child = root.child(0);
while let Some(c) = child {
if c.kind() == "class_declaration" {
found_class = true;
break;
}
child = c.next_sibling();
}
assert!(found_class, "Class declaration not found in AST");
}
#[test]
fn test_extract_function_declaration() {
let code = "fun myFunction() { }";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let root = tree.root_node();
let mut found_func = false;
let mut child = root.child(0);
while let Some(c) = child {
if c.kind() == "function_declaration" {
found_func = true;
break;
}
child = c.next_sibling();
}
assert!(found_func, "Function declaration not found in AST");
}
#[test]
fn test_extract_property_declaration() {
let code = "val myProperty: String = \"test\"";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let root = tree.root_node();
let mut found_property = false;
let mut child = root.child(0);
while let Some(c) = child {
if c.kind() == "property_declaration" {
found_property = true;
break;
}
child = c.next_sibling();
}
assert!(found_property, "Property declaration not found in AST");
}
#[test]
fn test_extract_call_intent_navigation() {
let code = "fun main() { userService.getUser(1) }";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let mut intents = Vec::new();
extract_call_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
assert_eq!(intents.len(), 1);
assert_eq!(intents[0].method, "getUser");
assert_eq!(intents[0].receiver, Some("userService".to_string()));
}
#[test]
fn test_extract_call_intent_chained_navigation() {
let code = "fun main() { Config.instance.getUser(1) }";
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
let tree = parser.parse(code, None).unwrap();
let mut intents = Vec::new();
extract_call_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
assert_eq!(intents.len(), 1);
assert_eq!(intents[0].method, "getUser");
assert!(intents[0].receiver.as_ref().unwrap().contains("Config"));
}
fn get_class_declaration(root: Node<'_>) -> Node<'_> {
let mut child = root.child(0);
while let Some(c) = child {
if c.kind() == "class_declaration" {
return c;
}
child = c.next_sibling();
}
panic!("class_declaration not found in AST");
}
fn parse_kotlin(code: &str) -> tree_sitter::Tree {
let lang = tree_sitter_kotlin_ng::LANGUAGE.into();
let mut parser = tree_sitter::Parser::new();
parser.set_language(&lang).unwrap();
parser.parse(code, None).unwrap()
}
#[test]
fn test_extract_class_inheritance_class_with_parent_and_interface() {
let source = "class Foo : Base(), Iface {\n fun bar() {}\n}";
let tree = parse_kotlin(source);
let node = get_class_declaration(tree.root_node());
let mut intents = Vec::new();
extract_class_inheritance_kotlin(node, source.as_bytes(), &mut intents);
let extends = crate::pipeline::parser::test_utils::collect_extends(&intents);
let implements = crate::pipeline::parser::test_utils::collect_implements(&intents);
assert_eq!(
extends,
&["Base"],
"Expected EXTENDS Base (constructor invocation)"
);
assert_eq!(
implements,
&["Iface"],
"Expected IMPLEMENTS Iface (no constructor)"
);
}
#[test]
fn test_extract_class_inheritance_interface_extends() {
let source = "interface Bar : Iface {\n fun baz()\n}";
let tree = parse_kotlin(source);
let node = get_class_declaration(tree.root_node());
let mut intents = Vec::new();
extract_class_inheritance_kotlin(node, source.as_bytes(), &mut intents);
let extends = crate::pipeline::parser::test_utils::collect_extends(&intents);
assert_eq!(
extends,
&["Iface"],
"Interface extending another interface should use EXTENDS"
);
}
#[test]
fn test_extract_class_inheritance_no_inheritance() {
let source = "class Foo {\n fun bar() {}\n}";
let tree = parse_kotlin(source);
let node = get_class_declaration(tree.root_node());
let mut intents = Vec::new();
extract_class_inheritance_kotlin(node, source.as_bytes(), &mut intents);
assert!(
intents.is_empty(),
"Expected no inheritance intents for class without supertypes"
);
}
#[test]
fn test_extract_class_inheritance_delegation() {
let source = "class Foo : Iface by delegate {\n fun bar() {}\n}";
let tree = parse_kotlin(source);
let node = get_class_declaration(tree.root_node());
let mut intents = Vec::new();
extract_class_inheritance_kotlin(node, source.as_bytes(), &mut intents);
let implements = crate::pipeline::parser::test_utils::collect_implements(&intents);
assert_eq!(
implements,
&["Iface"],
"Expected IMPLEMENTS Iface via delegation"
);
}
#[test]
fn test_extract_anonymous_object_implements() {
let source = "class Foo {\n fun bar() {\n val x = object : Iface {\n fun foo() {}\n }\n }\n}";
let tree = parse_kotlin(source);
let existing = vec![
ParsedEntity::new(
"Foo",
EntityKind::KotlinClass,
"Foo",
None,
None,
"kotlin",
"test.kt",
1,
7,
None,
"test",
),
ParsedEntity::new(
"bar",
EntityKind::KotlinMethod,
"Foo.bar",
None,
None,
"kotlin",
"test.kt",
2,
6,
Some("Foo".to_string()),
"test",
),
];
let mut out = Vec::new();
extract_anonymous_object_implementations(
tree.root_node(),
source.as_bytes(),
"test.kt",
"test",
&existing,
&mut out,
);
assert_eq!(out.len(), 1, "Expected 1 anonymous object entity");
assert_eq!(out[0].name, "<anonymous>");
assert_eq!(out[0].kind, EntityKind::KotlinObject);
assert!(
out[0].fqn.contains("Foo.bar"),
"FQN should contain enclosing method: {}",
out[0].fqn
);
assert!(
out[0].fqn.contains("<anonymous@"),
"FQN should contain <anonymous@LINE>"
);
let implements =
crate::pipeline::parser::test_utils::collect_implements(&out[0].reference_intents);
assert_eq!(
implements,
&["Iface"],
"Expected IMPLEMENTS Iface from anonymous object"
);
}
#[test]
fn test_extract_anonymous_object_no_inheritance() {
let source = "fun main() { val x = object { fun foo() {} } }";
let tree = parse_kotlin(source);
let existing = vec![ParsedEntity::new(
"main",
EntityKind::KotlinFunction,
"main",
None,
None,
"kotlin",
"test.kt",
1,
1,
None,
"test",
)];
let mut out = Vec::new();
extract_anonymous_object_implementations(
tree.root_node(),
source.as_bytes(),
"test.kt",
"test",
&existing,
&mut out,
);
assert!(
out.is_empty(),
"Anonymous object without inheritance should not create entity"
);
}
#[test]
fn test_import_class_emits_type_reference() {
let code = "import com.example.Foo\n\nclass Bar {}";
let tree = crate::pipeline::parser::test_utils::parse_kotlin_snippet(code)
.expect("Failed to parse Kotlin code");
let mut intents = Vec::new();
collect_all_reference_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
let has_foo = intents.iter().any(|(i, _)| match i {
ReferenceIntent::TypeReference { type_name, .. } => type_name == "Foo",
_ => false,
});
assert!(
has_foo,
"Should emit TypeReference for Foo from import, got: {:?}",
intents
);
}
#[test]
fn test_import_aliased_uses_original_name() {
let code = "import com.example.Foo as Bar\n\nclass Baz {}";
let tree = crate::pipeline::parser::test_utils::parse_kotlin_snippet(code)
.expect("Failed to parse Kotlin code");
let mut intents = Vec::new();
collect_all_reference_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
let has_foo = intents.iter().any(|(i, _)| match i {
ReferenceIntent::TypeReference { type_name, .. } => type_name == "Foo",
_ => false,
});
let has_bar = intents.iter().any(|(i, _)| match i {
ReferenceIntent::TypeReference { type_name, .. } => type_name == "Bar",
_ => false,
});
assert!(
has_foo,
"Should emit TypeReference for Foo (original), got: {:?}",
intents
);
assert!(
!has_bar,
"Should NOT emit TypeReference for Bar (alias), got: {:?}",
intents
);
}
#[test]
fn test_import_wildcard_ignored() {
let code = "import com.example.*\n\nclass Bar {}";
let tree = crate::pipeline::parser::test_utils::parse_kotlin_snippet(code)
.expect("Failed to parse Kotlin code");
let mut intents = Vec::new();
collect_all_reference_intents_kotlin(tree.root_node(), code.as_bytes(), &mut intents);
let import_type_refs: Vec<_> = intents.iter().filter(|(i, _)| {
matches!(i, ReferenceIntent::TypeReference { type_name, .. } if type_name == "example")
}).collect();
assert!(
import_type_refs.is_empty(),
"Wildcard import should not emit type refs for package name, got: {:?}",
import_type_refs
);
}
}