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