1use crate::models::{Language, SearchResult, Span, SymbolKind};
15use anyhow::{Context, Result};
16use streaming_iterator::StreamingIterator;
17use tree_sitter::{Parser, Query, QueryCursor};
18
19pub fn parse(path: &str, source: &str) -> Result<Vec<SearchResult>> {
21 let mut parser = Parser::new();
22 let language = tree_sitter_java::LANGUAGE;
23
24 parser
25 .set_language(&language.into())
26 .context("Failed to set Java language")?;
27
28 let tree = parser
29 .parse(source, None)
30 .context("Failed to parse Java source")?;
31
32 let root_node = tree.root_node();
33
34 let mut symbols = Vec::new();
35
36 symbols.extend(extract_classes(source, &root_node, &language.into())?);
38 symbols.extend(extract_interfaces(source, &root_node, &language.into())?);
39 symbols.extend(extract_enums(source, &root_node, &language.into())?);
40 symbols.extend(extract_annotations(source, &root_node, &language.into())?);
41 symbols.extend(extract_class_methods(source, &root_node, &language.into())?);
42 symbols.extend(extract_interface_methods(
43 source,
44 &root_node,
45 &language.into(),
46 )?);
47 symbols.extend(extract_fields(source, &root_node, &language.into())?);
48 symbols.extend(extract_constructors(source, &root_node, &language.into())?);
49 symbols.extend(extract_local_variables(
50 source,
51 &root_node,
52 &language.into(),
53 )?);
54
55 for symbol in &mut symbols {
57 symbol.path = path.to_string();
58 symbol.lang = Language::Java;
59 }
60
61 Ok(symbols)
62}
63
64fn extract_classes(
66 source: &str,
67 root: &tree_sitter::Node,
68 language: &tree_sitter::Language,
69) -> Result<Vec<SearchResult>> {
70 let query_str = r#"
71 (class_declaration
72 name: (identifier) @name) @class
73 "#;
74
75 let query = Query::new(language, query_str).context("Failed to create class query")?;
76
77 extract_symbols(source, root, &query, SymbolKind::Class, None)
78}
79
80fn extract_interfaces(
82 source: &str,
83 root: &tree_sitter::Node,
84 language: &tree_sitter::Language,
85) -> Result<Vec<SearchResult>> {
86 let query_str = r#"
87 (interface_declaration
88 name: (identifier) @name) @interface
89 "#;
90
91 let query = Query::new(language, query_str).context("Failed to create interface query")?;
92
93 extract_symbols(source, root, &query, SymbolKind::Interface, None)
94}
95
96fn extract_enums(
98 source: &str,
99 root: &tree_sitter::Node,
100 language: &tree_sitter::Language,
101) -> Result<Vec<SearchResult>> {
102 let query_str = r#"
103 (enum_declaration
104 name: (identifier) @name) @enum
105 "#;
106
107 let query = Query::new(language, query_str).context("Failed to create enum query")?;
108
109 extract_symbols(source, root, &query, SymbolKind::Enum, None)
110}
111
112fn extract_annotations(
116 source: &str,
117 root: &tree_sitter::Node,
118 language: &tree_sitter::Language,
119) -> Result<Vec<SearchResult>> {
120 let mut symbols = Vec::new();
121
122 let def_query_str = r#"
124 (annotation_type_declaration
125 name: (identifier) @name) @annotation
126 "#;
127
128 let def_query = Query::new(language, def_query_str)
129 .context("Failed to create annotation definition query")?;
130
131 symbols.extend(extract_symbols(
132 source,
133 root,
134 &def_query,
135 SymbolKind::Attribute,
136 None,
137 )?);
138
139 let use_query_str = r#"
141 (marker_annotation
142 name: (identifier) @name) @annotation
143
144 (annotation
145 name: (identifier) @name) @annotation
146 "#;
147
148 let use_query =
149 Query::new(language, use_query_str).context("Failed to create annotation use query")?;
150
151 symbols.extend(extract_symbols(
152 source,
153 root,
154 &use_query,
155 SymbolKind::Attribute,
156 None,
157 )?);
158
159 Ok(symbols)
160}
161
162fn extract_class_methods(
164 source: &str,
165 root: &tree_sitter::Node,
166 language: &tree_sitter::Language,
167) -> Result<Vec<SearchResult>> {
168 let query_str = r#"
169 (class_declaration
170 name: (identifier) @class_name
171 body: (class_body
172 (method_declaration
173 name: (identifier) @method_name))) @class
174
175 (enum_declaration
176 name: (identifier) @enum_name
177 body: (enum_body
178 (enum_body_declarations
179 (method_declaration
180 name: (identifier) @method_name)))) @enum
181 "#;
182
183 let query = Query::new(language, query_str).context("Failed to create method query")?;
184
185 let mut cursor = QueryCursor::new();
186 let mut matches = cursor.matches(&query, *root, source.as_bytes());
187
188 let mut symbols = Vec::new();
189
190 while let Some(match_) = matches.next() {
191 let mut scope_name = None;
192 let mut scope_type = None;
193 let mut method_name = None;
194 let mut method_node = None;
195
196 for capture in match_.captures {
197 let capture_name: &str = &query.capture_names()[capture.index as usize];
198 match capture_name {
199 "class_name" => {
200 scope_name = Some(
201 capture
202 .node
203 .utf8_text(source.as_bytes())
204 .unwrap_or("")
205 .to_string(),
206 );
207 scope_type = Some("class");
208 }
209 "enum_name" => {
210 scope_name = Some(
211 capture
212 .node
213 .utf8_text(source.as_bytes())
214 .unwrap_or("")
215 .to_string(),
216 );
217 scope_type = Some("enum");
218 }
219 "method_name" => {
220 method_name = Some(
221 capture
222 .node
223 .utf8_text(source.as_bytes())
224 .unwrap_or("")
225 .to_string(),
226 );
227 let mut current = capture.node;
229 while let Some(parent) = current.parent() {
230 if parent.kind() == "method_declaration" {
231 method_node = Some(parent);
232 break;
233 }
234 current = parent;
235 }
236 }
237 _ => {}
238 }
239 }
240
241 if let (Some(scope_name), Some(scope_type), Some(method_name), Some(node)) =
242 (scope_name, scope_type, method_name, method_node)
243 {
244 let scope = format!("{} {}", scope_type, scope_name);
245 let span = node_to_span(&node);
246 let preview = extract_preview(source, &span);
247
248 symbols.push(SearchResult::new(
249 String::new(),
250 Language::Java,
251 SymbolKind::Method,
252 Some(method_name),
253 span,
254 Some(scope),
255 preview,
256 ));
257 }
258 }
259
260 Ok(symbols)
261}
262
263fn extract_fields(
265 source: &str,
266 root: &tree_sitter::Node,
267 language: &tree_sitter::Language,
268) -> Result<Vec<SearchResult>> {
269 let query_str = r#"
270 (class_declaration
271 name: (identifier) @class_name
272 body: (class_body
273 (field_declaration
274 declarator: (variable_declarator
275 name: (identifier) @field_name)))) @class
276
277 (enum_declaration
278 name: (identifier) @enum_name
279 body: (enum_body
280 (enum_body_declarations
281 (field_declaration
282 declarator: (variable_declarator
283 name: (identifier) @field_name))))) @enum
284 "#;
285
286 let query = Query::new(language, query_str).context("Failed to create field query")?;
287
288 let mut cursor = QueryCursor::new();
289 let mut matches = cursor.matches(&query, *root, source.as_bytes());
290
291 let mut symbols = Vec::new();
292
293 while let Some(match_) = matches.next() {
294 let mut scope_name = None;
295 let mut scope_type = None;
296 let mut field_name = None;
297 let mut field_node = None;
298
299 for capture in match_.captures {
300 let capture_name: &str = &query.capture_names()[capture.index as usize];
301 match capture_name {
302 "class_name" => {
303 scope_name = Some(
304 capture
305 .node
306 .utf8_text(source.as_bytes())
307 .unwrap_or("")
308 .to_string(),
309 );
310 scope_type = Some("class");
311 }
312 "enum_name" => {
313 scope_name = Some(
314 capture
315 .node
316 .utf8_text(source.as_bytes())
317 .unwrap_or("")
318 .to_string(),
319 );
320 scope_type = Some("enum");
321 }
322 "field_name" => {
323 field_name = Some(
324 capture
325 .node
326 .utf8_text(source.as_bytes())
327 .unwrap_or("")
328 .to_string(),
329 );
330 let mut current = capture.node;
332 while let Some(parent) = current.parent() {
333 if parent.kind() == "field_declaration" {
334 field_node = Some(parent);
335 break;
336 }
337 current = parent;
338 }
339 }
340 _ => {}
341 }
342 }
343
344 if let (Some(scope_name), Some(scope_type), Some(field_name), Some(node)) =
345 (scope_name, scope_type, field_name, field_node)
346 {
347 let scope = format!("{} {}", scope_type, scope_name);
348 let span = node_to_span(&node);
349 let preview = extract_preview(source, &span);
350
351 symbols.push(SearchResult::new(
352 String::new(),
353 Language::Java,
354 SymbolKind::Variable,
355 Some(field_name),
356 span,
357 Some(scope),
358 preview,
359 ));
360 }
361 }
362
363 Ok(symbols)
364}
365
366fn extract_constructors(
368 source: &str,
369 root: &tree_sitter::Node,
370 language: &tree_sitter::Language,
371) -> Result<Vec<SearchResult>> {
372 let query_str = r#"
373 (class_declaration
374 name: (identifier) @class_name
375 body: (class_body
376 (constructor_declaration
377 name: (identifier) @constructor_name))) @class
378 "#;
379
380 let query = Query::new(language, query_str).context("Failed to create constructor query")?;
381
382 let mut cursor = QueryCursor::new();
383 let mut matches = cursor.matches(&query, *root, source.as_bytes());
384
385 let mut symbols = Vec::new();
386
387 while let Some(match_) = matches.next() {
388 let mut class_name = None;
389 let mut constructor_name = None;
390 let mut constructor_node = None;
391
392 for capture in match_.captures {
393 let capture_name: &str = &query.capture_names()[capture.index as usize];
394 match capture_name {
395 "class_name" => {
396 class_name = Some(
397 capture
398 .node
399 .utf8_text(source.as_bytes())
400 .unwrap_or("")
401 .to_string(),
402 );
403 }
404 "constructor_name" => {
405 constructor_name = Some(
406 capture
407 .node
408 .utf8_text(source.as_bytes())
409 .unwrap_or("")
410 .to_string(),
411 );
412 let mut current = capture.node;
414 while let Some(parent) = current.parent() {
415 if parent.kind() == "constructor_declaration" {
416 constructor_node = Some(parent);
417 break;
418 }
419 current = parent;
420 }
421 }
422 _ => {}
423 }
424 }
425
426 if let (Some(class_name), Some(constructor_name), Some(node)) =
427 (class_name, constructor_name, constructor_node)
428 {
429 let scope = format!("class {}", class_name);
430 let span = node_to_span(&node);
431 let preview = extract_preview(source, &span);
432
433 symbols.push(SearchResult::new(
434 String::new(),
435 Language::Java,
436 SymbolKind::Method,
437 Some(constructor_name),
438 span,
439 Some(scope),
440 preview,
441 ));
442 }
443 }
444
445 Ok(symbols)
446}
447
448fn extract_interface_methods(
450 source: &str,
451 root: &tree_sitter::Node,
452 language: &tree_sitter::Language,
453) -> Result<Vec<SearchResult>> {
454 let query_str = r#"
455 (interface_declaration
456 name: (identifier) @interface_name
457 body: (interface_body
458 (method_declaration
459 name: (identifier) @method_name))) @interface
460 "#;
461
462 let query =
463 Query::new(language, query_str).context("Failed to create interface method query")?;
464
465 let mut cursor = QueryCursor::new();
466 let mut matches = cursor.matches(&query, *root, source.as_bytes());
467
468 let mut symbols = Vec::new();
469
470 while let Some(match_) = matches.next() {
471 let mut interface_name = None;
472 let mut method_name = None;
473 let mut method_node = None;
474
475 for capture in match_.captures {
476 let capture_name: &str = &query.capture_names()[capture.index as usize];
477 match capture_name {
478 "interface_name" => {
479 interface_name = Some(
480 capture
481 .node
482 .utf8_text(source.as_bytes())
483 .unwrap_or("")
484 .to_string(),
485 );
486 }
487 "method_name" => {
488 method_name = Some(
489 capture
490 .node
491 .utf8_text(source.as_bytes())
492 .unwrap_or("")
493 .to_string(),
494 );
495 let mut current = capture.node;
497 while let Some(parent) = current.parent() {
498 if parent.kind() == "method_declaration" {
499 method_node = Some(parent);
500 break;
501 }
502 current = parent;
503 }
504 }
505 _ => {}
506 }
507 }
508
509 if let (Some(interface_name), Some(method_name), Some(node)) =
510 (interface_name, method_name, method_node)
511 {
512 let scope = format!("interface {}", interface_name);
513 let span = node_to_span(&node);
514 let preview = extract_preview(source, &span);
515
516 symbols.push(SearchResult::new(
517 String::new(),
518 Language::Java,
519 SymbolKind::Method,
520 Some(method_name),
521 span,
522 Some(scope),
523 preview,
524 ));
525 }
526 }
527
528 Ok(symbols)
529}
530
531fn extract_local_variables(
533 source: &str,
534 root: &tree_sitter::Node,
535 language: &tree_sitter::Language,
536) -> Result<Vec<SearchResult>> {
537 let query_str = r#"
538 (local_variable_declaration
539 declarator: (variable_declarator
540 name: (identifier) @name)) @var
541 "#;
542
543 let query = Query::new(language, query_str).context("Failed to create local variable query")?;
544
545 extract_symbols(source, root, &query, SymbolKind::Variable, None)
546}
547
548fn extract_symbols(
550 source: &str,
551 root: &tree_sitter::Node,
552 query: &Query,
553 kind: SymbolKind,
554 scope: Option<String>,
555) -> Result<Vec<SearchResult>> {
556 let mut cursor = QueryCursor::new();
557 let mut matches = cursor.matches(query, *root, source.as_bytes());
558
559 let mut symbols = Vec::new();
560
561 while let Some(match_) = matches.next() {
562 let mut name = None;
564 let mut full_node = None;
565
566 for capture in match_.captures {
567 let capture_name: &str = &query.capture_names()[capture.index as usize];
568 if capture_name == "name" {
569 name = Some(
570 capture
571 .node
572 .utf8_text(source.as_bytes())
573 .unwrap_or("")
574 .to_string(),
575 );
576 } else {
577 full_node = Some(capture.node);
579 }
580 }
581
582 if let (Some(name), Some(node)) = (name, full_node) {
583 let span = node_to_span(&node);
584 let preview = extract_preview(source, &span);
585
586 symbols.push(SearchResult::new(
587 String::new(),
588 Language::Java,
589 kind.clone(),
590 Some(name),
591 span,
592 scope.clone(),
593 preview,
594 ));
595 }
596 }
597
598 Ok(symbols)
599}
600
601fn node_to_span(node: &tree_sitter::Node) -> Span {
603 let start = node.start_position();
604 let end = node.end_position();
605
606 Span::new(
607 start.row + 1, start.column,
609 end.row + 1,
610 end.column,
611 )
612}
613
614fn extract_preview(source: &str, span: &Span) -> String {
616 let lines: Vec<&str> = source.lines().collect();
617
618 let start_idx = (span.start_line - 1) as usize; let end_idx = (start_idx + 7).min(lines.len());
621
622 lines[start_idx..end_idx].join("\n")
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn test_parse_class() {
631 let source = r#"
632public class User {
633 private String name;
634 private int age;
635}
636 "#;
637
638 let symbols = parse("test.java", source).unwrap();
639
640 let class_symbols: Vec<_> = symbols
641 .iter()
642 .filter(|s| matches!(s.kind, SymbolKind::Class))
643 .collect();
644
645 assert_eq!(class_symbols.len(), 1);
646 assert_eq!(class_symbols[0].symbol.as_deref(), Some("User"));
647 }
648
649 #[test]
650 fn test_parse_class_with_methods() {
651 let source = r#"
652public class Calculator {
653 public int add(int a, int b) {
654 return a + b;
655 }
656
657 public int subtract(int a, int b) {
658 return a - b;
659 }
660}
661 "#;
662
663 let symbols = parse("test.java", source).unwrap();
664
665 let method_symbols: Vec<_> = symbols
666 .iter()
667 .filter(|s| matches!(s.kind, SymbolKind::Method))
668 .collect();
669
670 assert_eq!(method_symbols.len(), 2);
671 assert!(
672 method_symbols
673 .iter()
674 .any(|s| s.symbol.as_deref() == Some("add"))
675 );
676 assert!(
677 method_symbols
678 .iter()
679 .any(|s| s.symbol.as_deref() == Some("subtract"))
680 );
681
682 for method in method_symbols {
684 }
686 }
687
688 #[test]
689 fn test_parse_interface() {
690 let source = r#"
691public interface Drawable {
692 void draw();
693 void resize(int width, int height);
694}
695 "#;
696
697 let symbols = parse("test.java", source).unwrap();
698
699 let interface_symbols: Vec<_> = symbols
700 .iter()
701 .filter(|s| matches!(s.kind, SymbolKind::Interface))
702 .collect();
703
704 assert_eq!(interface_symbols.len(), 1);
705 assert_eq!(interface_symbols[0].symbol.as_deref(), Some("Drawable"));
706 }
707
708 #[test]
709 fn test_parse_enum() {
710 let source = r#"
711public enum Status {
712 ACTIVE,
713 INACTIVE,
714 PENDING
715}
716 "#;
717
718 let symbols = parse("test.java", source).unwrap();
719
720 let enum_symbols: Vec<_> = symbols
721 .iter()
722 .filter(|s| matches!(s.kind, SymbolKind::Enum))
723 .collect();
724
725 assert_eq!(enum_symbols.len(), 1);
726 assert_eq!(enum_symbols[0].symbol.as_deref(), Some("Status"));
727 }
728
729 #[test]
730 fn test_parse_fields() {
731 let source = r#"
732public class Config {
733 private static final int MAX_SIZE = 100;
734 private String hostname;
735 public int port;
736}
737 "#;
738
739 let symbols = parse("test.java", source).unwrap();
740
741 let field_symbols: Vec<_> = symbols
742 .iter()
743 .filter(|s| matches!(s.kind, SymbolKind::Variable))
744 .collect();
745
746 assert_eq!(field_symbols.len(), 3);
747 assert!(
748 field_symbols
749 .iter()
750 .any(|s| s.symbol.as_deref() == Some("MAX_SIZE"))
751 );
752 assert!(
753 field_symbols
754 .iter()
755 .any(|s| s.symbol.as_deref() == Some("hostname"))
756 );
757 assert!(
758 field_symbols
759 .iter()
760 .any(|s| s.symbol.as_deref() == Some("port"))
761 );
762 }
763
764 #[test]
765 fn test_parse_constructor() {
766 let source = r#"
767public class User {
768 private String name;
769
770 public User(String name) {
771 this.name = name;
772 }
773
774 public User() {
775 this("Anonymous");
776 }
777}
778 "#;
779
780 let symbols = parse("test.java", source).unwrap();
781
782 let constructor_symbols: Vec<_> = symbols
783 .iter()
784 .filter(|s| matches!(s.kind, SymbolKind::Method) && s.symbol.as_deref() == Some("User"))
785 .collect();
786
787 assert_eq!(constructor_symbols.len(), 2);
788 }
789
790 #[test]
791 fn test_parse_abstract_class() {
792 let source = r#"
793public abstract class Animal {
794 protected String name;
795
796 public abstract void makeSound();
797
798 public void sleep() {
799 System.out.println("Sleeping...");
800 }
801}
802 "#;
803
804 let symbols = parse("test.java", source).unwrap();
805
806 let class_symbols: Vec<_> = symbols
807 .iter()
808 .filter(|s| matches!(s.kind, SymbolKind::Class))
809 .collect();
810
811 assert_eq!(class_symbols.len(), 1);
812 assert_eq!(class_symbols[0].symbol.as_deref(), Some("Animal"));
813
814 let method_symbols: Vec<_> = symbols
815 .iter()
816 .filter(|s| matches!(s.kind, SymbolKind::Method))
817 .collect();
818
819 assert_eq!(method_symbols.len(), 2);
820 assert!(
821 method_symbols
822 .iter()
823 .any(|s| s.symbol.as_deref() == Some("makeSound"))
824 );
825 assert!(
826 method_symbols
827 .iter()
828 .any(|s| s.symbol.as_deref() == Some("sleep"))
829 );
830 }
831
832 #[test]
833 fn test_parse_nested_class() {
834 let source = r#"
835public class Outer {
836 private int outerField;
837
838 public static class Nested {
839 private int nestedField;
840
841 public void nestedMethod() {
842 // ...
843 }
844 }
845
846 public void outerMethod() {
847 // ...
848 }
849}
850 "#;
851
852 let symbols = parse("test.java", source).unwrap();
853
854 let class_symbols: Vec<_> = symbols
855 .iter()
856 .filter(|s| matches!(s.kind, SymbolKind::Class))
857 .collect();
858
859 assert_eq!(class_symbols.len(), 2);
860 assert!(
861 class_symbols
862 .iter()
863 .any(|s| s.symbol.as_deref() == Some("Outer"))
864 );
865 assert!(
866 class_symbols
867 .iter()
868 .any(|s| s.symbol.as_deref() == Some("Nested"))
869 );
870 }
871
872 #[test]
873 fn test_parse_interface_with_methods() {
874 let source = r#"
875public interface Repository<T> {
876 T findById(Long id);
877 List<T> findAll();
878 void save(T entity);
879 void delete(T entity);
880}
881 "#;
882
883 let symbols = parse("test.java", source).unwrap();
884
885 let interface_symbols: Vec<_> = symbols
886 .iter()
887 .filter(|s| matches!(s.kind, SymbolKind::Interface))
888 .collect();
889
890 assert_eq!(interface_symbols.len(), 1);
891
892 let method_symbols: Vec<_> = symbols
893 .iter()
894 .filter(|s| matches!(s.kind, SymbolKind::Method))
895 .collect();
896
897 assert_eq!(method_symbols.len(), 4);
898 assert!(
899 method_symbols
900 .iter()
901 .any(|s| s.symbol.as_deref() == Some("findById"))
902 );
903 assert!(
904 method_symbols
905 .iter()
906 .any(|s| s.symbol.as_deref() == Some("findAll"))
907 );
908 assert!(
909 method_symbols
910 .iter()
911 .any(|s| s.symbol.as_deref() == Some("save"))
912 );
913 assert!(
914 method_symbols
915 .iter()
916 .any(|s| s.symbol.as_deref() == Some("delete"))
917 );
918 }
919
920 #[test]
921 fn test_parse_enum_with_methods() {
922 let source = r#"
923public enum Day {
924 MONDAY, TUESDAY, WEDNESDAY;
925
926 public boolean isWeekend() {
927 return this == SATURDAY || this == SUNDAY;
928 }
929}
930 "#;
931
932 let symbols = parse("test.java", source).unwrap();
933
934 let enum_symbols: Vec<_> = symbols
935 .iter()
936 .filter(|s| matches!(s.kind, SymbolKind::Enum))
937 .collect();
938
939 assert_eq!(enum_symbols.len(), 1);
940
941 let method_symbols: Vec<_> = symbols
942 .iter()
943 .filter(|s| matches!(s.kind, SymbolKind::Method))
944 .collect();
945
946 assert_eq!(method_symbols.len(), 1);
947 assert_eq!(method_symbols[0].symbol.as_deref(), Some("isWeekend"));
948 }
949
950 #[test]
951 fn test_parse_mixed_symbols() {
952 let source = r#"
953package com.example;
954
955public interface UserService {
956 User findUser(Long id);
957}
958
959public class User {
960 private Long id;
961 private String name;
962
963 public User(Long id, String name) {
964 this.id = id;
965 this.name = name;
966 }
967
968 public String getName() {
969 return name;
970 }
971}
972
973public enum UserRole {
974 ADMIN, USER, GUEST
975}
976 "#;
977
978 let symbols = parse("test.java", source).unwrap();
979
980 assert!(symbols.len() >= 7);
982
983 let kinds: Vec<&SymbolKind> = symbols.iter().map(|s| &s.kind).collect();
984 assert!(kinds.contains(&&SymbolKind::Interface));
985 assert!(kinds.contains(&&SymbolKind::Class));
986 assert!(kinds.contains(&&SymbolKind::Enum));
987 assert!(kinds.contains(&&SymbolKind::Variable));
988 assert!(kinds.contains(&&SymbolKind::Method));
989 }
990
991 #[test]
992 fn test_parse_generic_class() {
993 let source = r#"
994public class Container<T> {
995 private T value;
996
997 public Container(T value) {
998 this.value = value;
999 }
1000
1001 public T getValue() {
1002 return value;
1003 }
1004
1005 public void setValue(T value) {
1006 this.value = value;
1007 }
1008}
1009 "#;
1010
1011 let symbols = parse("test.java", source).unwrap();
1012
1013 let class_symbols: Vec<_> = symbols
1014 .iter()
1015 .filter(|s| matches!(s.kind, SymbolKind::Class))
1016 .collect();
1017
1018 assert_eq!(class_symbols.len(), 1);
1019 assert_eq!(class_symbols[0].symbol.as_deref(), Some("Container"));
1020
1021 let method_symbols: Vec<_> = symbols
1022 .iter()
1023 .filter(|s| matches!(s.kind, SymbolKind::Method))
1024 .collect();
1025
1026 assert!(method_symbols.len() >= 3);
1027 }
1028
1029 #[test]
1030 fn test_local_variables_included() {
1031 let source = r#"
1032public class Calculator {
1033 private int globalCount = 10;
1034
1035 public int calculate(int x) {
1036 int localVar = x * 2;
1037 int anotherLocal = 5;
1038 return localVar + anotherLocal + globalCount;
1039 }
1040}
1041 "#;
1042
1043 let symbols = parse("test.java", source).unwrap();
1044
1045 let var_symbols: Vec<_> = symbols
1046 .iter()
1047 .filter(|s| matches!(s.kind, SymbolKind::Variable))
1048 .collect();
1049
1050 assert_eq!(var_symbols.len(), 3);
1052 assert!(
1053 var_symbols
1054 .iter()
1055 .any(|s| s.symbol.as_deref() == Some("globalCount"))
1056 );
1057 assert!(
1058 var_symbols
1059 .iter()
1060 .any(|s| s.symbol.as_deref() == Some("localVar"))
1061 );
1062 assert!(
1063 var_symbols
1064 .iter()
1065 .any(|s| s.symbol.as_deref() == Some("anotherLocal"))
1066 );
1067
1068 let global_count = var_symbols
1070 .iter()
1071 .find(|s| s.symbol.as_deref() == Some("globalCount"))
1072 .unwrap();
1073 let local_var = var_symbols
1076 .iter()
1077 .find(|s| s.symbol.as_deref() == Some("localVar"))
1078 .unwrap();
1079 }
1081
1082 #[test]
1083 fn test_parse_annotation_type() {
1084 let source = r#"
1085public @interface Test {
1086}
1087
1088@interface Author {
1089 String name();
1090 String date();
1091}
1092
1093@interface Retention {
1094 RetentionPolicy value();
1095}
1096 "#;
1097
1098 let symbols = parse("test.java", source).unwrap();
1099
1100 let annotation_symbols: Vec<_> = symbols
1101 .iter()
1102 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1103 .collect();
1104
1105 assert!(
1107 annotation_symbols
1108 .iter()
1109 .any(|s| s.symbol.as_deref() == Some("Test"))
1110 );
1111 assert!(
1112 annotation_symbols
1113 .iter()
1114 .any(|s| s.symbol.as_deref() == Some("Author"))
1115 );
1116 assert!(
1117 annotation_symbols
1118 .iter()
1119 .any(|s| s.symbol.as_deref() == Some("Retention"))
1120 );
1121 }
1122
1123 #[test]
1124 fn test_parse_annotation_uses() {
1125 let source = r#"
1126@Test
1127public void testMethod() {
1128 assertEquals(1, 1);
1129}
1130
1131@Override
1132@Deprecated
1133public String toString() {
1134 return "example";
1135}
1136
1137@SuppressWarnings("unchecked")
1138public class MyClass {
1139 @Autowired
1140 private Service service;
1141
1142 @Test
1143 @DisplayName("Should work")
1144 public void anotherTest() {}
1145}
1146 "#;
1147
1148 let symbols = parse("test.java", source).unwrap();
1149
1150 let annotation_symbols: Vec<_> = symbols
1151 .iter()
1152 .filter(|s| matches!(s.kind, SymbolKind::Attribute))
1153 .collect();
1154
1155 assert!(
1157 annotation_symbols
1158 .iter()
1159 .any(|s| s.symbol.as_deref() == Some("Test"))
1160 );
1161 assert!(
1162 annotation_symbols
1163 .iter()
1164 .any(|s| s.symbol.as_deref() == Some("Override"))
1165 );
1166 assert!(
1167 annotation_symbols
1168 .iter()
1169 .any(|s| s.symbol.as_deref() == Some("Deprecated"))
1170 );
1171 assert!(
1172 annotation_symbols
1173 .iter()
1174 .any(|s| s.symbol.as_deref() == Some("SuppressWarnings"))
1175 );
1176 assert!(
1177 annotation_symbols
1178 .iter()
1179 .any(|s| s.symbol.as_deref() == Some("Autowired"))
1180 );
1181 assert!(
1182 annotation_symbols
1183 .iter()
1184 .any(|s| s.symbol.as_deref() == Some("DisplayName"))
1185 );
1186
1187 let test_count = annotation_symbols
1189 .iter()
1190 .filter(|s| s.symbol.as_deref() == Some("Test"))
1191 .count();
1192 assert_eq!(test_count, 2);
1193 }
1194
1195 #[test]
1196 fn test_extract_java_imports() {
1197 let source = r#"
1198 import java.util.List;
1199 import java.util.ArrayList;
1200 import java.io.IOException;
1201 import org.springframework.stereotype.Service;
1202
1203 @Service
1204 public class UserService {
1205 private List<String> users = new ArrayList<>();
1206
1207 public void addUser(String name) throws IOException {
1208 users.add(name);
1209 }
1210 }
1211 "#;
1212
1213 use crate::parsers::DependencyExtractor;
1214
1215 let deps = JavaDependencyExtractor::extract_dependencies(source).unwrap();
1216
1217 assert_eq!(deps.len(), 4, "Should extract 4 import statements");
1218 assert!(deps.iter().any(|d| d.imported_path == "java.util.List"));
1219 assert!(
1220 deps.iter()
1221 .any(|d| d.imported_path == "java.util.ArrayList")
1222 );
1223 assert!(
1224 deps.iter()
1225 .any(|d| d.imported_path == "java.io.IOException")
1226 );
1227 assert!(
1228 deps.iter()
1229 .any(|d| d.imported_path == "org.springframework.stereotype.Service")
1230 );
1231
1232 let java_util_list = deps
1234 .iter()
1235 .find(|d| d.imported_path == "java.util.List")
1236 .unwrap();
1237 assert!(
1238 matches!(java_util_list.import_type, ImportType::Stdlib),
1239 "java.util imports should be classified as Stdlib"
1240 );
1241
1242 let spring_service = deps
1244 .iter()
1245 .find(|d| d.imported_path == "org.springframework.stereotype.Service")
1246 .unwrap();
1247 assert!(
1248 matches!(spring_service.import_type, ImportType::External),
1249 "org.springframework imports should be classified as External"
1250 );
1251 }
1252}
1253
1254use crate::models::ImportType;
1259use crate::parsers::{DependencyExtractor, ImportInfo};
1260
1261pub struct JavaDependencyExtractor;
1263
1264impl DependencyExtractor for JavaDependencyExtractor {
1265 fn extract_dependencies(source: &str) -> Result<Vec<ImportInfo>> {
1266 let mut parser = Parser::new();
1267 let language = tree_sitter_java::LANGUAGE;
1268
1269 parser
1270 .set_language(&language.into())
1271 .context("Failed to set Java language")?;
1272
1273 let tree = parser
1274 .parse(source, None)
1275 .context("Failed to parse Java source")?;
1276
1277 let root_node = tree.root_node();
1278
1279 let mut imports = Vec::new();
1280
1281 imports.extend(extract_java_imports(source, &root_node)?);
1283
1284 Ok(imports)
1285 }
1286}
1287
1288fn extract_java_imports(source: &str, root: &tree_sitter::Node) -> Result<Vec<ImportInfo>> {
1290 let language = tree_sitter_java::LANGUAGE;
1291
1292 let query_str = r#"
1293 (import_declaration
1294 [
1295 (scoped_identifier) @import_path
1296 (identifier) @import_path
1297 ])
1298 "#;
1299
1300 let query =
1301 Query::new(&language.into(), query_str).context("Failed to create Java import query")?;
1302
1303 let mut cursor = QueryCursor::new();
1304 let mut matches = cursor.matches(&query, *root, source.as_bytes());
1305
1306 let mut imports = Vec::new();
1307
1308 while let Some(match_) = matches.next() {
1309 for capture in match_.captures {
1310 let capture_name: &str = &query.capture_names()[capture.index as usize];
1311 if capture_name == "import_path" {
1312 let path = capture
1313 .node
1314 .utf8_text(source.as_bytes())
1315 .unwrap_or("")
1316 .to_string();
1317 let import_type = classify_java_import(&path);
1318 let line_number = capture.node.start_position().row + 1;
1319
1320 imports.push(ImportInfo {
1321 imported_path: path,
1322 import_type,
1323 line_number,
1324 imported_symbols: None, });
1326 }
1327 }
1328 }
1329
1330 Ok(imports)
1331}
1332
1333fn classify_java_import(import_path: &str) -> ImportType {
1335 classify_java_import_impl(import_path, None)
1336}
1337
1338pub fn find_java_package_name(root: &std::path::Path) -> Option<String> {
1341 if let Some(package) = find_maven_package(root) {
1343 return Some(package);
1344 }
1345
1346 if let Some(package) = find_gradle_package(root) {
1348 return Some(package);
1349 }
1350
1351 find_package_from_sources(root)
1353}
1354
1355fn find_maven_package(root: &std::path::Path) -> Option<String> {
1357 let pom_path = root.join("pom.xml");
1358 if !pom_path.exists() {
1359 return None;
1360 }
1361
1362 let content = std::fs::read_to_string(&pom_path).ok()?;
1363
1364 for line in content.lines() {
1366 let trimmed = line.trim();
1367 if trimmed.starts_with("<groupId>") && trimmed.ends_with("</groupId>") {
1368 let start = "<groupId>".len();
1369 let end = trimmed.len() - "</groupId>".len();
1370 return Some(trimmed[start..end].to_string());
1371 }
1372 }
1373
1374 None
1375}
1376
1377fn find_gradle_package(root: &std::path::Path) -> Option<String> {
1379 if let Some(package) = find_gradle_package_in_file(&root.join("build.gradle")) {
1381 return Some(package);
1382 }
1383
1384 find_gradle_package_in_file(&root.join("build.gradle.kts"))
1386}
1387
1388fn find_gradle_package_in_file(gradle_path: &std::path::Path) -> Option<String> {
1389 if !gradle_path.exists() {
1390 return None;
1391 }
1392
1393 let content = std::fs::read_to_string(gradle_path).ok()?;
1394
1395 for line in content.lines() {
1396 let trimmed = line.trim();
1397
1398 if trimmed.starts_with("group") {
1401 if let Some(equals_idx) = trimmed.find('=') {
1402 let value = &trimmed[equals_idx + 1..].trim();
1403 let value = value.trim_matches(|c| c == '\'' || c == '"');
1405 return Some(value.to_string());
1406 }
1407 }
1408 }
1409
1410 None
1411}
1412
1413fn find_package_from_sources(root: &std::path::Path) -> Option<String> {
1415 use std::collections::HashMap;
1416
1417 let mut package_counts: HashMap<String, usize> = HashMap::new();
1418
1419 fn walk_dir(dir: &std::path::Path, package_counts: &mut HashMap<String, usize>, depth: usize) {
1421 if depth > 10 {
1423 return;
1424 }
1425
1426 let entries = match std::fs::read_dir(dir) {
1427 Ok(e) => e,
1428 Err(_) => return,
1429 };
1430
1431 for entry in entries.flatten() {
1432 let path = entry.path();
1433
1434 if path.is_dir() {
1435 walk_dir(&path, package_counts, depth + 1);
1436 } else if path.extension().and_then(|s| s.to_str()) == Some("java") {
1437 if let Ok(content) = std::fs::read_to_string(&path) {
1438 for line in content.lines().take(20) {
1440 let trimmed = line.trim();
1442 if trimmed.starts_with("package ") && trimmed.ends_with(';') {
1443 let package = &trimmed[8..trimmed.len() - 1].trim();
1444
1445 let parts: Vec<&str> = package.split('.').collect();
1447 if parts.len() >= 2 {
1448 let base_package = format!("{}.{}", parts[0], parts[1]);
1449 *package_counts.entry(base_package).or_insert(0) += 1;
1450 }
1451 break;
1452 }
1453 }
1454 }
1455 }
1456 }
1457 }
1458
1459 walk_dir(root, &mut package_counts, 0);
1460
1461 package_counts
1463 .into_iter()
1464 .max_by_key(|(_, count)| *count)
1465 .map(|(package, _)| package)
1466}
1467
1468pub fn reclassify_java_import(import_path: &str, package_prefix: Option<&str>) -> ImportType {
1471 classify_java_import_impl(import_path, package_prefix)
1472}
1473
1474fn classify_java_import_impl(import_path: &str, package_prefix: Option<&str>) -> ImportType {
1475 if let Some(prefix) = package_prefix {
1477 if import_path.starts_with(prefix) {
1478 return ImportType::Internal;
1479 }
1480 }
1481
1482 const STDLIB_PACKAGES: &[&str] = &[
1484 "java.lang",
1485 "java.util",
1486 "java.io",
1487 "java.nio",
1488 "java.net",
1489 "java.text",
1490 "java.math",
1491 "java.time",
1492 "java.sql",
1493 "java.security",
1494 "java.awt",
1495 "java.swing",
1496 "javax.swing",
1497 "javax.sql",
1498 "javax.crypto",
1499 "javax.net",
1500 "javax.xml",
1501 "javax.annotation",
1502 "javax.servlet",
1503 "org.w3c.dom",
1504 "org.xml.sax",
1505 ];
1506
1507 for stdlib_pkg in STDLIB_PACKAGES {
1509 if import_path.starts_with(stdlib_pkg) {
1510 return ImportType::Stdlib;
1511 }
1512 }
1513
1514 ImportType::External
1516}
1517
1518#[derive(Debug, Clone)]
1524pub struct JavaProject {
1525 pub package_name: String,
1527 pub project_root: String,
1529 pub abs_project_root: String,
1531}
1532
1533pub fn find_all_maven_gradle_projects(root: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
1536 let mut config_files = Vec::new();
1537
1538 let walker = ignore::WalkBuilder::new(root)
1539 .follow_links(false)
1540 .git_ignore(true)
1541 .build();
1542
1543 for entry in walker {
1544 let entry = entry?;
1545 let path = entry.path();
1546
1547 if path.is_file() {
1548 let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
1549
1550 if filename == "pom.xml" || filename == "build.gradle" || filename == "build.gradle.kts"
1552 {
1553 config_files.push(path.to_path_buf());
1554 log::trace!("Found Java/Kotlin config: {}", path.display());
1555 }
1556 }
1557 }
1558
1559 log::debug!(
1560 "Found {} Java/Kotlin project config files",
1561 config_files.len()
1562 );
1563 Ok(config_files)
1564}
1565
1566pub fn parse_all_java_projects(root: &std::path::Path) -> Result<Vec<JavaProject>> {
1569 let config_files = find_all_maven_gradle_projects(root)?;
1570 let mut projects = Vec::new();
1571
1572 let root_abs = root
1573 .canonicalize()
1574 .with_context(|| format!("Failed to canonicalize root path: {}", root.display()))?;
1575
1576 for config_path in &config_files {
1577 if let Some(project_dir) = config_path.parent() {
1579 if let Some(package_name) = extract_package_from_config(config_path) {
1581 let project_abs = project_dir.canonicalize().with_context(|| {
1582 format!(
1583 "Failed to canonicalize project path: {}",
1584 project_dir.display()
1585 )
1586 })?;
1587
1588 let project_rel = project_abs
1589 .strip_prefix(&root_abs)
1590 .unwrap_or(project_dir)
1591 .to_string_lossy()
1592 .to_string();
1593
1594 projects.push(JavaProject {
1595 package_name: package_name.clone(),
1596 project_root: project_rel,
1597 abs_project_root: project_abs.to_string_lossy().to_string(),
1598 });
1599
1600 log::trace!(
1601 "Parsed Java/Kotlin project: {} at {}",
1602 package_name,
1603 project_dir.display()
1604 );
1605 }
1606 }
1607 }
1608
1609 log::info!("Parsed {} Java/Kotlin projects", projects.len());
1610 Ok(projects)
1611}
1612
1613fn extract_package_from_config(config_path: &std::path::Path) -> Option<String> {
1615 let filename = config_path.file_name()?.to_str()?;
1616
1617 match filename {
1618 "pom.xml" => {
1619 let content = std::fs::read_to_string(config_path).ok()?;
1621 for line in content.lines() {
1622 let trimmed = line.trim();
1623 if trimmed.starts_with("<groupId>") && trimmed.ends_with("</groupId>") {
1624 let start = "<groupId>".len();
1625 let end = trimmed.len() - "</groupId>".len();
1626 return Some(trimmed[start..end].to_string());
1627 }
1628 }
1629 None
1630 }
1631 "build.gradle" | "build.gradle.kts" => {
1632 let content = std::fs::read_to_string(config_path).ok()?;
1634 for line in content.lines() {
1635 let trimmed = line.trim();
1636 if trimmed.starts_with("group") {
1637 if let Some(equals_idx) = trimmed.find('=') {
1638 let value = &trimmed[equals_idx + 1..].trim();
1639 let value = value.trim_matches(|c| c == '\'' || c == '"');
1640 return Some(value.to_string());
1641 }
1642 }
1643 }
1644 None
1645 }
1646 _ => None,
1647 }
1648}
1649
1650pub fn resolve_java_import_to_path(
1656 import_path: &str,
1657 projects: &[JavaProject],
1658 _current_file_path: Option<&str>,
1659) -> Option<String> {
1660 for project in projects {
1663 if import_path.starts_with(&project.package_name) {
1664 let file_path = import_path.replace('.', "/");
1666
1667 let candidates = vec![
1669 format!("{}/src/main/java/{}.java", project.project_root, file_path),
1671 format!("{}/src/{}.java", project.project_root, file_path),
1673 format!("{}/{}.java", project.project_root, file_path),
1675 ];
1676
1677 for candidate in candidates {
1678 log::trace!("Checking Java import path: {}", candidate);
1679 return Some(candidate);
1680 }
1681 }
1682 }
1683
1684 None
1685}
1686
1687pub fn resolve_kotlin_import_to_path(
1691 import_path: &str,
1692 projects: &[JavaProject],
1693 _current_file_path: Option<&str>,
1694) -> Option<String> {
1695 for project in projects {
1697 if import_path.starts_with(&project.package_name) {
1698 let file_path = import_path.replace('.', "/");
1699
1700 let candidates = vec![
1702 format!("{}/src/main/kotlin/{}.kt", project.project_root, file_path),
1704 format!("{}/src/main/java/{}.kt", project.project_root, file_path),
1706 format!("{}/src/{}.kt", project.project_root, file_path),
1708 format!("{}/{}.kt", project.project_root, file_path),
1710 ];
1711
1712 for candidate in candidates {
1713 log::trace!("Checking Kotlin import path: {}", candidate);
1714 return Some(candidate);
1715 }
1716 }
1717 }
1718
1719 None
1720}
1721
1722#[cfg(test)]
1723mod monorepo_tests {
1724 use super::*;
1725 use std::fs;
1726 use tempfile::TempDir;
1727
1728 #[test]
1729 fn test_resolve_java_import_maven_structure() {
1730 let projects = vec![JavaProject {
1731 package_name: "com.example".to_string(),
1732 project_root: "project1".to_string(),
1733 abs_project_root: "/abs/project1".to_string(),
1734 }];
1735
1736 let resolved = resolve_java_import_to_path("com.example.UserService", &projects, None);
1737
1738 assert!(resolved.is_some());
1739 let path = resolved.unwrap();
1740 assert!(path.contains("src/main/java/com/example/UserService.java"));
1742 }
1743
1744 #[test]
1745 fn test_resolve_kotlin_import() {
1746 let projects = vec![JavaProject {
1747 package_name: "org.acme".to_string(),
1748 project_root: "kotlin-project".to_string(),
1749 abs_project_root: "/abs/kotlin-project".to_string(),
1750 }];
1751
1752 let resolved = resolve_kotlin_import_to_path("org.acme.Repository", &projects, None);
1753
1754 assert!(resolved.is_some());
1755 let path = resolved.unwrap();
1756 assert!(path.contains("src/main/kotlin/org/acme/Repository.kt"));
1757 }
1758
1759 #[test]
1760 fn test_resolve_java_import_no_match() {
1761 let projects = vec![JavaProject {
1762 package_name: "com.example".to_string(),
1763 project_root: "project1".to_string(),
1764 abs_project_root: "/abs/project1".to_string(),
1765 }];
1766
1767 let resolved = resolve_java_import_to_path("org.other.Service", &projects, None);
1769
1770 assert!(resolved.is_none());
1771 }
1772
1773 #[test]
1774 fn test_resolve_java_import_monorepo() {
1775 let projects = vec![
1776 JavaProject {
1777 package_name: "com.example.service1".to_string(),
1778 project_root: "services/service1".to_string(),
1779 abs_project_root: "/abs/services/service1".to_string(),
1780 },
1781 JavaProject {
1782 package_name: "com.example.service2".to_string(),
1783 project_root: "services/service2".to_string(),
1784 abs_project_root: "/abs/services/service2".to_string(),
1785 },
1786 ];
1787
1788 let resolved1 =
1790 resolve_java_import_to_path("com.example.service1.UserController", &projects, None);
1791 assert!(resolved1.is_some());
1792 assert!(resolved1.unwrap().contains("services/service1"));
1793
1794 let resolved2 =
1796 resolve_java_import_to_path("com.example.service2.ProductController", &projects, None);
1797 assert!(resolved2.is_some());
1798 assert!(resolved2.unwrap().contains("services/service2"));
1799 }
1800
1801 #[test]
1802 fn test_extract_package_from_pom_xml() {
1803 let temp = TempDir::new().unwrap();
1804 let pom_path = temp.path().join("pom.xml");
1805
1806 fs::write(
1807 &pom_path,
1808 r#"
1809<?xml version="1.0" encoding="UTF-8"?>
1810<project>
1811 <groupId>com.example.myapp</groupId>
1812 <artifactId>my-application</artifactId>
1813</project>
1814 "#,
1815 )
1816 .unwrap();
1817
1818 let package = extract_package_from_config(&pom_path);
1819 assert_eq!(package, Some("com.example.myapp".to_string()));
1820 }
1821
1822 #[test]
1823 fn test_extract_package_from_gradle() {
1824 let temp = TempDir::new().unwrap();
1825 let gradle_path = temp.path().join("build.gradle");
1826
1827 fs::write(
1828 &gradle_path,
1829 r#"
1830group = 'org.example.myproject'
1831version = '1.0.0'
1832 "#,
1833 )
1834 .unwrap();
1835
1836 let package = extract_package_from_config(&gradle_path);
1837 assert_eq!(package, Some("org.example.myproject".to_string()));
1838 }
1839
1840 #[test]
1841 fn test_extract_package_from_gradle_kts() {
1842 let temp = TempDir::new().unwrap();
1843 let gradle_path = temp.path().join("build.gradle.kts");
1844
1845 fs::write(
1846 &gradle_path,
1847 r#"
1848group = "com.acme.tools"
1849version = "2.0.0"
1850 "#,
1851 )
1852 .unwrap();
1853
1854 let package = extract_package_from_config(&gradle_path);
1855 assert_eq!(package, Some("com.acme.tools".to_string()));
1856 }
1857}