Skip to main content

mati_core/analysis/resolvers/
java.rs

1//! Java import resolver.
2//!
3//! Converts dotted qualified names (`com.example.MyClass`) to filesystem paths
4//! (`com/example/MyClass.java`). Checks Maven/Gradle source roots
5//! (`src/main/java/`, `src/test/java/`). Handles inner class and static member
6//! imports by progressively stripping trailing segments until a `.java` file
7//! matches.
8
9use super::{FileIndex, LanguageResolver};
10use crate::analysis::parser::ImportStatement;
11use crate::analysis::walker::Language;
12
13pub struct JavaResolver;
14
15impl LanguageResolver for JavaResolver {
16    fn resolve(
17        &self,
18        import: &ImportStatement,
19        _importing_file: &str,
20        file_index: &FileIndex,
21    ) -> Option<String> {
22        resolve_java(&import.path, file_index)
23    }
24
25    fn language(&self) -> Language {
26        Language::Java
27    }
28
29    fn name(&self) -> &'static str {
30        "java"
31    }
32}
33
34/// Maven/Gradle source roots to search, in priority order.
35const JAVA_SOURCE_ROOTS: &[&str] = &["", "src/main/java/", "src/test/java/"];
36
37fn resolve_java(import_path: &str, file_index: &FileIndex) -> Option<String> {
38    // Skip standard library and known external deps
39    if is_java_stdlib(import_path) {
40        return None;
41    }
42
43    // Strip wildcard suffix for directory-level imports
44    let clean = import_path.trim_end_matches(".*");
45
46    // Convert dots to slashes: com.example.Foo → com/example/Foo
47    let rel = clean.replace('.', "/");
48
49    // Try direct match against each source root
50    for root in JAVA_SOURCE_ROOTS {
51        let candidate = format!("{root}{rel}.java");
52        if file_index.contains(&candidate) {
53            return Some(candidate);
54        }
55    }
56
57    // Inner class / static member stripping: progressively drop the last
58    // segment and retry. E.g. org/jsoup/nodes/Document/OutputSettings
59    // → try org/jsoup/nodes/Document.java (inner class OutputSettings).
60    // Also handles static imports like Parser.NamespaceHtml → Parser.java.
61    let mut segments: Vec<&str> = rel.split('/').collect();
62    while segments.len() > 2 {
63        segments.pop();
64        let parent = segments.join("/");
65        for root in JAVA_SOURCE_ROOTS {
66            let candidate = format!("{root}{parent}.java");
67            if file_index.contains(&candidate) {
68                return Some(candidate);
69            }
70        }
71    }
72
73    None
74}
75
76fn is_java_stdlib(path: &str) -> bool {
77    // JDK / Android platform
78    path.starts_with("java.")
79        || path.starts_with("javax.")
80        || path.starts_with("jakarta.")
81        || path.starts_with("sun.")
82        || path.starts_with("com.sun.")
83        || path.starts_with("jdk.")
84        || path.starts_with("android.")
85        // XML / W3C (shipped with JDK but separate namespace)
86        || path.starts_with("org.w3c.")
87        || path.starts_with("org.xml.")
88        // Common test frameworks
89        || path.starts_with("org.junit.")
90        || path.starts_with("org.hamcrest.")
91        || path.starts_with("org.mockito.")
92        || path.starts_with("org.assertj.")
93        // Common third-party libs (never project-local)
94        || path.starts_with("org.jspecify.")
95        || path.starts_with("org.slf4j.")
96        || path.starts_with("org.apache.")
97        || path.starts_with("org.springframework.")
98        || path.starts_with("org.eclipse.")
99        || path.starts_with("com.google.")
100}
101
102/// Convert a dotted qualified name to a slash-separated path.
103/// Reusable by Scala resolver.
104pub(super) fn dotted_to_path(dotted: &str) -> String {
105    dotted.replace('.', "/")
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::analysis::parser::import::ImportKind;
112
113    fn idx(paths: &[&str]) -> FileIndex {
114        FileIndex::new(paths.iter().map(|s| s.to_string()))
115    }
116
117    fn import(path: &str) -> ImportStatement {
118        ImportStatement::new(path, ImportKind::Normal, 1)
119    }
120
121    #[test]
122    fn stdlib_skipped() {
123        let file_index = idx(&["Main.java"]);
124        assert_eq!(
125            JavaResolver.resolve(&import("java.util.List"), "Main.java", &file_index),
126            None
127        );
128        assert_eq!(
129            JavaResolver.resolve(&import("javax.swing.JFrame"), "Main.java", &file_index),
130            None
131        );
132    }
133
134    #[test]
135    fn local_class_resolves() {
136        let file_index = idx(&["com/example/Foo.java", "Main.java"]);
137        let result = JavaResolver.resolve(&import("com.example.Foo"), "Main.java", &file_index);
138        assert_eq!(result, Some("com/example/Foo.java".into()));
139    }
140
141    #[test]
142    fn maven_layout_resolves() {
143        let file_index = idx(&["src/main/java/com/example/Foo.java", "Main.java"]);
144        let result = JavaResolver.resolve(&import("com.example.Foo"), "Main.java", &file_index);
145        assert_eq!(result, Some("src/main/java/com/example/Foo.java".into()));
146    }
147
148    #[test]
149    fn wildcard_import_resolves_to_directory_file() {
150        let file_index = idx(&["com/example/Utils.java", "Main.java"]);
151        // Wildcard stripped → tries com/example.java which doesn't exist
152        let result = JavaResolver.resolve(&import("com.example.*"), "Main.java", &file_index);
153        assert_eq!(result, None); // No directory-level resolution yet
154    }
155
156    #[test]
157    fn nonexistent_returns_none() {
158        let file_index = idx(&["Main.java"]);
159        assert_eq!(
160            JavaResolver.resolve(&import("com.example.Missing"), "Main.java", &file_index),
161            None
162        );
163    }
164
165    // ── Inner class / static member stripping ─────────────────────────────
166
167    #[test]
168    fn inner_class_import_resolves_to_outer_file() {
169        let file_index = idx(&["src/main/java/org/jsoup/nodes/Document.java"]);
170        let result = JavaResolver.resolve(
171            &import("org.jsoup.nodes.Document.OutputSettings"),
172            "Foo.java",
173            &file_index,
174        );
175        assert_eq!(
176            result,
177            Some("src/main/java/org/jsoup/nodes/Document.java".into())
178        );
179    }
180
181    #[test]
182    fn static_member_import_resolves_to_class_file() {
183        let file_index = idx(&["src/main/java/org/jsoup/parser/Parser.java"]);
184        let result = JavaResolver.resolve(
185            &import("org.jsoup.parser.Parser.NamespaceHtml"),
186            "Foo.java",
187            &file_index,
188        );
189        assert_eq!(
190            result,
191            Some("src/main/java/org/jsoup/parser/Parser.java".into())
192        );
193    }
194
195    #[test]
196    fn deeply_nested_inner_class_resolves() {
197        // Connection.Method.HEAD — three levels: file is Connection.java
198        let file_index = idx(&["src/main/java/org/jsoup/Connection.java"]);
199        let result = JavaResolver.resolve(
200            &import("org.jsoup.Connection.Method.HEAD"),
201            "Foo.java",
202            &file_index,
203        );
204        assert_eq!(
205            result,
206            Some("src/main/java/org/jsoup/Connection.java".into())
207        );
208    }
209
210    // ── Test source root ──────────────────────────────────────────────────
211
212    #[test]
213    fn test_source_root_resolves() {
214        let file_index = idx(&["src/test/java/org/jsoup/TextUtil.java"]);
215        let result = JavaResolver.resolve(
216            &import("org.jsoup.TextUtil"),
217            "src/test/java/org/jsoup/FooTest.java",
218            &file_index,
219        );
220        assert_eq!(result, Some("src/test/java/org/jsoup/TextUtil.java".into()));
221    }
222
223    // ── External dep classification ───────────────────────────────────────
224
225    #[test]
226    fn org_junit_is_external() {
227        let file_index = idx(&["Main.java"]);
228        assert_eq!(
229            JavaResolver.resolve(
230                &import("org.junit.jupiter.api.Test"),
231                "Main.java",
232                &file_index
233            ),
234            None
235        );
236    }
237
238    #[test]
239    fn org_jspecify_is_external() {
240        let file_index = idx(&["Main.java"]);
241        assert_eq!(
242            JavaResolver.resolve(
243                &import("org.jspecify.annotations.Nullable"),
244                "Main.java",
245                &file_index
246            ),
247            None
248        );
249    }
250}