mati_core/analysis/resolvers/
scala.rs1use super::java::dotted_to_path;
26use super::{FileIndex, LanguageResolver};
27use crate::analysis::parser::ImportStatement;
28use crate::analysis::walker::Language;
29
30pub struct ScalaResolver;
31
32impl LanguageResolver for ScalaResolver {
33 fn resolve(
34 &self,
35 import: &ImportStatement,
36 _importing_file: &str,
37 file_index: &FileIndex,
38 ) -> Option<String> {
39 resolve_scala(&import.path, file_index)
40 }
41
42 fn language(&self) -> Language {
43 Language::Scala
44 }
45
46 fn name(&self) -> &'static str {
47 "scala"
48 }
49}
50
51fn resolve_scala(import_path: &str, file_index: &FileIndex) -> Option<String> {
52 if is_scala_stdlib(import_path) {
53 return None;
54 }
55
56 let clean = import_path
58 .split('{')
59 .next()
60 .unwrap_or(import_path)
61 .trim_end_matches("._")
62 .trim_end_matches('.');
63
64 let rel = dotted_to_path(clean);
65
66 let direct = format!("{rel}.scala");
68 if file_index.contains(&direct) {
69 return Some(direct);
70 }
71
72 let sbt = format!("src/main/scala/{rel}.scala");
74 if file_index.contains(&sbt) {
75 return Some(sbt);
76 }
77
78 for root in file_index.scala_source_roots() {
81 let candidate = format!("{root}{rel}.scala");
82 if file_index.contains(&candidate) {
83 return Some(candidate);
84 }
85 }
86
87 None
88}
89
90fn is_scala_stdlib(path: &str) -> bool {
91 path.starts_with("scala.")
92 || path.starts_with("java.")
93 || path.starts_with("javax.")
94 || path.starts_with("akka.")
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::analysis::parser::import::ImportKind;
101
102 fn idx(paths: &[&str]) -> FileIndex {
103 FileIndex::new(paths.iter().map(|s| s.to_string()))
104 }
105
106 fn import(path: &str) -> ImportStatement {
107 ImportStatement::new(path, ImportKind::Normal, 1)
108 }
109
110 #[test]
111 fn stdlib_skipped() {
112 let file_index = idx(&["Main.scala"]);
113 assert_eq!(
114 ScalaResolver.resolve(
115 &import("scala.collection.mutable"),
116 "Main.scala",
117 &file_index
118 ),
119 None
120 );
121 }
122
123 #[test]
124 fn local_class_resolves() {
125 let file_index = idx(&["com/example/Utils.scala", "Main.scala"]);
126 let result = ScalaResolver.resolve(&import("com.example.Utils"), "Main.scala", &file_index);
127 assert_eq!(result, Some("com/example/Utils.scala".into()));
128 }
129
130 #[test]
131 fn sbt_layout_resolves() {
132 let file_index = idx(&["src/main/scala/com/example/Utils.scala", "Main.scala"]);
133 let result = ScalaResolver.resolve(&import("com.example.Utils"), "Main.scala", &file_index);
134 assert_eq!(
135 result,
136 Some("src/main/scala/com/example/Utils.scala".into())
137 );
138 }
139
140 #[test]
141 fn wildcard_stripped() {
142 let file_index = idx(&["com/example.scala", "Main.scala"]);
143 let result = ScalaResolver.resolve(&import("com.example._"), "Main.scala", &file_index);
144 assert_eq!(result, Some("com/example.scala".into()));
145 }
146
147 #[test]
148 fn nonexistent_returns_none() {
149 let file_index = idx(&["Main.scala"]);
150 assert_eq!(
151 ScalaResolver.resolve(&import("com.example.Missing"), "Main.scala", &file_index),
152 None
153 );
154 }
155
156 #[test]
159 fn simple_src_main_scala_resolves() {
160 let file_index = idx(&[
163 "src/main/scala/foo/Bar.scala",
164 "src/main/scala/foo/Main.scala",
165 ]);
166 let result = ScalaResolver.resolve(&import("foo.Bar"), "Main.scala", &file_index);
167 assert_eq!(result, Some("src/main/scala/foo/Bar.scala".into()));
168 }
169
170 #[test]
171 fn multi_project_subproject_resolves() {
172 let mut file_index = idx(&[
173 "myproject/src/main/scala/foo/Bar.scala",
174 "myproject/src/main/scala/foo/Main.scala",
175 ]);
176 file_index.set_scala_source_roots(vec!["myproject/src/main/scala/".to_string()]);
177 let result = ScalaResolver.resolve(&import("foo.Bar"), "Main.scala", &file_index);
178 assert_eq!(
179 result,
180 Some("myproject/src/main/scala/foo/Bar.scala".into())
181 );
182 }
183
184 #[test]
185 fn shared_directory_resolves() {
186 let mut file_index = idx(&[
188 "zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala",
189 "zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala",
190 ]);
191 file_index.set_scala_source_roots(vec!["zio-json/shared/src/main/scala/".to_string()]);
192 let result = ScalaResolver.resolve(
193 &import("zio.json.JsonDecoder"),
194 "zio-json/shared/src/main/scala/zio/json/JsonEncoder.scala",
195 &file_index,
196 );
197 assert_eq!(
198 result,
199 Some("zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala".into())
200 );
201 }
202
203 #[test]
204 fn scala_version_specific_root_resolves() {
205 let mut file_index = idx(&[
206 "myproject/src/main/scala-2.13/foo/Compat.scala",
207 "myproject/src/main/scala/foo/Main.scala",
208 ]);
209 file_index.set_scala_source_roots(vec![
210 "myproject/src/main/scala-2.13/".to_string(),
211 "myproject/src/main/scala/".to_string(),
212 ]);
213 let result = ScalaResolver.resolve(&import("foo.Compat"), "Main.scala", &file_index);
214 assert_eq!(
215 result,
216 Some("myproject/src/main/scala-2.13/foo/Compat.scala".into())
217 );
218 }
219
220 #[test]
221 fn test_source_root_resolves() {
222 let mut file_index = idx(&[
223 "myproject/src/test/scala/foo/BarSpec.scala",
224 "myproject/src/main/scala/foo/Bar.scala",
225 ]);
226 file_index.set_scala_source_roots(vec![
227 "myproject/src/main/scala/".to_string(),
228 "myproject/src/test/scala/".to_string(),
229 ]);
230 let result = ScalaResolver.resolve(&import("foo.BarSpec"), "Main.scala", &file_index);
231 assert_eq!(
232 result,
233 Some("myproject/src/test/scala/foo/BarSpec.scala".into())
234 );
235 }
236
237 #[test]
238 fn cross_source_root_imports_resolve() {
239 let mut file_index = idx(&[
241 "core/src/main/scala/com/acme/Model.scala",
242 "web/src/main/scala/com/acme/Controller.scala",
243 ]);
244 file_index.set_scala_source_roots(vec![
245 "core/src/main/scala/".to_string(),
246 "web/src/main/scala/".to_string(),
247 ]);
248 let result = ScalaResolver.resolve(
250 &import("com.acme.Model"),
251 "web/src/main/scala/com/acme/Controller.scala",
252 &file_index,
253 );
254 assert_eq!(
255 result,
256 Some("core/src/main/scala/com/acme/Model.scala".into())
257 );
258 }
259}