Skip to main content

astmap_languages/language/
java.rs

1// Java language support: parser + resolver
2
3use std::path::{Path, PathBuf};
4
5use tree_sitter::Language;
6
7use astmap_core::SymbolKind;
8
9use super::{
10    collect_type_refs_by_kind, detect_jvm_source_roots, find_enclosing_symbol, node_text,
11    parse_with_tree_sitter, symbol_from_node, ExtractedCall, ExtractedImport, ExtractedSymbol,
12    LanguageParser, LanguageResolver, LanguageSupport, ParseResult,
13};
14
15pub(super) fn lang() -> LanguageSupport {
16    LanguageSupport {
17        name: "java",
18        extensions: &["java"],
19        parser: &JavaParser,
20        resolver_factory: |root| Box::new(JavaResolver::new(root)),
21        config_files: &["pom.xml", "build.gradle", "build.gradle.kts"],
22        sibling_fn: java_sibling_expansion,
23    }
24}
25
26fn java_sibling_expansion(_rel_path: &str) -> Option<astmap_core::SiblingExpansion> {
27    // Java files within the same package directory are related, but unlike Go
28    // they don't share a flat namespace by default. Return None for now.
29    None
30}
31
32// ── Parser ──
33
34pub(crate) struct JavaParser;
35
36impl LanguageParser for JavaParser {
37    fn parse(&self, source: &str, _file_path: &Path) -> ParseResult {
38        parse_java_file(source)
39    }
40
41    fn language_name(&self) -> &str {
42        "java"
43    }
44}
45
46fn java_language() -> Language {
47    tree_sitter_java::LANGUAGE.into()
48}
49
50fn parse_java_file(source: &str) -> ParseResult {
51    parse_with_tree_sitter(
52        source,
53        &java_language(),
54        |root, src, symbols, imports| {
55            extract_from_node(root, src, symbols, imports, None);
56        },
57        extract_calls,
58        |root, src, symbols| {
59            collect_type_refs_by_kind(root, src, symbols, "type_identifier", &is_java_builtin)
60        },
61    )
62}
63
64// ---------------------------------------------------------------------------
65// Symbol extraction
66// ---------------------------------------------------------------------------
67
68fn extract_from_node(
69    node: tree_sitter::Node,
70    source: &str,
71    symbols: &mut Vec<ExtractedSymbol>,
72    imports: &mut Vec<ExtractedImport>,
73    parent_index: Option<usize>,
74) {
75    match node.kind() {
76        "class_declaration" => {
77            if let Some(sym) = extract_class_like(&node, source, SymbolKind::Class, parent_index) {
78                let idx = symbols.len();
79                symbols.push(sym);
80                recurse_into_body(&node, source, symbols, imports, idx);
81                return;
82            }
83        }
84        "interface_declaration" => {
85            if let Some(sym) =
86                extract_class_like(&node, source, SymbolKind::Interface, parent_index)
87            {
88                let idx = symbols.len();
89                symbols.push(sym);
90                recurse_into_body(&node, source, symbols, imports, idx);
91                return;
92            }
93        }
94        "enum_declaration" => {
95            if let Some(sym) = extract_class_like(&node, source, SymbolKind::Enum, parent_index) {
96                let idx = symbols.len();
97                symbols.push(sym);
98                recurse_into_enum_body(&node, source, symbols, imports, idx);
99                return;
100            }
101        }
102        "record_declaration" => {
103            if let Some(sym) = extract_class_like(&node, source, SymbolKind::Class, parent_index) {
104                let idx = symbols.len();
105                symbols.push(sym);
106                recurse_into_body(&node, source, symbols, imports, idx);
107                return;
108            }
109        }
110        "annotation_type_declaration" => {
111            if let Some(sym) =
112                extract_class_like(&node, source, SymbolKind::Annotation, parent_index)
113            {
114                let idx = symbols.len();
115                symbols.push(sym);
116                recurse_into_body(&node, source, symbols, imports, idx);
117                return;
118            }
119        }
120        "method_declaration" => {
121            if let Some(sym) = extract_method(&node, source, parent_index) {
122                symbols.push(sym);
123                return;
124            }
125        }
126        "constructor_declaration" => {
127            if let Some(sym) = extract_constructor(&node, source, parent_index) {
128                symbols.push(sym);
129                return;
130            }
131        }
132        "field_declaration" => {
133            extract_fields(&node, source, symbols, parent_index);
134            return;
135        }
136        "import_declaration" => {
137            if let Some(imp) = extract_import(&node, source) {
138                imports.push(imp);
139            }
140            return;
141        }
142        _ => {}
143    }
144
145    // Default: recurse into children
146    for i in 0..node.child_count() {
147        if let Some(child) = node.child(i as u32) {
148            extract_from_node(child, source, symbols, imports, parent_index);
149        }
150    }
151}
152
153fn extract_class_like(
154    node: &tree_sitter::Node,
155    source: &str,
156    kind: SymbolKind,
157    parent_index: Option<usize>,
158) -> Option<ExtractedSymbol> {
159    let name_node = node.child_by_field_name("name")?;
160    let name = node_text(&name_node, source);
161    Some(symbol_from_node(name, kind, node, source, parent_index))
162}
163
164/// Recurse into the body of a class-like declaration to extract members.
165fn recurse_into_body(
166    node: &tree_sitter::Node,
167    source: &str,
168    symbols: &mut Vec<ExtractedSymbol>,
169    imports: &mut Vec<ExtractedImport>,
170    parent_idx: usize,
171) {
172    if let Some(body) = node.child_by_field_name("body") {
173        for i in 0..body.child_count() {
174            if let Some(child) = body.child(i as u32) {
175                extract_from_node(child, source, symbols, imports, Some(parent_idx));
176            }
177        }
178    }
179}
180
181/// Recurse into the body of an enum declaration, also extracting enum_constant nodes.
182fn recurse_into_enum_body(
183    node: &tree_sitter::Node,
184    source: &str,
185    symbols: &mut Vec<ExtractedSymbol>,
186    imports: &mut Vec<ExtractedImport>,
187    parent_idx: usize,
188) {
189    if let Some(body) = node.child_by_field_name("body") {
190        for i in 0..body.child_count() {
191            if let Some(child) = body.child(i as u32) {
192                if child.kind() == "enum_constant" {
193                    if let Some(sym) = extract_enum_constant(&child, source, parent_idx) {
194                        symbols.push(sym);
195                    }
196                } else {
197                    extract_from_node(child, source, symbols, imports, Some(parent_idx));
198                }
199            }
200        }
201    }
202}
203
204fn extract_enum_constant(
205    node: &tree_sitter::Node,
206    source: &str,
207    parent_idx: usize,
208) -> Option<ExtractedSymbol> {
209    // enum_constant has a child identifier for the name
210    let name_node = node.child_by_field_name("name").or_else(|| {
211        (0..node.child_count())
212            .filter_map(|i| node.child(i as u32))
213            .find(|c| c.kind() == "identifier")
214    })?;
215    let name = node_text(&name_node, source);
216    Some(symbol_from_node(
217        name,
218        SymbolKind::Const,
219        node,
220        source,
221        Some(parent_idx),
222    ))
223}
224
225fn extract_method(
226    node: &tree_sitter::Node,
227    source: &str,
228    parent_index: Option<usize>,
229) -> Option<ExtractedSymbol> {
230    let name_node = node.child_by_field_name("name")?;
231    let name = node_text(&name_node, source);
232    Some(symbol_from_node(
233        name,
234        SymbolKind::Method,
235        node,
236        source,
237        parent_index,
238    ))
239}
240
241fn extract_constructor(
242    node: &tree_sitter::Node,
243    source: &str,
244    parent_index: Option<usize>,
245) -> Option<ExtractedSymbol> {
246    let name_node = node.child_by_field_name("name")?;
247    let name = node_text(&name_node, source);
248    Some(symbol_from_node(
249        name,
250        SymbolKind::Method,
251        node,
252        source,
253        parent_index,
254    ))
255}
256
257/// Extract field declarations.
258///
259/// A `field_declaration` may have modifiers. If modifiers contain both "static" and
260/// "final", it's a constant (Const), otherwise a variable (Variable).
261///
262/// The actual variable names are inside `variable_declarator` children of the
263/// `declarator` field.
264fn extract_fields(
265    node: &tree_sitter::Node,
266    source: &str,
267    symbols: &mut Vec<ExtractedSymbol>,
268    parent_index: Option<usize>,
269) {
270    let kind = if has_static_final_modifiers(node, source) {
271        SymbolKind::Const
272    } else {
273        SymbolKind::Variable
274    };
275
276    // Collect variable names from variable_declarator children
277    for i in 0..node.child_count() {
278        if let Some(child) = node.child(i as u32) {
279            if child.kind() == "variable_declarator" {
280                if let Some(name_node) = child.child_by_field_name("name") {
281                    let name = node_text(&name_node, source);
282                    symbols.push(symbol_from_node(name, kind, node, source, parent_index));
283                }
284            }
285        }
286    }
287}
288
289/// Check if a node has `modifiers` child containing both "static" and "final".
290fn has_static_final_modifiers(node: &tree_sitter::Node, source: &str) -> bool {
291    for i in 0..node.child_count() {
292        if let Some(child) = node.child(i as u32) {
293            if child.kind() == "modifiers" {
294                let mut has_static = false;
295                let mut has_final = false;
296                for j in 0..child.child_count() {
297                    if let Some(mod_child) = child.child(j as u32) {
298                        let text = node_text(&mod_child, source);
299                        if text == "static" {
300                            has_static = true;
301                        }
302                        if text == "final" {
303                            has_final = true;
304                        }
305                    }
306                }
307                return has_static && has_final;
308            }
309        }
310    }
311    false
312}
313
314// ---------------------------------------------------------------------------
315// Import extraction
316// ---------------------------------------------------------------------------
317
318/// Extract a single `import_declaration` node into an `ExtractedImport`.
319///
320/// Examples:
321/// - `import com.example.Foo;` → path="com/example/Foo", imported_symbols=["Foo"]
322/// - `import com.example.*;` → path="com/example::*", imported_symbols=[]
323/// - `import static com.example.Foo.bar;` → path="com/example/Foo", imported_symbols=["bar"]
324/// - `import static com.example.Foo.*;` → path="com/example/Foo::*", imported_symbols=[]
325fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
326    // Detect static import
327    let is_static = (0..node.child_count())
328        .filter_map(|i| node.child(i as u32))
329        .any(|c| node_text(&c, source) == "static");
330
331    // Detect wildcard import (asterisk child)
332    let has_asterisk = (0..node.child_count())
333        .filter_map(|i| node.child(i as u32))
334        .any(|c| c.kind() == "asterisk");
335
336    // Get the full import path from scoped_identifier or identifier child
337    let path_text = (0..node.child_count())
338        .filter_map(|i| node.child(i as u32))
339        .find(|c| c.kind() == "scoped_identifier" || c.kind() == "identifier")
340        .map(|c| node_text(&c, source))?;
341
342    // Convert dots to slashes
343    let slash_path = path_text.replace('.', "/");
344
345    if is_static {
346        if has_asterisk {
347            // import static com.example.Foo.* → path="com/example/Foo::*"
348            Some(ExtractedImport {
349                path: format!("{}::*", slash_path),
350                imported_symbols: vec![],
351            })
352        } else {
353            // import static com.example.Foo.bar → path="com/example/Foo", symbols=["bar"]
354            let (parent, member) = split_last_segment(&slash_path)?;
355            Some(ExtractedImport {
356                path: parent.to_string(),
357                imported_symbols: vec![member.to_string()],
358            })
359        }
360    } else if has_asterisk {
361        // import com.example.* → path="com/example::*"
362        Some(ExtractedImport {
363            path: format!("{}::*", slash_path),
364            imported_symbols: vec![],
365        })
366    } else {
367        // import com.example.Foo → path="com/example/Foo", symbols=["Foo"]
368        let last = slash_path.rsplit('/').next().unwrap_or(&slash_path);
369        Some(ExtractedImport {
370            path: slash_path.clone(),
371            imported_symbols: vec![last.to_string()],
372        })
373    }
374}
375
376/// Split "com/example/Foo" into ("com/example", "Foo").
377fn split_last_segment(path: &str) -> Option<(&str, &str)> {
378    let idx = path.rfind('/')?;
379    Some((&path[..idx], &path[idx + 1..]))
380}
381
382// ---------------------------------------------------------------------------
383// Call extraction
384// ---------------------------------------------------------------------------
385
386fn extract_calls(
387    root: tree_sitter::Node,
388    source: &str,
389    symbols: &[ExtractedSymbol],
390) -> Vec<ExtractedCall> {
391    let mut calls = Vec::new();
392    collect_calls(root, source, symbols, &mut calls);
393    calls
394}
395
396fn collect_calls(
397    node: tree_sitter::Node,
398    source: &str,
399    symbols: &[ExtractedSymbol],
400    calls: &mut Vec<ExtractedCall>,
401) {
402    match node.kind() {
403        "method_invocation" => {
404            if let Some(name_node) = node.child_by_field_name("name") {
405                let callee_name = node_text(&name_node, source);
406                let call_line = node.start_position().row + 1;
407                if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
408                    calls.push(ExtractedCall {
409                        caller_name: caller.to_string(),
410                        callee_name,
411                        line: call_line,
412                    });
413                }
414            }
415        }
416        "object_creation_expression" => {
417            // new Foo(...) — extract type name as constructor call
418            if let Some(type_node) = node.child_by_field_name("type") {
419                let callee_name = node_text(&type_node, source);
420                let call_line = node.start_position().row + 1;
421                if let Some(caller) = find_enclosing_symbol(symbols, call_line) {
422                    calls.push(ExtractedCall {
423                        caller_name: caller.to_string(),
424                        callee_name,
425                        line: call_line,
426                    });
427                }
428            }
429        }
430        _ => {}
431    }
432
433    for i in 0..node.child_count() {
434        if let Some(child) = node.child(i as u32) {
435            collect_calls(child, source, symbols, calls);
436        }
437    }
438}
439
440// ---------------------------------------------------------------------------
441// Type reference extraction
442// ---------------------------------------------------------------------------
443
444fn is_java_builtin(name: &str) -> bool {
445    matches!(
446        name,
447        "int"
448            | "long"
449            | "float"
450            | "double"
451            | "boolean"
452            | "char"
453            | "byte"
454            | "short"
455            | "void"
456            | "String"
457            | "Object"
458            | "Integer"
459            | "Long"
460            | "Float"
461            | "Double"
462            | "Boolean"
463            | "Character"
464            | "Byte"
465            | "Short"
466            | "Void"
467            | "Number"
468            | "Class"
469            | "Enum"
470            | "Throwable"
471            | "Exception"
472            | "RuntimeException"
473            | "Error"
474            | "Override"
475            | "Deprecated"
476            | "SuppressWarnings"
477            | "FunctionalInterface"
478            | "SafeVarargs"
479    )
480}
481
482// ── Resolver ──
483
484pub(crate) struct JavaResolver {
485    source_roots: Vec<PathBuf>,
486}
487
488impl JavaResolver {
489    fn new(root: &Path) -> Self {
490        let source_roots = detect_jvm_source_roots(root);
491        Self { source_roots }
492    }
493}
494
495impl LanguageResolver for JavaResolver {
496    fn resolve_import(
497        &self,
498        import_path: &str,
499        _source_file: &str,
500        _project_root: &Path,
501    ) -> Option<PathBuf> {
502        // Strip ::* suffix for wildcard imports (resolve to directory-level)
503        let effective_path = import_path.strip_suffix("::*").unwrap_or(import_path);
504
505        // Convert slashes to platform path separators
506        let rel_path = PathBuf::from(effective_path.replace('/', std::path::MAIN_SEPARATOR_STR));
507
508        for root in &self.source_roots {
509            let candidate = root.join(&rel_path).with_extension("java");
510            if candidate.exists() {
511                return Some(candidate);
512            }
513        }
514
515        // External dependency or JDK class — not resolvable locally
516        None
517    }
518}
519
520// ---------------------------------------------------------------------------
521// Tests
522// ---------------------------------------------------------------------------
523
524#[cfg(test)]
525mod tests {
526    use super::*;
527
528    fn parse(source: &str) -> ParseResult {
529        JavaParser.parse(source, &PathBuf::from("Test.java"))
530    }
531
532    // --- Symbol extraction: class-like declarations ---
533
534    #[test]
535    fn test_parse_class() {
536        let result = parse("public class Foo {}");
537        assert!(!result.has_errors);
538        assert_eq!(result.symbols.len(), 1);
539        assert_eq!(result.symbols[0].name, "Foo");
540        assert_eq!(result.symbols[0].kind, SymbolKind::Class);
541        assert!(result.symbols[0].parent_index.is_none());
542    }
543
544    #[test]
545    fn test_parse_interface() {
546        let result = parse("public interface Service { void run(); }");
547        assert!(!result.has_errors);
548        let ifaces: Vec<_> = result
549            .symbols
550            .iter()
551            .filter(|s| s.kind == SymbolKind::Interface)
552            .collect();
553        assert_eq!(ifaces.len(), 1);
554        assert_eq!(ifaces[0].name, "Service");
555    }
556
557    #[test]
558    fn test_parse_enum() {
559        let result = parse("public enum Color { RED, GREEN, BLUE }");
560        assert!(!result.has_errors);
561        let enums: Vec<_> = result
562            .symbols
563            .iter()
564            .filter(|s| s.kind == SymbolKind::Enum)
565            .collect();
566        assert_eq!(enums.len(), 1);
567        assert_eq!(enums[0].name, "Color");
568    }
569
570    #[test]
571    fn test_parse_record() {
572        let result = parse("public record Point(int x, int y) {}");
573        assert!(!result.has_errors);
574        let records: Vec<_> = result
575            .symbols
576            .iter()
577            .filter(|s| s.name == "Point")
578            .collect();
579        assert_eq!(records.len(), 1);
580        assert_eq!(records[0].kind, SymbolKind::Class);
581    }
582
583    #[test]
584    fn test_parse_annotation_type() {
585        let result = parse("public @interface MyAnnotation {}");
586        assert!(!result.has_errors);
587        let annots: Vec<_> = result
588            .symbols
589            .iter()
590            .filter(|s| s.kind == SymbolKind::Annotation)
591            .collect();
592        assert_eq!(annots.len(), 1);
593        assert_eq!(annots[0].name, "MyAnnotation");
594    }
595
596    // --- Methods and constructors ---
597
598    #[test]
599    fn test_parse_method() {
600        let result = parse(
601            r#"public class Foo {
602    public void doStuff() {}
603    private int compute(int x) { return x * 2; }
604}"#,
605        );
606        assert!(!result.has_errors);
607        let methods: Vec<_> = result
608            .symbols
609            .iter()
610            .filter(|s| s.kind == SymbolKind::Method)
611            .collect();
612        assert_eq!(methods.len(), 2);
613        assert_eq!(methods[0].name, "doStuff");
614        assert_eq!(methods[1].name, "compute");
615        // Methods should have Foo as parent
616        let foo_idx = result.symbols.iter().position(|s| s.name == "Foo").unwrap();
617        assert_eq!(methods[0].parent_index, Some(foo_idx));
618        assert_eq!(methods[1].parent_index, Some(foo_idx));
619    }
620
621    #[test]
622    fn test_parse_constructor() {
623        let result = parse(
624            r#"public class Foo {
625    public Foo(int x) {}
626}"#,
627        );
628        assert!(!result.has_errors);
629        let constructors: Vec<_> = result
630            .symbols
631            .iter()
632            .filter(|s| s.kind == SymbolKind::Method && s.name == "Foo")
633            .collect();
634        // class Foo + constructor Foo
635        assert_eq!(constructors.len(), 1);
636        let foo_idx = result
637            .symbols
638            .iter()
639            .position(|s| s.kind == SymbolKind::Class && s.name == "Foo")
640            .unwrap();
641        assert_eq!(constructors[0].parent_index, Some(foo_idx));
642    }
643
644    // --- Fields ---
645
646    #[test]
647    fn test_parse_field_variable() {
648        let result = parse(
649            r#"public class Foo {
650    private int count;
651    protected String name;
652}"#,
653        );
654        assert!(!result.has_errors);
655        let vars: Vec<_> = result
656            .symbols
657            .iter()
658            .filter(|s| s.kind == SymbolKind::Variable)
659            .collect();
660        assert_eq!(vars.len(), 2);
661        assert_eq!(vars[0].name, "count");
662        assert_eq!(vars[1].name, "name");
663    }
664
665    #[test]
666    fn test_parse_field_static_final_is_const() {
667        let result = parse(
668            r#"public class Config {
669    public static final int MAX_SIZE = 100;
670    public static final String DEFAULT_NAME = "foo";
671}"#,
672        );
673        assert!(!result.has_errors);
674        let consts: Vec<_> = result
675            .symbols
676            .iter()
677            .filter(|s| s.kind == SymbolKind::Const)
678            .collect();
679        assert_eq!(consts.len(), 2);
680        assert_eq!(consts[0].name, "MAX_SIZE");
681        assert_eq!(consts[1].name, "DEFAULT_NAME");
682    }
683
684    #[test]
685    fn test_parse_field_static_only_is_variable() {
686        let result = parse(
687            r#"public class Foo {
688    private static int instanceCount;
689}"#,
690        );
691        assert!(!result.has_errors);
692        let vars: Vec<_> = result
693            .symbols
694            .iter()
695            .filter(|s| s.kind == SymbolKind::Variable)
696            .collect();
697        assert_eq!(vars.len(), 1);
698        assert_eq!(vars[0].name, "instanceCount");
699    }
700
701    #[test]
702    fn test_parse_field_final_only_is_variable() {
703        let result = parse(
704            r#"public class Foo {
705    private final int id;
706}"#,
707        );
708        assert!(!result.has_errors);
709        let vars: Vec<_> = result
710            .symbols
711            .iter()
712            .filter(|s| s.kind == SymbolKind::Variable)
713            .collect();
714        assert_eq!(vars.len(), 1);
715        assert_eq!(vars[0].name, "id");
716    }
717
718    // --- Enum constants ---
719
720    #[test]
721    fn test_parse_enum_constants() {
722        let result = parse("public enum Status { ACTIVE, INACTIVE, PENDING }");
723        assert!(!result.has_errors);
724        let consts: Vec<_> = result
725            .symbols
726            .iter()
727            .filter(|s| s.kind == SymbolKind::Const)
728            .collect();
729        assert_eq!(consts.len(), 3);
730        assert_eq!(consts[0].name, "ACTIVE");
731        assert_eq!(consts[1].name, "INACTIVE");
732        assert_eq!(consts[2].name, "PENDING");
733        // All should have the enum as parent
734        let enum_idx = result
735            .symbols
736            .iter()
737            .position(|s| s.kind == SymbolKind::Enum)
738            .unwrap();
739        for c in &consts {
740            assert_eq!(c.parent_index, Some(enum_idx));
741        }
742    }
743
744    // --- Nested class ---
745
746    #[test]
747    fn test_parse_nested_class() {
748        let result = parse(
749            r#"public class Outer {
750    public class Inner {
751        public void innerMethod() {}
752    }
753}"#,
754        );
755        assert!(!result.has_errors);
756        let outer_idx = result
757            .symbols
758            .iter()
759            .position(|s| s.name == "Outer")
760            .unwrap();
761        let inner = result.symbols.iter().find(|s| s.name == "Inner").unwrap();
762        assert_eq!(inner.kind, SymbolKind::Class);
763        assert_eq!(inner.parent_index, Some(outer_idx));
764        let inner_idx = result
765            .symbols
766            .iter()
767            .position(|s| s.name == "Inner")
768            .unwrap();
769        let inner_method = result
770            .symbols
771            .iter()
772            .find(|s| s.name == "innerMethod")
773            .unwrap();
774        assert_eq!(inner_method.parent_index, Some(inner_idx));
775    }
776
777    // --- Import extraction ---
778
779    #[test]
780    fn test_import_regular() {
781        let result = parse("import com.example.Foo;");
782        assert_eq!(result.imports.len(), 1);
783        assert_eq!(result.imports[0].path, "com/example/Foo");
784        assert_eq!(result.imports[0].imported_symbols, vec!["Foo"]);
785    }
786
787    #[test]
788    fn test_import_wildcard() {
789        let result = parse("import com.example.*;");
790        assert_eq!(result.imports.len(), 1);
791        assert_eq!(result.imports[0].path, "com/example::*");
792        assert!(result.imports[0].imported_symbols.is_empty());
793    }
794
795    #[test]
796    fn test_import_static() {
797        let result = parse("import static com.example.Foo.bar;");
798        assert_eq!(result.imports.len(), 1);
799        assert_eq!(result.imports[0].path, "com/example/Foo");
800        assert_eq!(result.imports[0].imported_symbols, vec!["bar"]);
801    }
802
803    #[test]
804    fn test_import_static_wildcard() {
805        let result = parse("import static com.example.Foo.*;");
806        assert_eq!(result.imports.len(), 1);
807        assert_eq!(result.imports[0].path, "com/example/Foo::*");
808        assert!(result.imports[0].imported_symbols.is_empty());
809    }
810
811    #[test]
812    fn test_import_multiple() {
813        let result = parse(
814            r#"import java.util.List;
815import java.util.Map;
816import java.io.IOException;"#,
817        );
818        assert_eq!(result.imports.len(), 3);
819        let paths: Vec<&str> = result.imports.iter().map(|i| i.path.as_str()).collect();
820        assert!(paths.contains(&"java/util/List"));
821        assert!(paths.contains(&"java/util/Map"));
822        assert!(paths.contains(&"java/io/IOException"));
823    }
824
825    // --- Call extraction ---
826
827    #[test]
828    fn test_extract_method_call() {
829        let result = parse(
830            r#"public class Foo {
831    public void caller() {
832        helper();
833    }
834    public void helper() {}
835}"#,
836        );
837        let call = result
838            .calls
839            .iter()
840            .find(|c| c.callee_name == "helper")
841            .expect("should find call to helper");
842        assert_eq!(call.caller_name, "caller");
843    }
844
845    #[test]
846    fn test_extract_qualified_method_call() {
847        let result = parse(
848            r#"public class Foo {
849    public void run() {
850        bar.process();
851    }
852}"#,
853        );
854        let call = result
855            .calls
856            .iter()
857            .find(|c| c.callee_name == "process")
858            .expect("should find call to process");
859        assert_eq!(call.caller_name, "run");
860    }
861
862    #[test]
863    fn test_extract_constructor_call() {
864        let result = parse(
865            r#"public class Foo {
866    public void create() {
867        Bar b = new Bar();
868    }
869}"#,
870        );
871        let call = result
872            .calls
873            .iter()
874            .find(|c| c.callee_name == "Bar")
875            .expect("should find constructor call to Bar");
876        assert_eq!(call.caller_name, "create");
877    }
878
879    #[test]
880    fn test_extract_chained_method_calls() {
881        let result = parse(
882            r#"public class Foo {
883    public void run() {
884        builder.setName("x").build();
885    }
886}"#,
887        );
888        let names: Vec<&str> = result
889            .calls
890            .iter()
891            .map(|c| c.callee_name.as_str())
892            .collect();
893        assert!(names.contains(&"setName"), "calls: {:?}", names);
894        assert!(names.contains(&"build"), "calls: {:?}", names);
895    }
896
897    // --- Type reference extraction ---
898
899    #[test]
900    fn test_type_ref_parameter() {
901        let result = parse(
902            r#"public class Foo {
903    public void process(Config cfg) {}
904}"#,
905        );
906        let refs: Vec<&str> = result
907            .type_refs
908            .iter()
909            .map(|r| r.to_type.as_str())
910            .collect();
911        assert!(refs.contains(&"Config"), "type_refs: {:?}", refs);
912    }
913
914    #[test]
915    fn test_type_ref_return_type() {
916        let result = parse(
917            r#"public class Foo {
918    public Response handle() { return null; }
919}"#,
920        );
921        let refs: Vec<&str> = result
922            .type_refs
923            .iter()
924            .map(|r| r.to_type.as_str())
925            .collect();
926        assert!(refs.contains(&"Response"), "type_refs: {:?}", refs);
927    }
928
929    #[test]
930    fn test_type_ref_field_type() {
931        let result = parse(
932            r#"public class Foo {
933    private AppConfig config;
934}"#,
935        );
936        let refs: Vec<&str> = result
937            .type_refs
938            .iter()
939            .map(|r| r.to_type.as_str())
940            .collect();
941        assert!(refs.contains(&"AppConfig"), "type_refs: {:?}", refs);
942    }
943
944    #[test]
945    fn test_type_ref_builtin_filtered() {
946        let result = parse(
947            r#"public class Foo {
948    public void process(int x, String s, boolean b) {}
949}"#,
950        );
951        let refs: Vec<&str> = result
952            .type_refs
953            .iter()
954            .map(|r| r.to_type.as_str())
955            .collect();
956        assert!(!refs.contains(&"String"), "builtins should be filtered");
957    }
958
959    // --- Resolver ---
960
961    #[test]
962    fn test_resolve_import_maven_layout() {
963        let tmp = tempfile::tempdir().unwrap();
964        let src_dir = tmp.path().join("src/main/java/com/example");
965        std::fs::create_dir_all(&src_dir).unwrap();
966        std::fs::write(src_dir.join("Foo.java"), "public class Foo {}").unwrap();
967
968        let resolver = JavaResolver::new(tmp.path());
969        let resolved = resolver.resolve_import("com/example/Foo", "App.java", tmp.path());
970        assert!(resolved.is_some(), "should resolve to Foo.java");
971        assert!(resolved.unwrap().ends_with("Foo.java"));
972    }
973
974    #[test]
975    fn test_resolve_import_wildcard() {
976        let tmp = tempfile::tempdir().unwrap();
977        let src_dir = tmp.path().join("src/main/java/com/example");
978        std::fs::create_dir_all(&src_dir).unwrap();
979        std::fs::write(src_dir.join("Foo.java"), "public class Foo {}").unwrap();
980
981        let resolver = JavaResolver::new(tmp.path());
982        // Wildcard import: com/example::* — should strip ::* and try com/example.java (won't exist)
983        let resolved = resolver.resolve_import("com/example::*", "App.java", tmp.path());
984        // Wildcard resolves to com/example.java which doesn't exist
985        assert!(
986            resolved.is_none(),
987            "wildcard import resolves to directory, not a file"
988        );
989    }
990
991    #[test]
992    fn test_resolve_import_not_found() {
993        let tmp = tempfile::tempdir().unwrap();
994        let src_dir = tmp.path().join("src/main/java");
995        std::fs::create_dir_all(&src_dir).unwrap();
996
997        let resolver = JavaResolver::new(tmp.path());
998        let resolved = resolver.resolve_import("com/example/Missing", "App.java", tmp.path());
999        assert!(resolved.is_none());
1000    }
1001
1002    #[test]
1003    fn test_resolve_import_flat_src_layout() {
1004        let tmp = tempfile::tempdir().unwrap();
1005        let src_dir = tmp.path().join("src/com/example");
1006        std::fs::create_dir_all(&src_dir).unwrap();
1007        std::fs::write(src_dir.join("Bar.java"), "public class Bar {}").unwrap();
1008
1009        let resolver = JavaResolver::new(tmp.path());
1010        let resolved = resolver.resolve_import("com/example/Bar", "App.java", tmp.path());
1011        assert!(resolved.is_some(), "should resolve via src/ fallback");
1012        assert!(resolved.unwrap().ends_with("Bar.java"));
1013    }
1014
1015    #[test]
1016    fn test_resolve_import_no_source_dirs() {
1017        let tmp = tempfile::tempdir().unwrap();
1018        // No src/ or src/main/java — falls back to project root
1019        std::fs::create_dir_all(tmp.path().join("com/example")).unwrap();
1020        std::fs::write(
1021            tmp.path().join("com/example/Baz.java"),
1022            "public class Baz {}",
1023        )
1024        .unwrap();
1025
1026        let resolver = JavaResolver::new(tmp.path());
1027        let resolved = resolver.resolve_import("com/example/Baz", "App.java", tmp.path());
1028        assert!(resolved.is_some(), "should resolve from project root");
1029        assert!(resolved.unwrap().ends_with("Baz.java"));
1030    }
1031
1032    // --- Edge cases ---
1033
1034    #[test]
1035    fn test_empty_class() {
1036        let result = parse("public class Empty {}");
1037        assert!(!result.has_errors);
1038        assert_eq!(result.symbols.len(), 1);
1039        assert_eq!(result.symbols[0].name, "Empty");
1040        assert_eq!(result.symbols[0].kind, SymbolKind::Class);
1041    }
1042
1043    #[test]
1044    fn test_interface_with_default_method() {
1045        let result = parse(
1046            r#"public interface Service {
1047    void run();
1048    default void stop() {}
1049}"#,
1050        );
1051        assert!(!result.has_errors);
1052        let iface = result.symbols.iter().find(|s| s.name == "Service").unwrap();
1053        assert_eq!(iface.kind, SymbolKind::Interface);
1054        let methods: Vec<_> = result
1055            .symbols
1056            .iter()
1057            .filter(|s| s.kind == SymbolKind::Method)
1058            .collect();
1059        // run() and stop() are both method_declarations
1060        assert_eq!(methods.len(), 2);
1061    }
1062
1063    #[test]
1064    fn test_empty_file() {
1065        let result = parse("");
1066        assert!(!result.has_errors);
1067        assert!(result.symbols.is_empty());
1068    }
1069
1070    #[test]
1071    fn test_malformed_file() {
1072        let result = parse("public class {{ invalid }}");
1073        assert!(result.has_errors);
1074    }
1075
1076    #[test]
1077    fn test_class_with_generics() {
1078        let result = parse("public class Box<T> { private T value; }");
1079        assert!(!result.has_errors);
1080        let cls = result.symbols.iter().find(|s| s.name == "Box").unwrap();
1081        assert_eq!(cls.kind, SymbolKind::Class);
1082    }
1083
1084    #[test]
1085    fn test_abstract_class_with_abstract_method() {
1086        let result = parse(
1087            r#"public abstract class Shape {
1088    public abstract double area();
1089    public void describe() {}
1090}"#,
1091        );
1092        assert!(!result.has_errors);
1093        let methods: Vec<_> = result
1094            .symbols
1095            .iter()
1096            .filter(|s| s.kind == SymbolKind::Method)
1097            .collect();
1098        assert_eq!(methods.len(), 2);
1099        assert_eq!(methods[0].name, "area");
1100        assert_eq!(methods[1].name, "describe");
1101    }
1102
1103    #[test]
1104    fn test_enum_with_methods() {
1105        let result = parse(
1106            r#"public enum Planet {
1107    EARTH, MARS;
1108    public double mass() { return 0.0; }
1109}"#,
1110        );
1111        assert!(!result.has_errors);
1112        let consts: Vec<_> = result
1113            .symbols
1114            .iter()
1115            .filter(|s| s.kind == SymbolKind::Const)
1116            .collect();
1117        assert_eq!(consts.len(), 2);
1118        let methods: Vec<_> = result
1119            .symbols
1120            .iter()
1121            .filter(|s| s.kind == SymbolKind::Method)
1122            .collect();
1123        assert_eq!(methods.len(), 1);
1124        assert_eq!(methods[0].name, "mass");
1125    }
1126
1127    #[test]
1128    fn test_multiple_classes_in_file() {
1129        let result = parse(
1130            r#"class Foo { void foo() {} }
1131class Bar { void bar() {} }"#,
1132        );
1133        assert!(!result.has_errors);
1134        let classes: Vec<_> = result
1135            .symbols
1136            .iter()
1137            .filter(|s| s.kind == SymbolKind::Class)
1138            .collect();
1139        assert_eq!(classes.len(), 2);
1140    }
1141
1142    #[test]
1143    fn test_package_declaration_ignored() {
1144        let result = parse(
1145            r#"package com.example;
1146public class Foo {}"#,
1147        );
1148        assert!(!result.has_errors);
1149        // package_declaration should not produce symbols
1150        assert_eq!(result.symbols.len(), 1);
1151        assert_eq!(result.symbols[0].name, "Foo");
1152    }
1153
1154    #[test]
1155    fn test_is_java_builtin_positive() {
1156        assert!(is_java_builtin("int"));
1157        assert!(is_java_builtin("String"));
1158        assert!(is_java_builtin("Object"));
1159        assert!(is_java_builtin("Override"));
1160        assert!(is_java_builtin("Exception"));
1161        assert!(is_java_builtin("Boolean"));
1162    }
1163
1164    #[test]
1165    fn test_is_java_builtin_negative() {
1166        assert!(!is_java_builtin("MyClass"));
1167        assert!(!is_java_builtin("AppConfig"));
1168        assert!(!is_java_builtin(""));
1169    }
1170
1171    #[test]
1172    fn test_language_parser_trait_name() {
1173        let parser = JavaParser;
1174        assert_eq!(parser.language_name(), "java");
1175    }
1176
1177    #[test]
1178    fn test_language_parser_parse_via_trait() {
1179        let parser = JavaParser;
1180        let result = parser.parse("public class Test {}", &PathBuf::from("Test.java"));
1181        assert!(!result.has_errors);
1182        assert_eq!(result.symbols.len(), 1);
1183        assert_eq!(result.symbols[0].name, "Test");
1184    }
1185
1186    #[test]
1187    fn test_detect_jvm_source_roots_maven() {
1188        let tmp = tempfile::tempdir().unwrap();
1189        std::fs::create_dir_all(tmp.path().join("src/main/java")).unwrap();
1190        std::fs::create_dir_all(tmp.path().join("src/test/java")).unwrap();
1191        let roots = detect_jvm_source_roots(tmp.path());
1192        assert!(roots.iter().any(|r| r.ends_with("src/main/java")));
1193        assert!(roots.iter().any(|r| r.ends_with("src/test/java")));
1194    }
1195
1196    #[test]
1197    fn test_detect_jvm_source_roots_fallback() {
1198        let tmp = tempfile::tempdir().unwrap();
1199        // No src/ directories at all — should fall back to project root
1200        let roots = detect_jvm_source_roots(tmp.path());
1201        assert_eq!(roots.len(), 1);
1202        assert_eq!(roots[0], tmp.path().to_path_buf());
1203    }
1204
1205    #[test]
1206    fn test_class_with_implements() {
1207        let result = parse(
1208            r#"public class Server implements Runnable {
1209    public void run() {}
1210}"#,
1211        );
1212        assert!(!result.has_errors);
1213        let cls = result.symbols.iter().find(|s| s.name == "Server").unwrap();
1214        assert_eq!(cls.kind, SymbolKind::Class);
1215        let sig = cls.signature.as_ref().unwrap();
1216        assert!(
1217            sig.contains("Runnable"),
1218            "signature should mention interface: {}",
1219            sig
1220        );
1221    }
1222
1223    #[test]
1224    fn test_interface_extends() {
1225        let result = parse("public interface Worker extends Runnable {}");
1226        assert!(!result.has_errors);
1227        let iface = result.symbols.iter().find(|s| s.name == "Worker").unwrap();
1228        assert_eq!(iface.kind, SymbolKind::Interface);
1229    }
1230
1231    #[test]
1232    fn test_type_ref_generic_parameter() {
1233        let result = parse(
1234            r#"public class Foo {
1235    public void process(List<Config> items) {}
1236}"#,
1237        );
1238        let refs: Vec<&str> = result
1239            .type_refs
1240            .iter()
1241            .map(|r| r.to_type.as_str())
1242            .collect();
1243        assert!(refs.contains(&"Config"), "type_refs: {:?}", refs);
1244    }
1245
1246    #[test]
1247    fn test_call_inside_constructor() {
1248        let result = parse(
1249            r#"public class Foo {
1250    public Foo() {
1251        init();
1252    }
1253    private void init() {}
1254}"#,
1255        );
1256        let call = result
1257            .calls
1258            .iter()
1259            .find(|c| c.callee_name == "init")
1260            .expect("should find call to init from constructor");
1261        assert_eq!(call.caller_name, "Foo");
1262    }
1263
1264    #[test]
1265    fn test_static_method() {
1266        let result = parse(
1267            r#"public class Utils {
1268    public static void helper() {}
1269}"#,
1270        );
1271        assert!(!result.has_errors);
1272        let methods: Vec<_> = result
1273            .symbols
1274            .iter()
1275            .filter(|s| s.kind == SymbolKind::Method)
1276            .collect();
1277        assert_eq!(methods.len(), 1);
1278        assert_eq!(methods[0].name, "helper");
1279    }
1280
1281    #[test]
1282    fn test_split_last_segment() {
1283        assert_eq!(
1284            split_last_segment("com/example/Foo"),
1285            Some(("com/example", "Foo"))
1286        );
1287        assert_eq!(split_last_segment("Foo"), None);
1288    }
1289
1290    #[test]
1291    fn test_resolve_test_source_root() {
1292        let tmp = tempfile::tempdir().unwrap();
1293        let test_dir = tmp.path().join("src/test/java/com/example");
1294        std::fs::create_dir_all(&test_dir).unwrap();
1295        std::fs::write(test_dir.join("FooTest.java"), "public class FooTest {}").unwrap();
1296
1297        let resolver = JavaResolver::new(tmp.path());
1298        let resolved = resolver.resolve_import("com/example/FooTest", "Test.java", tmp.path());
1299        assert!(resolved.is_some(), "should resolve from test source root");
1300        assert!(resolved.unwrap().ends_with("FooTest.java"));
1301    }
1302}