use std::path::{Path, PathBuf};
use tree_sitter::Language;
use astmap_core::SymbolKind;
use super::{
collect_type_refs_by_kind, detect_jvm_source_roots, find_enclosing_symbol, node_text,
parse_with_tree_sitter, symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol,
LanguageParser, LanguageResolver, LanguageSupport, ParseResult,
};
pub(super) fn lang() -> LanguageSupport {
LanguageSupport {
name: "java",
extensions: &["java"],
parser: &JavaParser,
resolver_factory: |root| Box::new(JavaResolver::new(root)),
config_files: &["pom.xml", "build.gradle", "build.gradle.kts"],
sibling_fn: java_sibling_expansion,
}
}
fn java_sibling_expansion(_rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
None
}
pub(crate) struct JavaParser;
impl LanguageParser for JavaParser {
fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
parse_java_file(source)
}
fn language_name(&self) -> &str {
"java"
}
}
fn java_language() -> Language {
tree_sitter_java::LANGUAGE.into()
}
fn parse_java_file(source: &str) -> ParseResult {
parse_with_tree_sitter(
source,
&java_language(),
|root, src, symbols, imports| {
extract_from_node(root, src, symbols, imports, None);
},
extract_calls,
|root, src, symbols| {
collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_java_builtin)
},
)
}
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_like(&node, source, SymbolKind::Class, parent_index) {
let idx = symbols.len();
symbols.push(sym);
recurse_into_body(&node, source, symbols, imports, idx);
return;
}
}
"interface_declaration" => {
if let Some(sym) =
extract_class_like(&node, source, SymbolKind::Interface, parent_index)
{
let idx = symbols.len();
symbols.push(sym);
recurse_into_body(&node, source, symbols, imports, idx);
return;
}
}
"enum_declaration" => {
if let Some(sym) = extract_class_like(&node, source, SymbolKind::Enum, parent_index) {
let idx = symbols.len();
symbols.push(sym);
recurse_into_enum_body(&node, source, symbols, imports, idx);
return;
}
}
"record_declaration" => {
if let Some(sym) = extract_class_like(&node, source, SymbolKind::Class, parent_index) {
let idx = symbols.len();
symbols.push(sym);
recurse_into_body(&node, source, symbols, imports, idx);
return;
}
}
"annotation_type_declaration" => {
if let Some(sym) =
extract_class_like(&node, source, SymbolKind::Annotation, parent_index)
{
let idx = symbols.len();
symbols.push(sym);
recurse_into_body(&node, source, symbols, imports, idx);
return;
}
}
"method_declaration" => {
if let Some(sym) = extract_method(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"constructor_declaration" => {
if let Some(sym) = extract_constructor(&node, source, parent_index) {
symbols.push(sym);
return;
}
}
"field_declaration" => {
extract_fields(&node, source, symbols, parent_index);
return;
}
"import_declaration" => {
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 extract_class_like(
node: &tree_sitter::Node,
source: &str,
kind: SymbolKind,
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, kind, node, source, parent_index))
}
fn recurse_into_body(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
parent_idx: usize,
) {
if let Some(body) = node.child_by_field_name("body") {
for i in 0..body.child_count() {
if let Some(child) = body.child(i as u32) {
extract_from_node(child, source, symbols, imports, Some(parent_idx));
}
}
}
}
fn recurse_into_enum_body(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
imports: &mut Vec<ExtractedImport>,
parent_idx: usize,
) {
if let Some(body) = node.child_by_field_name("body") {
for i in 0..body.child_count() {
if let Some(child) = body.child(i as u32) {
if child.kind() == "enum_constant" {
if let Some(sym) = extract_enum_constant(&child, source, parent_idx) {
symbols.push(sym);
}
} else {
extract_from_node(child, source, symbols, imports, Some(parent_idx));
}
}
}
}
}
fn extract_enum_constant(
node: &tree_sitter::Node,
source: &str,
parent_idx: usize,
) -> Option<ExtractedSymbol> {
let name_node = node.child_by_field_name("name").or_else(|| {
(0..node.child_count())
.filter_map(|i| node.child(i as u32))
.find(|c| c.kind() == "identifier")
})?;
let name = node_text(&name_node, source);
Some(symbol_from_node(
name,
SymbolKind::Const,
node,
source,
Some(parent_idx),
))
}
fn extract_method(
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::Method,
node,
source,
parent_index,
))
}
fn extract_constructor(
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::Method,
node,
source,
parent_index,
))
}
fn extract_fields(
node: &tree_sitter::Node,
source: &str,
symbols: &mut Vec<ExtractedSymbol>,
parent_index: Option<usize>,
) {
let kind = if has_static_final_modifiers(node, source) {
SymbolKind::Const
} else {
SymbolKind::Variable
};
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "variable_declarator" {
if let Some(name_node) = child.child_by_field_name("name") {
let name = node_text(&name_node, source);
symbols.push(symbol_from_node(name, kind, node, source, parent_index));
}
}
}
}
}
fn has_static_final_modifiers(node: &tree_sitter::Node, source: &str) -> bool {
for i in 0..node.child_count() {
if let Some(child) = node.child(i as u32) {
if child.kind() == "modifiers" {
let mut has_static = false;
let mut has_final = false;
for j in 0..child.child_count() {
if let Some(mod_child) = child.child(j as u32) {
let text = node_text(&mod_child, source);
if text == "static" {
has_static = true;
}
if text == "final" {
has_final = true;
}
}
}
return has_static && has_final;
}
}
}
false
}
fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
let is_static = (0..node.child_count())
.filter_map(|i| node.child(i as u32))
.any(|c| node_text(&c, source) == "static");
let has_asterisk = (0..node.child_count())
.filter_map(|i| node.child(i as u32))
.any(|c| c.kind() == "asterisk");
let path_text = (0..node.child_count())
.filter_map(|i| node.child(i as u32))
.find(|c| c.kind() == "scoped_identifier" || c.kind() == "identifier")
.map(|c| node_text(&c, source))?;
let slash_path = path_text.replace('.', "/");
if is_static {
if has_asterisk {
Some(ExtractedImport {
path: format!("{}::*", slash_path),
imported_symbols: vec![],
})
} else {
let (parent, member) = split_last_segment(&slash_path)?;
Some(ExtractedImport {
path: parent.to_string(),
imported_symbols: vec![member.to_string()],
})
}
} else if has_asterisk {
Some(ExtractedImport {
path: format!("{}::*", slash_path),
imported_symbols: vec![],
})
} else {
let last = slash_path.rsplit('/').next().unwrap_or(&slash_path);
Some(ExtractedImport {
path: slash_path.clone(),
imported_symbols: vec![last.to_string()],
})
}
}
fn split_last_segment(path: &str) -> Option<(&str, &str)> {
let idx = path.rfind('/')?;
Some((&path[..idx], &path[idx + 1..]))
}
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>,
) {
match node.kind() {
"method_invocation" => {
if let Some(name_node) = node.child_by_field_name("name") {
let callee_name = node_text(&name_node, source);
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,
});
}
}
}
"object_creation_expression" => {
if let Some(type_node) = node.child_by_field_name("type") {
let callee_name = node_text(&type_node, source);
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 is_java_builtin(name: &str) -> bool {
matches!(
name,
"int"
| "long"
| "float"
| "double"
| "boolean"
| "char"
| "byte"
| "short"
| "void"
| "String"
| "Object"
| "Integer"
| "Long"
| "Float"
| "Double"
| "Boolean"
| "Character"
| "Byte"
| "Short"
| "Void"
| "Number"
| "Class"
| "Enum"
| "Throwable"
| "Exception"
| "RuntimeException"
| "Error"
| "Override"
| "Deprecated"
| "SuppressWarnings"
| "FunctionalInterface"
| "SafeVarargs"
)
}
pub(crate) struct JavaResolver {
source_roots: Vec<PathBuf>,
}
impl JavaResolver {
fn new(root: &Path) -> Self {
let source_roots = detect_jvm_source_roots(root);
Self { source_roots }
}
}
impl LanguageResolver for JavaResolver {
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 rel_path = PathBuf::from(effective_path.replace('/', std::path::MAIN_SEPARATOR_STR));
for root in &self.source_roots {
let candidate = root.join(&rel_path).with_extension("java");
if candidate.exists() {
return Some(candidate);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(source: &str) -> ParseResult {
JavaParser.parse(source, &PathBuf::from("Test.java"))
}
#[test]
fn test_parse_class() {
let result = parse("public 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);
assert!(result.symbols[0].parent_index.is_none());
}
#[test]
fn test_parse_interface() {
let result = parse("public interface Service { void run(); }");
assert!(!result.has_errors);
let ifaces: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Interface)
.collect();
assert_eq!(ifaces.len(), 1);
assert_eq!(ifaces[0].name, "Service");
}
#[test]
fn test_parse_enum() {
let result = parse("public enum 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");
}
#[test]
fn test_parse_record() {
let result = parse("public record Point(int x, int y) {}");
assert!(!result.has_errors);
let records: Vec<_> = result
.symbols
.iter()
.filter(|s| s.name == "Point")
.collect();
assert_eq!(records.len(), 1);
assert_eq!(records[0].kind, SymbolKind::Class);
}
#[test]
fn test_parse_annotation_type() {
let result = parse("public @interface MyAnnotation {}");
assert!(!result.has_errors);
let annots: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Annotation)
.collect();
assert_eq!(annots.len(), 1);
assert_eq!(annots[0].name, "MyAnnotation");
}
#[test]
fn test_parse_method() {
let result = parse(
r#"public class Foo {
public void doStuff() {}
private int compute(int x) { return x * 2; }
}"#,
);
assert!(!result.has_errors);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 2);
assert_eq!(methods[0].name, "doStuff");
assert_eq!(methods[1].name, "compute");
let foo_idx = result.symbols.iter().position(|s| s.name == "Foo").unwrap();
assert_eq!(methods[0].parent_index, Some(foo_idx));
assert_eq!(methods[1].parent_index, Some(foo_idx));
}
#[test]
fn test_parse_constructor() {
let result = parse(
r#"public class Foo {
public Foo(int x) {}
}"#,
);
assert!(!result.has_errors);
let constructors: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method && s.name == "Foo")
.collect();
assert_eq!(constructors.len(), 1);
let foo_idx = result
.symbols
.iter()
.position(|s| s.kind == SymbolKind::Class && s.name == "Foo")
.unwrap();
assert_eq!(constructors[0].parent_index, Some(foo_idx));
}
#[test]
fn test_parse_field_variable() {
let result = parse(
r#"public class Foo {
private int count;
protected String name;
}"#,
);
assert!(!result.has_errors);
let vars: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Variable)
.collect();
assert_eq!(vars.len(), 2);
assert_eq!(vars[0].name, "count");
assert_eq!(vars[1].name, "name");
}
#[test]
fn test_parse_field_static_final_is_const() {
let result = parse(
r#"public class Config {
public static final int MAX_SIZE = 100;
public static final String DEFAULT_NAME = "foo";
}"#,
);
assert!(!result.has_errors);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert_eq!(consts.len(), 2);
assert_eq!(consts[0].name, "MAX_SIZE");
assert_eq!(consts[1].name, "DEFAULT_NAME");
}
#[test]
fn test_parse_field_static_only_is_variable() {
let result = parse(
r#"public class Foo {
private static int instanceCount;
}"#,
);
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, "instanceCount");
}
#[test]
fn test_parse_field_final_only_is_variable() {
let result = parse(
r#"public class Foo {
private final int id;
}"#,
);
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, "id");
}
#[test]
fn test_parse_enum_constants() {
let result = parse("public enum Status { ACTIVE, INACTIVE, PENDING }");
assert!(!result.has_errors);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert_eq!(consts.len(), 3);
assert_eq!(consts[0].name, "ACTIVE");
assert_eq!(consts[1].name, "INACTIVE");
assert_eq!(consts[2].name, "PENDING");
let enum_idx = result
.symbols
.iter()
.position(|s| s.kind == SymbolKind::Enum)
.unwrap();
for c in &consts {
assert_eq!(c.parent_index, Some(enum_idx));
}
}
#[test]
fn test_parse_nested_class() {
let result = parse(
r#"public class Outer {
public class Inner {
public void innerMethod() {}
}
}"#,
);
assert!(!result.has_errors);
let outer_idx = result
.symbols
.iter()
.position(|s| s.name == "Outer")
.unwrap();
let inner = result.symbols.iter().find(|s| s.name == "Inner").unwrap();
assert_eq!(inner.kind, SymbolKind::Class);
assert_eq!(inner.parent_index, Some(outer_idx));
let inner_idx = result
.symbols
.iter()
.position(|s| s.name == "Inner")
.unwrap();
let inner_method = result
.symbols
.iter()
.find(|s| s.name == "innerMethod")
.unwrap();
assert_eq!(inner_method.parent_index, Some(inner_idx));
}
#[test]
fn test_import_regular() {
let result = parse("import com.example.Foo;");
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.*;");
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_static() {
let result = parse("import static com.example.Foo.bar;");
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_import_static_wildcard() {
let result = parse("import static com.example.Foo.*;");
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].path, "com/example/Foo::*");
assert!(result.imports[0].imported_symbols.is_empty());
}
#[test]
fn test_import_multiple() {
let result = parse(
r#"import java.util.List;
import java.util.Map;
import java.io.IOException;"#,
);
assert_eq!(result.imports.len(), 3);
let paths: Vec<&str> = result.imports.iter().map(|i| i.path.as_str()).collect();
assert!(paths.contains(&"java/util/List"));
assert!(paths.contains(&"java/util/Map"));
assert!(paths.contains(&"java/io/IOException"));
}
#[test]
fn test_extract_method_call() {
let result = parse(
r#"public class Foo {
public void caller() {
helper();
}
public void helper() {}
}"#,
);
let call = result
.calls
.iter()
.find(|c| c.callee_name == "helper")
.expect("should find call to helper");
assert_eq!(call.caller_name, "caller");
}
#[test]
fn test_extract_qualified_method_call() {
let result = parse(
r#"public class Foo {
public void run() {
bar.process();
}
}"#,
);
let call = result
.calls
.iter()
.find(|c| c.callee_name == "process")
.expect("should find call to process");
assert_eq!(call.caller_name, "run");
}
#[test]
fn test_extract_constructor_call() {
let result = parse(
r#"public class Foo {
public void create() {
Bar b = new Bar();
}
}"#,
);
let call = result
.calls
.iter()
.find(|c| c.callee_name == "Bar")
.expect("should find constructor call to Bar");
assert_eq!(call.caller_name, "create");
}
#[test]
fn test_extract_chained_method_calls() {
let result = parse(
r#"public class Foo {
public void run() {
builder.setName("x").build();
}
}"#,
);
let names: Vec<&str> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(names.contains(&"setName"), "calls: {:?}", names);
assert!(names.contains(&"build"), "calls: {:?}", names);
}
#[test]
fn test_type_ref_parameter() {
let result = parse(
r#"public class Foo {
public void process(Config cfg) {}
}"#,
);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(refs.contains(&"Config"), "type_refs: {:?}", refs);
}
#[test]
fn test_type_ref_return_type() {
let result = parse(
r#"public class Foo {
public Response handle() { return null; }
}"#,
);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(refs.contains(&"Response"), "type_refs: {:?}", refs);
}
#[test]
fn test_type_ref_field_type() {
let result = parse(
r#"public class Foo {
private AppConfig config;
}"#,
);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(refs.contains(&"AppConfig"), "type_refs: {:?}", refs);
}
#[test]
fn test_type_ref_builtin_filtered() {
let result = parse(
r#"public class Foo {
public void process(int x, String s, boolean b) {}
}"#,
);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(!refs.contains(&"String"), "builtins should be filtered");
}
#[test]
fn test_resolve_import_maven_layout() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/java/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("Foo.java"), "public class Foo {}").unwrap();
let resolver = JavaResolver::new(tmp.path());
let resolved = resolver.resolve_import("com/example/Foo", "App.java", tmp.path());
assert!(resolved.is_some(), "should resolve to Foo.java");
assert!(resolved.unwrap().ends_with("Foo.java"));
}
#[test]
fn test_resolve_import_wildcard() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/java/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("Foo.java"), "public class Foo {}").unwrap();
let resolver = JavaResolver::new(tmp.path());
let resolved = resolver.resolve_import("com/example::*", "App.java", tmp.path());
assert!(
resolved.is_none(),
"wildcard import resolves to directory, not a file"
);
}
#[test]
fn test_resolve_import_not_found() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/main/java");
std::fs::create_dir_all(&src_dir).unwrap();
let resolver = JavaResolver::new(tmp.path());
let resolved = resolver.resolve_import("com/example/Missing", "App.java", tmp.path());
assert!(resolved.is_none());
}
#[test]
fn test_resolve_import_flat_src_layout() {
let tmp = tempfile::tempdir().unwrap();
let src_dir = tmp.path().join("src/com/example");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::write(src_dir.join("Bar.java"), "public class Bar {}").unwrap();
let resolver = JavaResolver::new(tmp.path());
let resolved = resolver.resolve_import("com/example/Bar", "App.java", tmp.path());
assert!(resolved.is_some(), "should resolve via src/ fallback");
assert!(resolved.unwrap().ends_with("Bar.java"));
}
#[test]
fn test_resolve_import_no_source_dirs() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("com/example")).unwrap();
std::fs::write(
tmp.path().join("com/example/Baz.java"),
"public class Baz {}",
)
.unwrap();
let resolver = JavaResolver::new(tmp.path());
let resolved = resolver.resolve_import("com/example/Baz", "App.java", tmp.path());
assert!(resolved.is_some(), "should resolve from project root");
assert!(resolved.unwrap().ends_with("Baz.java"));
}
#[test]
fn test_empty_class() {
let result = parse("public class Empty {}");
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Empty");
assert_eq!(result.symbols[0].kind, SymbolKind::Class);
}
#[test]
fn test_interface_with_default_method() {
let result = parse(
r#"public interface Service {
void run();
default void stop() {}
}"#,
);
assert!(!result.has_errors);
let iface = result.symbols.iter().find(|s| s.name == "Service").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_empty_file() {
let result = parse("");
assert!(!result.has_errors);
assert!(result.symbols.is_empty());
}
#[test]
fn test_malformed_file() {
let result = parse("public class {{ invalid }}");
assert!(result.has_errors);
}
#[test]
fn test_class_with_generics() {
let result = parse("public class Box<T> { private T value; }");
assert!(!result.has_errors);
let cls = result.symbols.iter().find(|s| s.name == "Box").unwrap();
assert_eq!(cls.kind, SymbolKind::Class);
}
#[test]
fn test_abstract_class_with_abstract_method() {
let result = parse(
r#"public abstract class Shape {
public abstract double area();
public void describe() {}
}"#,
);
assert!(!result.has_errors);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 2);
assert_eq!(methods[0].name, "area");
assert_eq!(methods[1].name, "describe");
}
#[test]
fn test_enum_with_methods() {
let result = parse(
r#"public enum Planet {
EARTH, MARS;
public double mass() { return 0.0; }
}"#,
);
assert!(!result.has_errors);
let consts: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Const)
.collect();
assert_eq!(consts.len(), 2);
let methods: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Method)
.collect();
assert_eq!(methods.len(), 1);
assert_eq!(methods[0].name, "mass");
}
#[test]
fn test_multiple_classes_in_file() {
let result = parse(
r#"class Foo { void foo() {} }
class Bar { void bar() {} }"#,
);
assert!(!result.has_errors);
let classes: Vec<_> = result
.symbols
.iter()
.filter(|s| s.kind == SymbolKind::Class)
.collect();
assert_eq!(classes.len(), 2);
}
#[test]
fn test_package_declaration_ignored() {
let result = parse(
r#"package com.example;
public class Foo {}"#,
);
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Foo");
}
#[test]
fn test_is_java_builtin_positive() {
assert!(is_java_builtin("int"));
assert!(is_java_builtin("String"));
assert!(is_java_builtin("Object"));
assert!(is_java_builtin("Override"));
assert!(is_java_builtin("Exception"));
assert!(is_java_builtin("Boolean"));
}
#[test]
fn test_is_java_builtin_negative() {
assert!(!is_java_builtin("MyClass"));
assert!(!is_java_builtin("AppConfig"));
assert!(!is_java_builtin(""));
}
#[test]
fn test_language_parser_trait_name() {
let parser = JavaParser;
assert_eq!(parser.language_name(), "java");
}
#[test]
fn test_language_parser_parse_via_trait() {
let parser = JavaParser;
let result = parser.parse("public class Test {}", &PathBuf::from("Test.java"));
assert!(!result.has_errors);
assert_eq!(result.symbols.len(), 1);
assert_eq!(result.symbols[0].name, "Test");
}
#[test]
fn test_detect_jvm_source_roots_maven() {
let tmp = tempfile::tempdir().unwrap();
std::fs::create_dir_all(tmp.path().join("src/main/java")).unwrap();
std::fs::create_dir_all(tmp.path().join("src/test/java")).unwrap();
let roots = detect_jvm_source_roots(tmp.path());
assert!(roots.iter().any(|r| r.ends_with("src/main/java")));
assert!(roots.iter().any(|r| r.ends_with("src/test/java")));
}
#[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_class_with_implements() {
let result = parse(
r#"public class Server implements Runnable {
public void run() {}
}"#,
);
assert!(!result.has_errors);
let cls = result.symbols.iter().find(|s| s.name == "Server").unwrap();
assert_eq!(cls.kind, SymbolKind::Class);
let sig = cls.signature.as_ref().unwrap();
assert!(
sig.contains("Runnable"),
"signature should mention interface: {}",
sig
);
}
#[test]
fn test_interface_extends() {
let result = parse("public interface Worker extends Runnable {}");
assert!(!result.has_errors);
let iface = result.symbols.iter().find(|s| s.name == "Worker").unwrap();
assert_eq!(iface.kind, SymbolKind::Interface);
}
#[test]
fn test_type_ref_generic_parameter() {
let result = parse(
r#"public class Foo {
public void process(List<Config> items) {}
}"#,
);
let refs: Vec<&str> = result
.type_refs
.iter()
.map(|r| r.to_type.as_str())
.collect();
assert!(refs.contains(&"Config"), "type_refs: {:?}", refs);
}
#[test]
fn test_call_inside_constructor() {
let result = parse(
r#"public class Foo {
public Foo() {
init();
}
private void init() {}
}"#,
);
let call = result
.calls
.iter()
.find(|c| c.callee_name == "init")
.expect("should find call to init from constructor");
assert_eq!(call.caller_name, "Foo");
}
#[test]
fn test_static_method() {
let result = parse(
r#"public class Utils {
public static void helper() {}
}"#,
);
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, "helper");
}
#[test]
fn test_split_last_segment() {
assert_eq!(
split_last_segment("com/example/Foo"),
Some(("com/example", "Foo"))
);
assert_eq!(split_last_segment("Foo"), None);
}
#[test]
fn test_resolve_test_source_root() {
let tmp = tempfile::tempdir().unwrap();
let test_dir = tmp.path().join("src/test/java/com/example");
std::fs::create_dir_all(&test_dir).unwrap();
std::fs::write(test_dir.join("FooTest.java"), "public class FooTest {}").unwrap();
let resolver = JavaResolver::new(tmp.path());
let resolved = resolver.resolve_import("com/example/FooTest", "Test.java", tmp.path());
assert!(resolved.is_some(), "should resolve from test source root");
assert!(resolved.unwrap().ends_with("FooTest.java"));
}
}