mati_core/analysis/resolvers/
java.rs1use 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
34const JAVA_SOURCE_ROOTS: &[&str] = &["", "src/main/java/", "src/test/java/"];
36
37fn resolve_java(import_path: &str, file_index: &FileIndex) -> Option<String> {
38 if is_java_stdlib(import_path) {
40 return None;
41 }
42
43 let clean = import_path.trim_end_matches(".*");
45
46 let rel = clean.replace('.', "/");
48
49 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 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 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 || path.starts_with("org.w3c.")
87 || path.starts_with("org.xml.")
88 || path.starts_with("org.junit.")
90 || path.starts_with("org.hamcrest.")
91 || path.starts_with("org.mockito.")
92 || path.starts_with("org.assertj.")
93 || 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
102pub(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 let result = JavaResolver.resolve(&import("com.example.*"), "Main.java", &file_index);
153 assert_eq!(result, None); }
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 #[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 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]
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 #[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}