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 anyhow::{Context, Result};
15use streaming_iterator::StreamingIterator;
16use tree_sitter::{Parser, Query, QueryCursor};
17use crate::models::{Language, SearchResult, Span, SymbolKind};
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(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    // Add file path to all symbols
48    for symbol in &mut symbols {
49        symbol.path = path.to_string();
50        symbol.lang = Language::Java;
51    }
52
53    Ok(symbols)
54}
55
56/// Extract class declarations
57fn 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
73/// Extract interface declarations
74fn 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
90/// Extract enum declarations
91fn 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
107/// Extract annotations: BOTH definitions and uses
108/// Definitions: @interface Test { ... }
109/// Uses: @Test public void testMethod()
110fn 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    // Part 1: Extract annotation type DEFINITIONS (@interface)
118    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    // Part 2: Extract annotation USES (@Test, @Override, etc.)
129    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
145/// Extract method declarations from classes
146fn 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                    // Find the parent method_declaration node
194                    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
228/// Extract field declarations from classes
229fn 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                    // Find the parent field_declaration node
279                    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
313/// Extract constructor declarations
314fn 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                    // Find the parent constructor_declaration node
349                    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
383/// Extract method declarations from interfaces
384fn 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                    // Find the parent method_declaration node
419                    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
453/// Extract local variable declarations from method bodies
454fn 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
471/// Generic symbol extraction helper
472fn 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        // Find the name capture and the full node
486        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                // Assume any other capture is the full node
495                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
518/// Convert a Tree-sitter node to a Span
519fn 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,  // Convert 0-indexed to 1-indexed
525        start.column,
526        end.row + 1,
527        end.column,
528    )
529}
530
531/// Extract a preview (7 lines) around the symbol
532fn extract_preview(source: &str, span: &Span) -> String {
533    let lines: Vec<&str> = source.lines().collect();
534
535    // Extract 7 lines: the start line and 6 following lines
536    let start_idx = (span.start_line - 1) as usize; // Convert back to 0-indexed
537    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        // Check scope
590        for method in method_symbols {
591            // Removed: scope field no longer exists: assert_eq!(method.scope.as_ref().unwrap(), "class Calculator");
592        }
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        // Should find: interface, class, enum, fields, constructor, methods
833        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        // Should find both field (globalCount) and local variables (localVar, anotherLocal)
900        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        // Check scopes: field should have scope, local vars should not
906        let global_count = var_symbols.iter().find(|s| s.symbol.as_deref() == Some("globalCount")).unwrap();
907        // Removed: scope field no longer exists: assert_eq!(global_count.scope.as_ref().unwrap(), "class Calculator");
908
909        let local_var = var_symbols.iter().find(|s| s.symbol.as_deref() == Some("localVar")).unwrap();
910        // Removed: scope field no longer exists: assert_eq!(local_var.scope, None);
911    }
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        // Should find annotation definitions
936        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        // Should find annotation uses
973        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        // Should find Test twice (2 uses)
981        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        // Check stdlib classification
1014        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        // Check external classification
1019        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
1025// ============================================================================
1026// Dependency Extraction
1027// ============================================================================
1028
1029use crate::models::ImportType;
1030use crate::parsers::{DependencyExtractor, ImportInfo};
1031
1032/// Java dependency extractor
1033pub 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        // Extract import statements
1053        imports.extend(extract_java_imports(source, &root_node)?);
1054
1055        Ok(imports)
1056    }
1057}
1058
1059/// Extract Java import statements
1060fn 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, // Java imports are package-level
1095                });
1096            }
1097        }
1098    }
1099
1100    Ok(imports)
1101}
1102
1103/// Classify a Java import as internal, external, or stdlib
1104fn classify_java_import(import_path: &str) -> ImportType {
1105    classify_java_import_impl(import_path, None)
1106}
1107
1108/// Parse pom.xml or build.gradle to find Java package name
1109/// Similar to find_go_module_name() for Go projects
1110pub fn find_java_package_name(root: &std::path::Path) -> Option<String> {
1111    // Try Maven first (pom.xml)
1112    if let Some(package) = find_maven_package(root) {
1113        return Some(package);
1114    }
1115
1116    // Try Gradle second (build.gradle or build.gradle.kts)
1117    if let Some(package) = find_gradle_package(root) {
1118        return Some(package);
1119    }
1120
1121    // Fallback: scan package declarations in .java files
1122    find_package_from_sources(root)
1123}
1124
1125/// Parse pom.xml to extract <groupId>
1126fn 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    // Simple XML parsing for <groupId>
1135    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
1147/// Parse build.gradle or build.gradle.kts to extract group
1148fn find_gradle_package(root: &std::path::Path) -> Option<String> {
1149    // Try build.gradle (Groovy)
1150    if let Some(package) = find_gradle_package_in_file(&root.join("build.gradle")) {
1151        return Some(package);
1152    }
1153
1154    // Try build.gradle.kts (Kotlin)
1155    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        // Groovy: group = 'org.neo4j'
1169        // Kotlin: group = "org.neo4j"
1170        if trimmed.starts_with("group") {
1171            if let Some(equals_idx) = trimmed.find('=') {
1172                let value = &trimmed[equals_idx + 1..].trim();
1173                // Remove quotes
1174                let value = value.trim_matches(|c| c == '\'' || c == '"');
1175                return Some(value.to_string());
1176            }
1177        }
1178    }
1179
1180    None
1181}
1182
1183/// Scan .java files to find common package prefix
1184fn 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    // Walk the directory tree looking for .java files
1190    fn walk_dir(dir: &std::path::Path, package_counts: &mut HashMap<String, usize>, depth: usize) {
1191        // Limit depth to avoid excessive scanning
1192        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                    // Extract package declaration
1209                    for line in content.lines().take(20) { // Check first 20 lines
1210                        let trimmed = line.trim();
1211                        if trimmed.starts_with("package ") && trimmed.ends_with(';') {
1212                            let package = &trimmed[8..trimmed.len() - 1].trim();
1213
1214                            // Extract base package (first 2 components: org.neo4j)
1215                            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    // Find the most common package prefix
1231    package_counts.into_iter()
1232        .max_by_key(|(_, count)| *count)
1233        .map(|(package, _)| package)
1234}
1235
1236/// Reclassify a Java import using the project package prefix
1237/// Similar to reclassify_go_import() for Go
1238pub 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    // First check if this is an internal import (matches project package)
1244    if let Some(prefix) = package_prefix {
1245        if import_path.starts_with(prefix) {
1246            return ImportType::Internal;
1247        }
1248    }
1249
1250    // Java standard library packages (common ones)
1251    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    // Check if it starts with any stdlib package
1260    for stdlib_pkg in STDLIB_PACKAGES {
1261        if import_path.starts_with(stdlib_pkg) {
1262            return ImportType::Stdlib;
1263        }
1264    }
1265
1266    // Everything else is external
1267    ImportType::External
1268}
1269
1270// ============================================================================
1271// Monorepo Support - Java/Kotlin Dependency Resolution
1272// ============================================================================
1273
1274/// Represents a Java/Kotlin project in a monorepo
1275#[derive(Debug, Clone)]
1276pub struct JavaProject {
1277    /// Package name (groupId from Maven or group from Gradle)
1278    pub package_name: String,
1279    /// Relative path to project root (where pom.xml or build.gradle is)
1280    pub project_root: String,
1281    /// Absolute path to project root
1282    pub abs_project_root: String,
1283}
1284
1285/// Find all Maven/Gradle projects in the repository recursively
1286/// Similar to find_all_go_mods() for Go
1287pub 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            // Match pom.xml (Maven) or build.gradle/build.gradle.kts (Gradle)
1303            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
1316/// Parse all Maven/Gradle projects and return JavaProject structs
1317/// Similar to parse_all_go_modules() for Go
1318pub 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        // Get the directory containing the config file (project root)
1327        if let Some(project_dir) = config_path.parent() {
1328            // Parse the config file to get package name
1329            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
1353/// Extract package name from pom.xml or build.gradle
1354fn 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            // Extract <groupId> from Maven pom.xml
1360            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            // Extract group from Gradle build file
1373            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
1390/// Resolve a Java import to a file path
1391///
1392/// Java imports look like: `com.example.myapp.UserService`
1393/// Files are located at: `src/main/java/com/example/myapp/UserService.java`
1394/// or: `src/com/example/myapp/UserService.java`
1395pub fn resolve_java_import_to_path(
1396    import_path: &str,
1397    projects: &[JavaProject],
1398    _current_file_path: Option<&str>,
1399) -> Option<String> {
1400    // Java imports are absolute package paths, not relative
1401    // Find which project this import belongs to
1402    for project in projects {
1403        if import_path.starts_with(&project.package_name) {
1404            // Convert package to file path: com.example.UserService → com/example/UserService.java
1405            let file_path = import_path.replace('.', "/");
1406
1407            // Try common Java source directory structures
1408            let candidates = vec![
1409                // Maven/Gradle standard structure
1410                format!("{}/src/main/java/{}.java", project.project_root, file_path),
1411                // Simpler structure
1412                format!("{}/src/{}.java", project.project_root, file_path),
1413                // Root-level src
1414                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
1427/// Resolve a Kotlin import to a file path
1428///
1429/// Kotlin uses the same package system as Java, but with .kt extension
1430pub fn resolve_kotlin_import_to_path(
1431    import_path: &str,
1432    projects: &[JavaProject],
1433    _current_file_path: Option<&str>,
1434) -> Option<String> {
1435    // Kotlin imports are identical to Java imports
1436    for project in projects {
1437        if import_path.starts_with(&project.package_name) {
1438            let file_path = import_path.replace('.', "/");
1439
1440            // Try common Kotlin source directory structures
1441            let candidates = vec![
1442                // Maven/Gradle standard structure
1443                format!("{}/src/main/kotlin/{}.kt", project.project_root, file_path),
1444                // Java source dir (Kotlin can be in java dir)
1445                format!("{}/src/main/java/{}.kt", project.project_root, file_path),
1446                // Simpler structure
1447                format!("{}/src/{}.kt", project.project_root, file_path),
1448                // Root-level src
1449                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        // Should try Maven standard structure first
1485        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        // Different package
1516        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        // Should resolve to service1
1541        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        // Should resolve to service2
1550        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}