mati_core/analysis/resolvers/
elixir.rs1use super::{camel_to_snake, FileIndex, LanguageResolver};
8use crate::analysis::parser::ImportStatement;
9use crate::analysis::walker::Language;
10
11pub struct ElixirResolver;
12
13impl LanguageResolver for ElixirResolver {
14 fn resolve(
15 &self,
16 import: &ImportStatement,
17 _importing_file: &str,
18 file_index: &FileIndex,
19 ) -> Option<String> {
20 resolve_elixir(&import.path, file_index)
21 }
22
23 fn language(&self) -> Language {
24 Language::Elixir
25 }
26
27 fn name(&self) -> &'static str {
28 "elixir"
29 }
30}
31
32fn resolve_elixir(module_path: &str, file_index: &FileIndex) -> Option<String> {
33 if is_elixir_stdlib(module_path) {
34 return None;
35 }
36
37 let segments: Vec<String> = module_path.split('.').map(camel_to_snake).collect();
39 let rel = segments.join("/");
40
41 let lib_ex = format!("lib/{rel}.ex");
43 if file_index.contains(&lib_ex) {
44 return Some(lib_ex);
45 }
46
47 let lib_exs = format!("lib/{rel}.exs");
49 if file_index.contains(&lib_exs) {
50 return Some(lib_exs);
51 }
52
53 let direct_ex = format!("{rel}.ex");
55 if file_index.contains(&direct_ex) {
56 return Some(direct_ex);
57 }
58
59 None
60}
61
62fn is_elixir_stdlib(module: &str) -> bool {
63 let first = module.split('.').next().unwrap_or(module);
64 matches!(
65 first,
66 "Absinthe"
67 | "Access"
68 | "Agent"
69 | "Application"
70 | "Atom"
71 | "Base"
72 | "Bitwise"
73 | "Broadway"
74 | "Code"
75 | "Collectable"
76 | "Date"
77 | "DateTime"
78 | "DynamicSupervisor"
79 | "Ecto"
80 | "Enum"
81 | "Enumerable"
82 | "ETS"
83 | "Exception"
84 | "ExUnit"
85 | "File"
86 | "Finch"
87 | "Float"
88 | "Flow"
89 | "GenServer"
90 | "GenStage"
91 | "HTTPoison"
92 | "IEx"
93 | "IO"
94 | "Inspect"
95 | "Integer"
96 | "Jason"
97 | "Kernel"
98 | "Keyword"
99 | "List"
100 | "LiveBook"
101 | "LiveView"
102 | "Logger"
103 | "Macro"
104 | "Map"
105 | "MapSet"
106 | "Mix"
107 | "Module"
108 | "NaiveDateTime"
109 | "Node"
110 | "Oban"
111 | "Path"
112 | "Phoenix"
113 | "Plug"
114 | "Poison"
115 | "Port"
116 | "Process"
117 | "Protocol"
118 | "Range"
119 | "Regex"
120 | "Registry"
121 | "Stream"
122 | "String"
123 | "Supervisor"
124 | "System"
125 | "Task"
126 | "Time"
127 | "Tuple"
128 | "URI"
129 )
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::analysis::parser::import::ImportKind;
136
137 fn idx(paths: &[&str]) -> FileIndex {
138 FileIndex::new(paths.iter().map(|s| s.to_string()))
139 }
140
141 fn import(path: &str) -> ImportStatement {
142 ImportStatement::new(path, ImportKind::Normal, 1)
143 }
144
145 #[test]
148 fn camel_to_snake_simple() {
149 assert_eq!(camel_to_snake("MyModule"), "my_module");
150 }
151
152 #[test]
153 fn camel_to_snake_acronym_start() {
154 assert_eq!(camel_to_snake("HTTPServer"), "http_server");
155 }
156
157 #[test]
158 fn camel_to_snake_acronym_only() {
159 assert_eq!(camel_to_snake("HTTP"), "http");
160 }
161
162 #[test]
163 fn camel_to_snake_mixed() {
164 assert_eq!(camel_to_snake("XMLParserV2"), "xml_parser_v2");
165 }
166
167 #[test]
168 fn camel_to_snake_single_word() {
169 assert_eq!(camel_to_snake("User"), "user");
170 }
171
172 #[test]
173 fn camel_to_snake_my_app() {
174 assert_eq!(camel_to_snake("MyApp"), "my_app");
175 }
176
177 #[test]
178 fn camel_to_snake_router() {
179 assert_eq!(camel_to_snake("Router"), "router");
180 }
181
182 #[test]
185 fn stdlib_skipped() {
186 let file_index = idx(&["lib/my_app.ex"]);
187 assert_eq!(
188 ElixirResolver.resolve(&import("Enum"), "lib/my_app.ex", &file_index),
189 None
190 );
191 assert_eq!(
192 ElixirResolver.resolve(&import("GenServer"), "lib/my_app.ex", &file_index),
193 None
194 );
195 }
196
197 #[test]
198 fn phoenix_skipped() {
199 let file_index = idx(&["lib/my_app.ex"]);
200 assert_eq!(
201 ElixirResolver.resolve(&import("Phoenix.Router"), "lib/my_app.ex", &file_index),
202 None
203 );
204 }
205
206 #[test]
207 fn ecto_skipped() {
208 let file_index = idx(&["lib/my_app.ex"]);
209 assert_eq!(
210 ElixirResolver.resolve(&import("Ecto.Schema"), "lib/my_app.ex", &file_index),
211 None
212 );
213 }
214
215 #[test]
216 fn plug_skipped() {
217 let file_index = idx(&["lib/my_app.ex"]);
218 assert_eq!(
219 ElixirResolver.resolve(&import("Plug.Conn"), "lib/my_app.ex", &file_index),
220 None
221 );
222 }
223
224 #[test]
225 fn absinthe_skipped() {
226 let file_index = idx(&["lib/my_app.ex"]);
227 assert_eq!(
228 ElixirResolver.resolve(&import("Absinthe.Schema"), "lib/my_app.ex", &file_index),
229 None
230 );
231 }
232
233 #[test]
234 fn broadway_skipped() {
235 let file_index = idx(&["lib/my_app.ex"]);
236 assert_eq!(
237 ElixirResolver.resolve(&import("Broadway"), "lib/my_app.ex", &file_index),
238 None
239 );
240 }
241
242 #[test]
243 fn oban_skipped() {
244 let file_index = idx(&["lib/my_app.ex"]);
245 assert_eq!(
246 ElixirResolver.resolve(&import("Oban.Worker"), "lib/my_app.ex", &file_index),
247 None
248 );
249 }
250
251 #[test]
252 fn ex_unit_skipped() {
253 let file_index = idx(&["lib/my_app.ex"]);
254 assert_eq!(
255 ElixirResolver.resolve(&import("ExUnit.Case"), "lib/my_app.ex", &file_index),
256 None
257 );
258 }
259
260 #[test]
261 fn mix_skipped() {
262 let file_index = idx(&["lib/my_app.ex"]);
263 assert_eq!(
264 ElixirResolver.resolve(&import("Mix.Task"), "lib/my_app.ex", &file_index),
265 None
266 );
267 }
268
269 #[test]
270 fn jason_skipped() {
271 let file_index = idx(&["lib/my_app.ex"]);
272 assert_eq!(
273 ElixirResolver.resolve(&import("Jason"), "lib/my_app.ex", &file_index),
274 None
275 );
276 }
277
278 #[test]
281 fn local_module_resolves() {
282 let file_index = idx(&["lib/my_app/router.ex"]);
283 let result = ElixirResolver.resolve(&import("MyApp.Router"), "lib/my_app.ex", &file_index);
284 assert_eq!(result, Some("lib/my_app/router.ex".into()));
285 }
286
287 #[test]
288 fn single_segment_local_resolves() {
289 let file_index = idx(&["lib/my_app.ex"]);
290 let result = ElixirResolver.resolve(&import("MyApp"), "lib/other.ex", &file_index);
291 assert_eq!(result, Some("lib/my_app.ex".into()));
292 }
293
294 #[test]
295 fn acronym_module_resolves() {
296 let file_index = idx(&["lib/my_app/http_server.ex"]);
297 let result =
298 ElixirResolver.resolve(&import("MyApp.HTTPServer"), "lib/my_app.ex", &file_index);
299 assert_eq!(result, Some("lib/my_app/http_server.ex".into()));
300 }
301
302 #[test]
303 fn xml_parser_module_resolves() {
304 let file_index = idx(&["lib/my_app/xml_parser.ex"]);
305 let result =
306 ElixirResolver.resolve(&import("MyApp.XMLParser"), "lib/my_app.ex", &file_index);
307 assert_eq!(result, Some("lib/my_app/xml_parser.ex".into()));
308 }
309
310 #[test]
311 fn nonexistent_returns_none() {
312 let file_index = idx(&["lib/my_app.ex"]);
313 assert_eq!(
314 ElixirResolver.resolve(&import("Missing.Module"), "lib/my_app.ex", &file_index),
315 None
316 );
317 }
318}