Skip to main content

reflex/parsers/
java.rs

1//! Java language parser using Tree-sitter
2//!
3//! Extracts symbols from Java source code:
4//! - Classes (regular, abstract, final)
5//! - Interfaces
6//! - Enums
7//! - Records (Java 14+)
8//! - Methods (with class scope, visibility)
9//! - Fields (public, private, protected, static)
10//! - Constructors
11//! - Annotations
12//! - Local variables (inside method bodies)
13
14use crate::models::{Language, SearchResult, Span, SymbolKind};
15use anyhow::{Context, Result};
16use streaming_iterator::StreamingIterator;
17use tree_sitter::{Parser, Query, QueryCursor};
18
19/// Parse Java source code and extract symbols
20pub 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    // Extract different types of symbols using Tree-sitter queries
37    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    // Add file path to all symbols
56    for symbol in &mut symbols {
57        symbol.path = path.to_string();
58        symbol.lang = Language::Java;
59    }
60
61    Ok(symbols)
62}
63
64/// Extract class declarations
65fn 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
80/// Extract interface declarations
81fn 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
96/// Extract enum declarations
97fn 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
112/// Extract annotations: BOTH definitions and uses
113/// Definitions: @interface Test { ... }
114/// Uses: @Test public void testMethod()
115fn 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    // Part 1: Extract annotation type DEFINITIONS (@interface)
123    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    // Part 2: Extract annotation USES (@Test, @Override, etc.)
140    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
162/// Extract method declarations from classes
163fn 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                    // Find the parent method_declaration node
228                    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
263/// Extract field declarations from classes
264fn 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                    // Find the parent field_declaration node
331                    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
366/// Extract constructor declarations
367fn 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                    // Find the parent constructor_declaration node
413                    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
448/// Extract method declarations from interfaces
449fn 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                    // Find the parent method_declaration node
496                    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
531/// Extract local variable declarations from method bodies
532fn 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
548/// Generic symbol extraction helper
549fn 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        // Find the name capture and the full node
563        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                // Assume any other capture is the full node
578                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
601/// Convert a Tree-sitter node to a Span
602fn 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, // Convert 0-indexed to 1-indexed
608        start.column,
609        end.row + 1,
610        end.column,
611    )
612}
613
614/// Extract a preview (7 lines) around the symbol
615fn extract_preview(source: &str, span: &Span) -> String {
616    let lines: Vec<&str> = source.lines().collect();
617
618    // Extract 7 lines: the start line and 6 following lines
619    let start_idx = (span.start_line - 1) as usize; // Convert back to 0-indexed
620    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        // Check scope
683        for method in method_symbols {
684            // Removed: scope field no longer exists: assert_eq!(method.scope.as_ref().unwrap(), "class Calculator");
685        }
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        // Should find: interface, class, enum, fields, constructor, methods
981        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        // Should find both field (globalCount) and local variables (localVar, anotherLocal)
1051        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        // Check scopes: field should have scope, local vars should not
1069        let global_count = var_symbols
1070            .iter()
1071            .find(|s| s.symbol.as_deref() == Some("globalCount"))
1072            .unwrap();
1073        // Removed: scope field no longer exists: assert_eq!(global_count.scope.as_ref().unwrap(), "class Calculator");
1074
1075        let local_var = var_symbols
1076            .iter()
1077            .find(|s| s.symbol.as_deref() == Some("localVar"))
1078            .unwrap();
1079        // Removed: scope field no longer exists: assert_eq!(local_var.scope, None);
1080    }
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        // Should find annotation definitions
1106        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        // Should find annotation uses
1156        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        // Should find Test twice (2 uses)
1188        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        // Check stdlib classification
1233        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        // Check external classification
1243        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
1254// ============================================================================
1255// Dependency Extraction
1256// ============================================================================
1257
1258use crate::models::ImportType;
1259use crate::parsers::{DependencyExtractor, ImportInfo};
1260
1261/// Java dependency extractor
1262pub 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        // Extract import statements
1282        imports.extend(extract_java_imports(source, &root_node)?);
1283
1284        Ok(imports)
1285    }
1286}
1287
1288/// Extract Java import statements
1289fn 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, // Java imports are package-level
1325                });
1326            }
1327        }
1328    }
1329
1330    Ok(imports)
1331}
1332
1333/// Classify a Java import as internal, external, or stdlib
1334fn classify_java_import(import_path: &str) -> ImportType {
1335    classify_java_import_impl(import_path, None)
1336}
1337
1338/// Parse pom.xml or build.gradle to find Java package name
1339/// Similar to find_go_module_name() for Go projects
1340pub fn find_java_package_name(root: &std::path::Path) -> Option<String> {
1341    // Try Maven first (pom.xml)
1342    if let Some(package) = find_maven_package(root) {
1343        return Some(package);
1344    }
1345
1346    // Try Gradle second (build.gradle or build.gradle.kts)
1347    if let Some(package) = find_gradle_package(root) {
1348        return Some(package);
1349    }
1350
1351    // Fallback: scan package declarations in .java files
1352    find_package_from_sources(root)
1353}
1354
1355/// Parse pom.xml to extract <groupId>
1356fn 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    // Simple XML parsing for <groupId>
1365    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
1377/// Parse build.gradle or build.gradle.kts to extract group
1378fn find_gradle_package(root: &std::path::Path) -> Option<String> {
1379    // Try build.gradle (Groovy)
1380    if let Some(package) = find_gradle_package_in_file(&root.join("build.gradle")) {
1381        return Some(package);
1382    }
1383
1384    // Try build.gradle.kts (Kotlin)
1385    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        // Groovy: group = 'org.neo4j'
1399        // Kotlin: group = "org.neo4j"
1400        if trimmed.starts_with("group") {
1401            if let Some(equals_idx) = trimmed.find('=') {
1402                let value = &trimmed[equals_idx + 1..].trim();
1403                // Remove quotes
1404                let value = value.trim_matches(|c| c == '\'' || c == '"');
1405                return Some(value.to_string());
1406            }
1407        }
1408    }
1409
1410    None
1411}
1412
1413/// Scan .java files to find common package prefix
1414fn 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    // Walk the directory tree looking for .java files
1420    fn walk_dir(dir: &std::path::Path, package_counts: &mut HashMap<String, usize>, depth: usize) {
1421        // Limit depth to avoid excessive scanning
1422        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                    // Extract package declaration
1439                    for line in content.lines().take(20) {
1440                        // Check first 20 lines
1441                        let trimmed = line.trim();
1442                        if trimmed.starts_with("package ") && trimmed.ends_with(';') {
1443                            let package = &trimmed[8..trimmed.len() - 1].trim();
1444
1445                            // Extract base package (first 2 components: org.neo4j)
1446                            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    // Find the most common package prefix
1462    package_counts
1463        .into_iter()
1464        .max_by_key(|(_, count)| *count)
1465        .map(|(package, _)| package)
1466}
1467
1468/// Reclassify a Java import using the project package prefix
1469/// Similar to reclassify_go_import() for Go
1470pub 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    // First check if this is an internal import (matches project package)
1476    if let Some(prefix) = package_prefix {
1477        if import_path.starts_with(prefix) {
1478            return ImportType::Internal;
1479        }
1480    }
1481
1482    // Java standard library packages (common ones)
1483    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    // Check if it starts with any stdlib package
1508    for stdlib_pkg in STDLIB_PACKAGES {
1509        if import_path.starts_with(stdlib_pkg) {
1510            return ImportType::Stdlib;
1511        }
1512    }
1513
1514    // Everything else is external
1515    ImportType::External
1516}
1517
1518// ============================================================================
1519// Monorepo Support - Java/Kotlin Dependency Resolution
1520// ============================================================================
1521
1522/// Represents a Java/Kotlin project in a monorepo
1523#[derive(Debug, Clone)]
1524pub struct JavaProject {
1525    /// Package name (groupId from Maven or group from Gradle)
1526    pub package_name: String,
1527    /// Relative path to project root (where pom.xml or build.gradle is)
1528    pub project_root: String,
1529    /// Absolute path to project root
1530    pub abs_project_root: String,
1531}
1532
1533/// Find all Maven/Gradle projects in the repository recursively
1534/// Similar to find_all_go_mods() for Go
1535pub 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            // Match pom.xml (Maven) or build.gradle/build.gradle.kts (Gradle)
1551            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
1566/// Parse all Maven/Gradle projects and return JavaProject structs
1567/// Similar to parse_all_go_modules() for Go
1568pub 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        // Get the directory containing the config file (project root)
1578        if let Some(project_dir) = config_path.parent() {
1579            // Parse the config file to get package name
1580            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
1613/// Extract package name from pom.xml or build.gradle
1614fn 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            // Extract <groupId> from Maven pom.xml
1620            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            // Extract group from Gradle build file
1633            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
1650/// Resolve a Java import to a file path
1651///
1652/// Java imports look like: `com.example.myapp.UserService`
1653/// Files are located at: `src/main/java/com/example/myapp/UserService.java`
1654/// or: `src/com/example/myapp/UserService.java`
1655pub fn resolve_java_import_to_path(
1656    import_path: &str,
1657    projects: &[JavaProject],
1658    _current_file_path: Option<&str>,
1659) -> Option<String> {
1660    // Java imports are absolute package paths, not relative
1661    // Find which project this import belongs to
1662    for project in projects {
1663        if import_path.starts_with(&project.package_name) {
1664            // Convert package to file path: com.example.UserService → com/example/UserService.java
1665            let file_path = import_path.replace('.', "/");
1666
1667            // Try common Java source directory structures
1668            let candidates = vec![
1669                // Maven/Gradle standard structure
1670                format!("{}/src/main/java/{}.java", project.project_root, file_path),
1671                // Simpler structure
1672                format!("{}/src/{}.java", project.project_root, file_path),
1673                // Root-level src
1674                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
1687/// Resolve a Kotlin import to a file path
1688///
1689/// Kotlin uses the same package system as Java, but with .kt extension
1690pub fn resolve_kotlin_import_to_path(
1691    import_path: &str,
1692    projects: &[JavaProject],
1693    _current_file_path: Option<&str>,
1694) -> Option<String> {
1695    // Kotlin imports are identical to Java imports
1696    for project in projects {
1697        if import_path.starts_with(&project.package_name) {
1698            let file_path = import_path.replace('.', "/");
1699
1700            // Try common Kotlin source directory structures
1701            let candidates = vec![
1702                // Maven/Gradle standard structure
1703                format!("{}/src/main/kotlin/{}.kt", project.project_root, file_path),
1704                // Java source dir (Kotlin can be in java dir)
1705                format!("{}/src/main/java/{}.kt", project.project_root, file_path),
1706                // Simpler structure
1707                format!("{}/src/{}.kt", project.project_root, file_path),
1708                // Root-level src
1709                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        // Should try Maven standard structure first
1741        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        // Different package
1768        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        // Should resolve to service1
1789        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        // Should resolve to service2
1795        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}