use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc};
use tree_sitter::{Node, Parser, TreeCursor};
use uuid::Uuid;
use crate::graph::types::{RelationType, Relationship, Symbol, SymbolType};
use crate::parser::python::{FileParseResult, RawImport};
pub fn parse_java_file(
file_path: &str,
source: &str,
project: &str,
file_mtime: DateTime<Utc>,
) -> FileParseResult {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_java::LANGUAGE.into())
.expect("failed to load Java grammar");
let Some(tree) = parser.parse(source, None) else {
tracing::warn!("tree-sitter failed to parse {file_path}");
return FileParseResult::default();
};
let source_bytes = source.as_bytes();
let root = tree.root_node();
let mut ctx = ParseContext {
file_path,
project,
file_mtime,
result: FileParseResult::default(),
name_to_id: HashMap::new(),
imported_names: HashSet::new(),
class_fields: HashMap::new(),
};
let file_symbol_id = Uuid::new_v4();
ctx.result.symbols.push(Symbol {
id: file_symbol_id,
name: file_path.to_string(),
symbol_type: SymbolType::File,
file_path: file_path.to_string(),
start_line: Some(1),
end_line: Some(source.lines().count() as i32),
language: "java".to_string(),
project: project.to_string(),
signature: None,
file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
let mut cursor = root.walk();
collect_imports(&root, source_bytes, &mut ctx, &mut cursor);
let mut cursor2 = root.walk();
collect_definitions(
&root,
file_symbol_id,
None,
source_bytes,
&mut ctx,
&mut cursor2,
);
let mut cursor3 = root.walk();
collect_calls(&root, source_bytes, &mut ctx, &mut cursor3);
ctx.result
}
struct ParseContext<'a> {
file_path: &'a str,
project: &'a str,
file_mtime: DateTime<Utc>,
result: FileParseResult,
name_to_id: HashMap<String, Uuid>,
imported_names: HashSet<String>,
class_fields: HashMap<(Uuid, String), Uuid>,
}
fn collect_imports<'a>(
node: &Node<'a>,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
if child.kind() == "import_declaration" {
process_import(&child, source, ctx);
}
}
}
fn process_import(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let raw_text = node_text(node, source);
let stripped = raw_text
.trim_start_matches("import")
.trim()
.trim_start_matches("static")
.trim()
.trim_end_matches(';')
.trim();
if stripped.is_empty() {
return;
}
if !stripped.ends_with('*') {
let simple = stripped.split('.').last().unwrap_or(stripped);
ctx.imported_names.insert(simple.to_string());
}
let file_id = ctx.result.symbols[0].id;
ctx.result.raw_imports.push(RawImport {
source_id: file_id,
module_raw: stripped.to_string(),
is_relative: false,
dot_count: 0,
module_path: stripped.to_string(),
});
let target_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, stripped.as_bytes());
ctx.result.relationships.push(Relationship {
source_id: file_id,
target_id,
rel_type: RelationType::Imports,
confidence: 0.3,
});
}
fn is_type_decl(kind: &str) -> bool {
matches!(
kind,
"class_declaration"
| "interface_declaration"
| "enum_declaration"
| "record_declaration"
| "annotation_type_declaration"
)
}
fn collect_definitions<'a>(
node: &Node<'a>,
parent_id: Uuid,
enclosing_class: Option<Uuid>,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
let kind = child.kind();
if is_type_decl(kind) {
let class_id = process_type_decl(&child, parent_id, source, ctx);
if let Some(body) = child.child_by_field_name("body") {
let mut inner = body.walk();
collect_definitions(&body, class_id, Some(class_id), source, ctx, &mut inner);
}
} else if kind == "method_declaration" || kind == "constructor_declaration" {
if let Some(class_id) = enclosing_class {
process_method(&child, class_id, source, ctx);
}
} else if kind == "field_declaration" {
if let Some(class_id) = enclosing_class {
process_field(&child, class_id, source, ctx);
}
} else if kind == "block" || kind == "class_body" || kind == "interface_body" || kind == "enum_body" {
let mut inner = child.walk();
collect_definitions(&child, parent_id, enclosing_class, source, ctx, &mut inner);
} else {
let mut inner = child.walk();
collect_definitions(&child, parent_id, enclosing_class, source, ctx, &mut inner);
}
}
}
fn process_type_decl(
node: &Node<'_>,
parent_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) -> Uuid {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "<anonymous>".to_string());
let start_line = node.start_position().row as i32 + 1;
let end_line = node.end_position().row as i32 + 1;
let id = Uuid::new_v4();
ctx.name_to_id.insert(name.clone(), id);
ctx.result.symbols.push(Symbol {
id,
name: name.clone(),
symbol_type: SymbolType::Class,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "java".to_string(),
project: ctx.project.to_string(),
signature: Some(build_type_signature(node, &name, source)),
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
ctx.result.relationships.push(Relationship {
source_id: parent_id,
target_id: id,
rel_type: RelationType::Defines,
confidence: 1.0,
});
extract_extends(node, id, source, ctx);
extract_implements(node, id, source, ctx);
id
}
fn extract_extends(node: &Node<'_>, class_id: Uuid, source: &[u8], ctx: &mut ParseContext<'_>) {
for field in &["superclass", "extends_interfaces"] {
if let Some(super_node) = node.child_by_field_name(field) {
emit_type_list_inherits(&super_node, class_id, source, ctx);
}
}
}
fn extract_implements(node: &Node<'_>, class_id: Uuid, source: &[u8], ctx: &mut ParseContext<'_>) {
if let Some(impl_node) = node.child_by_field_name("interfaces") {
emit_type_list_inherits(&impl_node, class_id, source, ctx);
}
}
fn emit_type_list_inherits(
node: &Node<'_>,
class_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let mut cursor = node.walk();
emit_inherits_for_node(node, class_id, source, ctx, &mut cursor);
}
fn emit_inherits_for_node<'a>(
node: &Node<'a>,
class_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
match child.kind() {
"type_identifier" => {
let base_name = node_text(&child, source);
if base_name.is_empty() {
continue;
}
let target_id = ctx
.name_to_id
.get(&base_name)
.copied()
.unwrap_or_else(|| Uuid::new_v5(&Uuid::NAMESPACE_OID, base_name.as_bytes()));
let confidence = if ctx.name_to_id.contains_key(&base_name) {
1.0
} else if ctx.imported_names.contains(&base_name) {
0.8
} else {
0.5
};
ctx.result.relationships.push(Relationship {
source_id: class_id,
target_id,
rel_type: RelationType::Inherits,
confidence,
});
}
_ => {
let mut inner = child.walk();
emit_inherits_for_node(&child, class_id, source, ctx, &mut inner);
}
}
}
}
fn process_method(
node: &Node<'_>,
class_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) -> Uuid {
let name = node
.child_by_field_name("name")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "<constructor>".to_string());
let start_line = node.start_position().row as i32 + 1;
let end_line = node.end_position().row as i32 + 1;
let id = Uuid::new_v4();
ctx.name_to_id.insert(name.clone(), id);
ctx.result.symbols.push(Symbol {
id,
name: name.clone(),
symbol_type: SymbolType::Method,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "java".to_string(),
project: ctx.project.to_string(),
signature: Some(build_method_signature(node, &name, source)),
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: None,
moniker: None,
});
ctx.result.relationships.push(Relationship {
source_id: class_id,
target_id: id,
rel_type: RelationType::Defines,
confidence: 1.0,
});
collect_type_annotations(node, id, source, ctx);
id
}
fn process_field(
node: &Node<'_>,
class_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
if child.kind() != "variable_declarator" {
continue;
}
let Some(name_node) = child.child_by_field_name("name") else {
continue;
};
let name = node_text(&name_node, source);
if name.is_empty() {
continue;
}
let start_line = child.start_position().row as i32 + 1;
let end_line = child.end_position().row as i32 + 1;
let id = Uuid::new_v4();
ctx.class_fields.insert((class_id, name.clone()), id);
ctx.result.symbols.push(Symbol {
id,
name,
symbol_type: SymbolType::Field,
file_path: ctx.file_path.to_string(),
start_line: Some(start_line),
end_line: Some(end_line),
language: "java".to_string(),
project: ctx.project.to_string(),
signature: None,
file_mtime: ctx.file_mtime,
layer: None,
parent_symbol_id: Some(class_id),
moniker: None,
});
ctx.result.relationships.push(Relationship {
source_id: class_id,
target_id: id,
rel_type: RelationType::Defines,
confidence: 1.0,
});
}
}
fn collect_type_annotations(
method_node: &Node<'_>,
method_id: Uuid,
source: &[u8],
ctx: &mut ParseContext<'_>,
) {
let mut type_names: Vec<String> = Vec::new();
if let Some(params) = method_node.child_by_field_name("parameters") {
let mut cursor = params.walk();
for param in params.named_children(&mut cursor) {
if let Some(type_node) = param.child_by_field_name("type") {
extract_type_identifiers(&type_node, source, &mut type_names);
}
}
}
if let Some(return_type) = method_node.child_by_field_name("type") {
extract_type_identifiers(&return_type, source, &mut type_names);
}
for type_name in type_names {
if is_builtin_type(&type_name) {
continue;
}
let (target_id, confidence) = if let Some(&id) = ctx.name_to_id.get(&type_name) {
(id, 1.0_f32)
} else if ctx.imported_names.contains(&type_name) {
(Uuid::new_v5(&Uuid::NAMESPACE_OID, type_name.as_bytes()), 0.8)
} else {
(Uuid::new_v5(&Uuid::NAMESPACE_OID, type_name.as_bytes()), 0.5)
};
ctx.result.relationships.push(Relationship {
source_id: method_id,
target_id,
rel_type: RelationType::UsesType,
confidence,
});
}
}
fn extract_type_identifiers(node: &Node<'_>, source: &[u8], out: &mut Vec<String>) {
match node.kind() {
"type_identifier" => {
let name = node_text(node, source);
if !name.is_empty() {
out.push(name);
}
}
"scoped_type_identifier" => {
let last_idx = node.named_child_count().saturating_sub(1) as u32;
if let Some(last) = node.named_child(last_idx) {
if last.kind() == "type_identifier" || last.kind() == "identifier" {
let name = node_text(&last, source);
if !name.is_empty() {
out.push(name);
}
}
}
}
"generic_type" => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_type_identifiers(&child, source, out);
}
}
"array_type" => {
if let Some(elem) = node.child_by_field_name("element") {
extract_type_identifiers(&elem, source, out);
} else {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_type_identifiers(&child, source, out);
}
}
}
"type_arguments" | "wildcard" => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_type_identifiers(&child, source, out);
}
}
_ => {
let mut cursor = node.walk();
for child in node.named_children(&mut cursor) {
extract_type_identifiers(&child, source, out);
}
}
}
}
fn is_builtin_type(name: &str) -> bool {
matches!(
name,
"int" | "long" | "short" | "byte" | "float" | "double" | "boolean" | "char" | "void"
| "String" | "Object" | "Integer" | "Long" | "Short" | "Byte" | "Float" | "Double"
| "Boolean" | "Character" | "Void" | "Class" | "Number" | "Comparable" | "Iterable"
| "Runnable" | "Callable"
| "Throwable" | "Exception" | "RuntimeException" | "Error"
| "List" | "Map" | "Set" | "Collection" | "ArrayList" | "HashMap" | "HashSet"
| "Optional" | "Stream" | "Iterator"
| "Enum" | "Override" | "Deprecated" | "SuppressWarnings" | "FunctionalInterface"
)
}
fn build_type_signature(node: &Node<'_>, name: &str, source: &[u8]) -> String {
let keyword = match node.kind() {
"interface_declaration" => "interface",
"enum_declaration" => "enum",
"record_declaration" => "record",
"annotation_type_declaration" => "@interface",
_ => "class",
};
let mut sig = format!("{keyword} {name}");
if let Some(super_node) = node.child_by_field_name("superclass") {
let super_text = node_text(&super_node, source);
let cleaned = super_text
.trim_start_matches("extends")
.trim();
if !cleaned.is_empty() {
sig.push_str(&format!(" extends {cleaned}"));
}
}
if let Some(impl_node) = node.child_by_field_name("interfaces") {
let impl_text = node_text(&impl_node, source);
let cleaned = impl_text
.trim_start_matches("implements")
.trim();
if !cleaned.is_empty() {
sig.push_str(&format!(" implements {cleaned}"));
}
}
sig
}
fn build_method_signature(node: &Node<'_>, name: &str, source: &[u8]) -> String {
let modifiers = node
.child_by_field_name("modifiers")
.map(|n| format!("{} ", node_text(&n, source)))
.unwrap_or_default();
let return_type = node
.child_by_field_name("type")
.map(|n| format!("{} ", node_text(&n, source)))
.unwrap_or_default();
let params = node
.child_by_field_name("parameters")
.map(|n| node_text(&n, source))
.unwrap_or_else(|| "()".to_string());
format!("{modifiers}{return_type}{name}{params}")
.trim()
.to_string()
}
fn collect_calls<'a>(
node: &Node<'a>,
source: &[u8],
ctx: &mut ParseContext<'_>,
cursor: &mut TreeCursor<'a>,
) {
for child in node.children(cursor) {
if child.kind() == "method_invocation" {
process_call(&child, source, ctx);
}
if child.kind() == "field_access" {
process_field_read(&child, source, ctx);
}
let mut inner = child.walk();
collect_calls(&child, source, ctx, &mut inner);
}
}
fn process_call(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let callee_name = node_text(&name_node, source);
if callee_name.is_empty() {
return;
}
let has_object = node.child_by_field_name("object").is_some();
let is_this_call = node
.child_by_field_name("object")
.map(|obj| {
let text = node_text(&obj, source);
text == "this" || text == "super"
})
.unwrap_or(false);
let caller_id = find_enclosing_method(node, ctx);
let (target_id, confidence) = if let Some(&id) = ctx.name_to_id.get(&callee_name) {
(id, 1.0_f32)
} else if ctx.imported_names.contains(&callee_name) {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.8,
)
} else if is_this_call {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.6,
)
} else if has_object {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.5,
)
} else {
(
Uuid::new_v5(&Uuid::NAMESPACE_OID, callee_name.as_bytes()),
0.5,
)
};
let source_id = caller_id.unwrap_or(ctx.result.symbols[0].id);
ctx.result.relationships.push(Relationship {
source_id,
target_id,
rel_type: RelationType::Calls,
confidence,
});
}
fn find_enclosing_method(call_node: &Node<'_>, ctx: &ParseContext<'_>) -> Option<Uuid> {
let call_start = call_node.start_position().row as i32 + 1;
let mut best: Option<(Uuid, i32, i32)> = None;
for sym in &ctx.result.symbols {
if sym.symbol_type != SymbolType::Method {
continue;
}
if sym.file_path != ctx.file_path {
continue;
}
let (start, end) = match (sym.start_line, sym.end_line) {
(Some(s), Some(e)) => (s, e),
_ => continue,
};
if call_start >= start && call_start <= end {
let range = end - start;
let current_best_range = best.map(|(_, s, e)| e - s).unwrap_or(i32::MAX);
if range < current_best_range {
best = Some((sym.id, start, end));
}
}
}
best.map(|(id, _, _)| id)
}
fn find_enclosing_class(node: &Node<'_>, ctx: &ParseContext<'_>) -> Option<Uuid> {
let target = node.start_position().row as i32 + 1;
let mut best: Option<(Uuid, i32)> = None;
for sym in &ctx.result.symbols {
if sym.symbol_type != SymbolType::Class || sym.file_path != ctx.file_path {
continue;
}
let (Some(s), Some(e)) = (sym.start_line, sym.end_line) else {
continue;
};
if target >= s && target <= e {
let range = e - s;
if range < best.map(|(_, r)| r).unwrap_or(i32::MAX) {
best = Some((sym.id, range));
}
}
}
best.map(|(id, _)| id)
}
fn process_field_read(node: &Node<'_>, source: &[u8], ctx: &mut ParseContext<'_>) {
let Some(obj) = node.child_by_field_name("object") else {
return;
};
if node_text(&obj, source) != "this" {
return;
}
let Some(field_node) = node.child_by_field_name("field") else {
return;
};
let field_name = node_text(&field_node, source);
if field_name.is_empty() {
return;
}
if let Some(parent) = node.parent() {
if parent.kind() == "assignment_expression"
&& parent.child_by_field_name("left").map(|l| l.id()) == Some(node.id())
{
return;
}
}
let Some(class_id) = find_enclosing_class(node, ctx) else {
return;
};
let Some(&field_id) = ctx.class_fields.get(&(class_id, field_name.clone())) else {
return;
};
let source_id = find_enclosing_method(node, ctx)
.unwrap_or_else(|| ctx.result.symbols[0].id);
ctx.result.relationships.push(Relationship {
source_id,
target_id: field_id,
rel_type: RelationType::References,
confidence: 1.0,
});
}
fn node_text(node: &Node<'_>, source: &[u8]) -> String {
node.utf8_text(source)
.unwrap_or("")
.trim()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(src: &str) -> FileParseResult {
let now = chrono::Utc::now();
parse_java_file("Test.java", src, "test", now)
}
fn uses_type_rels(result: &FileParseResult) -> Vec<(uuid::Uuid, uuid::Uuid, f32)> {
result
.relationships
.iter()
.filter(|r| r.rel_type == RelationType::UsesType)
.map(|r| (r.source_id, r.target_id, r.confidence))
.collect()
}
fn method_id(result: &FileParseResult, name: &str) -> Option<uuid::Uuid> {
result
.symbols
.iter()
.find(|s| s.symbol_type == SymbolType::Method && s.name == name)
.map(|s| s.id)
}
#[test]
fn test_uses_type_param_annotation() {
let src = r#"
class OrderService {
public void processOrder(Order order) {}
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
assert!(!rels.is_empty(), "expected at least one UsesType relationship");
let process_id = method_id(&result, "processOrder").expect("processOrder not found");
assert!(
rels.iter().any(|(src_id, _, _)| *src_id == process_id),
"UsesType should originate from processOrder"
);
}
#[test]
fn test_uses_type_return_annotation() {
let src = r#"
class UserRepository {
public User findById(long id) { return null; }
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
let find_id = method_id(&result, "findById").expect("findById not found");
assert!(
rels.iter().any(|(src_id, _, _)| *src_id == find_id),
"UsesType should originate from findById for User return type"
);
}
#[test]
fn test_no_uses_type_for_builtins() {
let src = r#"
class Calculator {
public int add(int a, int b) { return a + b; }
public String format(double value) { return ""; }
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
assert!(
rels.is_empty(),
"primitive and String types should not produce UsesType, got: {rels:?}"
);
}
#[test]
fn test_uses_type_generic_type_param() {
let src = r#"
class CartService {
public List<Product> getItems(Cart cart) { return null; }
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
let get_id = method_id(&result, "getItems").expect("getItems not found");
let from_get: Vec<_> = rels.iter().filter(|(s, _, _)| *s == get_id).collect();
assert!(
from_get.len() >= 2,
"expected UsesType for Product and Cart (List is filtered), got {from_get:?}"
);
}
#[test]
fn test_uses_type_confidence_same_file() {
let src = r#"
class Payment {}
class PaymentService {
public void process(Payment p) {}
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
let process_id = method_id(&result, "process").expect("process not found");
let rel = rels
.iter()
.find(|(s, _, _)| *s == process_id)
.expect("no UsesType from process");
assert_eq!(rel.2, 1.0, "same-file type should have confidence 1.0");
}
#[test]
fn test_uses_type_confidence_external() {
let src = r#"
import com.example.Order;
class ShipmentService {
public void ship(Order o) {}
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
let ship_id = method_id(&result, "ship").expect("ship not found");
let rel = rels
.iter()
.find(|(s, _, _)| *s == ship_id)
.expect("no UsesType from ship");
assert_eq!(
rel.2, 0.8,
"imported type should have confidence 0.8"
);
}
#[test]
fn test_uses_type_array_type() {
let src = r#"
class FileProcessor {
public void process(Document[] docs) {}
}
"#;
let result = parse(src);
let rels = uses_type_rels(&result);
let proc_id = method_id(&result, "process").expect("process not found");
assert!(
rels.iter().any(|(s, _, _)| *s == proc_id),
"array element type Document should produce UsesType"
);
}
}