1use 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 None
30}
31
32pub(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
64fn 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 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
164fn 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
181fn 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 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
257fn 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 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
289fn 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
314fn extract_import(node: &tree_sitter::Node, source: &str) -> Option<ExtractedImport> {
326 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 let has_asterisk = (0..node.child_count())
333 .filter_map(|i| node.child(i as u32))
334 .any(|c| c.kind() == "asterisk");
335
336 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 let slash_path = path_text.replace('.', "/");
344
345 if is_static {
346 if has_asterisk {
347 Some(ExtractedImport {
349 path: format!("{}::*", slash_path),
350 imported_symbols: vec![],
351 })
352 } else {
353 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 Some(ExtractedImport {
363 path: format!("{}::*", slash_path),
364 imported_symbols: vec![],
365 })
366 } else {
367 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
376fn split_last_segment(path: &str) -> Option<(&str, &str)> {
378 let idx = path.rfind('/')?;
379 Some((&path[..idx], &path[idx + 1..]))
380}
381
382fn 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 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
440fn 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
482pub(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 let effective_path = import_path.strip_suffix("::*").unwrap_or(import_path);
504
505 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 None
517 }
518}
519
520#[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 #[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 #[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 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 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 #[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 #[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 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 #[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 #[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 #[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 #[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 #[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 let resolved = resolver.resolve_import("com/example::*", "App.java", tmp.path());
984 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 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 #[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 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 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 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}