mati_core/analysis/resolvers/
mod.rs1pub mod c;
23pub mod cpp;
24pub mod elixir;
25pub mod go;
26pub mod haskell;
27pub mod java;
28pub mod python;
29pub mod ruby;
30pub mod rust;
31pub mod scala;
32pub mod typescript;
33
34use std::collections::HashMap;
35use std::collections::HashSet;
36use std::path::PathBuf;
37
38use crate::analysis::parser::import::ImportKind;
39use crate::analysis::parser::ImportStatement;
40use crate::analysis::walker::Language;
41
42pub trait LanguageResolver: Send + Sync {
49 fn resolve(
53 &self,
54 import: &ImportStatement,
55 importing_file: &str,
56 file_index: &FileIndex,
57 ) -> Option<String>;
58
59 fn language(&self) -> Language;
61
62 fn name(&self) -> &'static str;
64}
65
66pub struct FileIndex {
74 files: HashSet<String>,
75 root: Option<PathBuf>,
76 crate_roots: Vec<String>,
80 workspace_members: HashMap<String, String>,
85 scala_source_roots: Vec<String>,
89 ruby_autoload_roots: Vec<String>,
93 ruby_lib_roots: Vec<String>,
96}
97
98impl FileIndex {
99 pub fn new(paths: impl IntoIterator<Item = String>) -> Self {
101 Self {
102 files: paths.into_iter().collect(),
103 root: None,
104 crate_roots: Vec::new(),
105 workspace_members: HashMap::new(),
106 scala_source_roots: Vec::new(),
107 ruby_autoload_roots: Vec::new(),
108 ruby_lib_roots: Vec::new(),
109 }
110 }
111
112 pub fn new_with_root(root: PathBuf, paths: impl IntoIterator<Item = String>) -> Self {
114 Self {
115 files: paths.into_iter().collect(),
116 root: Some(root),
117 crate_roots: Vec::new(),
118 workspace_members: HashMap::new(),
119 scala_source_roots: Vec::new(),
120 ruby_autoload_roots: Vec::new(),
121 ruby_lib_roots: Vec::new(),
122 }
123 }
124
125 pub fn set_crate_roots(&mut self, mut roots: Vec<String>) {
129 roots.sort_by_key(|b| std::cmp::Reverse(b.len()));
130 self.crate_roots = roots;
131 }
132
133 pub fn crate_root_for(&self, file_path: &str) -> Option<&str> {
137 self.crate_roots
138 .iter()
139 .find(|root| file_path.starts_with(root.as_str()))
140 .map(|s| s.as_str())
141 }
142
143 pub fn set_workspace_members(&mut self, members: HashMap<String, String>) {
145 self.workspace_members = members;
146 }
147
148 pub fn workspace_member_root(&self, crate_name: &str) -> Option<&str> {
152 self.workspace_members.get(crate_name).map(|s| s.as_str())
153 }
154
155 pub fn has_workspace_members(&self) -> bool {
157 !self.workspace_members.is_empty()
158 }
159
160 pub fn set_scala_source_roots(&mut self, roots: Vec<String>) {
162 self.scala_source_roots = roots;
163 }
164
165 pub fn scala_source_roots(&self) -> &[String] {
167 &self.scala_source_roots
168 }
169
170 pub fn set_ruby_autoload_roots(&mut self, roots: Vec<String>) {
172 self.ruby_autoload_roots = roots;
173 }
174
175 pub fn ruby_autoload_roots(&self) -> &[String] {
177 &self.ruby_autoload_roots
178 }
179
180 pub fn set_ruby_lib_roots(&mut self, roots: Vec<String>) {
182 self.ruby_lib_roots = roots;
183 }
184
185 pub fn ruby_lib_roots(&self) -> &[String] {
187 &self.ruby_lib_roots
188 }
189
190 pub fn read_file(&self, rel_path: &str) -> Option<String> {
194 let root = self.root.as_ref()?;
195 std::fs::read_to_string(root.join(rel_path)).ok()
196 }
197
198 pub fn contains(&self, path: &str) -> bool {
200 self.files.contains(path)
201 }
202
203 pub fn files_with_prefix(&self, prefix: &str) -> Vec<&String> {
207 let mut matches: Vec<&String> = self
208 .files
209 .iter()
210 .filter(|f| f.starts_with(prefix))
211 .collect();
212 matches.sort_unstable();
213 matches
214 }
215
216 pub fn files_with_stem(&self, stem: &str) -> Vec<&String> {
221 let mut matches: Vec<&String> = self
222 .files
223 .iter()
224 .filter(|f| {
225 std::path::Path::new(f.as_str())
226 .file_stem()
227 .and_then(|s| s.to_str())
228 == Some(stem)
229 })
230 .collect();
231 matches.sort_unstable();
232 matches
233 }
234}
235
236pub struct ResolverRegistry {
243 resolvers: HashMap<Language, Box<dyn LanguageResolver>>,
244}
245
246impl ResolverRegistry {
247 pub fn new() -> Self {
249 let mut resolvers: HashMap<Language, Box<dyn LanguageResolver>> = HashMap::new();
250 resolvers.insert(Language::Rust, Box::new(rust::RustResolver));
251 resolvers.insert(Language::Python, Box::new(python::PythonResolver));
252 resolvers.insert(
253 Language::TypeScript,
254 Box::new(typescript::TypeScriptResolver),
255 );
256 resolvers.insert(
257 Language::JavaScript,
258 Box::new(typescript::TypeScriptResolver),
259 );
260 resolvers.insert(Language::Go, Box::new(go::GoResolver));
261 resolvers.insert(Language::Java, Box::new(java::JavaResolver));
262 resolvers.insert(Language::C, Box::new(c::CResolver));
263 resolvers.insert(Language::Cpp, Box::new(cpp::CppResolver));
264 resolvers.insert(Language::Ruby, Box::new(ruby::RubyResolver));
265 resolvers.insert(Language::Scala, Box::new(scala::ScalaResolver));
266 resolvers.insert(Language::Elixir, Box::new(elixir::ElixirResolver));
267 resolvers.insert(Language::Haskell, Box::new(haskell::HaskellResolver));
268 Self { resolvers }
269 }
270
271 pub fn resolve(
278 &self,
279 import: &ImportStatement,
280 importing_file: &str,
281 language: Language,
282 file_index: &FileIndex,
283 ) -> Option<String> {
284 if import.kind == ImportKind::External {
286 return None;
287 }
288 self.resolvers
289 .get(&language)?
290 .resolve(import, importing_file, file_index)
291 }
292}
293
294impl Default for ResolverRegistry {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300pub(crate) fn camel_to_snake(s: &str) -> String {
315 let mut result = String::with_capacity(s.len() + 4);
316 let chars: Vec<char> = s.chars().collect();
317
318 for (i, &c) in chars.iter().enumerate() {
319 if c.is_uppercase() {
320 if i > 0 {
321 let prev = chars[i - 1];
322 let next_is_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());
323 if prev.is_lowercase()
327 || prev.is_ascii_digit()
328 || (prev.is_uppercase() && next_is_lower)
329 {
330 result.push('_');
331 }
332 }
333 result.push(c.to_ascii_lowercase());
334 } else {
335 result.push(c);
336 }
337 }
338
339 result
340}
341
342#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn file_index_contains() {
350 let idx = FileIndex::new(vec!["src/main.rs".into(), "src/lib.rs".into()]);
351 assert!(idx.contains("src/main.rs"));
352 assert!(!idx.contains("src/foo.rs"));
353 }
354
355 #[test]
356 fn file_index_prefix() {
357 let idx = FileIndex::new(vec![
358 "src/store/db.rs".into(),
359 "src/store/mod.rs".into(),
360 "src/main.rs".into(),
361 ]);
362 let results = idx.files_with_prefix("src/store/");
363 assert_eq!(results.len(), 2);
364 }
365
366 #[test]
367 fn file_index_stem() {
368 let idx = FileIndex::new(vec![
369 "src/utils.rs".into(),
370 "lib/utils.py".into(),
371 "src/main.rs".into(),
372 ]);
373 let results = idx.files_with_stem("utils");
374 assert_eq!(results.len(), 2);
375 }
376
377 #[test]
378 fn registry_skips_external() {
379 let registry = ResolverRegistry::new();
380 let idx = FileIndex::new(vec!["src/main.rs".into()]);
381 let import = ImportStatement::new("react", ImportKind::External, 1);
382 assert_eq!(
383 registry.resolve(&import, "src/app.ts", Language::TypeScript, &idx),
384 None
385 );
386 }
387
388 #[test]
389 fn registry_returns_none_for_unregistered_language() {
390 let registry = ResolverRegistry::new();
391 let idx = FileIndex::new(vec!["main.go".into()]);
392 let import = ImportStatement::new("fmt", ImportKind::Normal, 1);
393 assert_eq!(
394 registry.resolve(&import, "main.go", Language::Go, &idx),
395 None
396 );
397 }
398
399 #[test]
402 fn camel_to_snake_simple_word() {
403 assert_eq!(camel_to_snake("User"), "user");
404 assert_eq!(camel_to_snake("Router"), "router");
405 }
406
407 #[test]
408 fn camel_to_snake_multi_word() {
409 assert_eq!(camel_to_snake("UserNotification"), "user_notification");
410 assert_eq!(camel_to_snake("MyApp"), "my_app");
411 assert_eq!(
412 camel_to_snake("ApplicationController"),
413 "application_controller"
414 );
415 }
416
417 #[test]
418 fn camel_to_snake_acronyms() {
419 assert_eq!(camel_to_snake("HTTPServer"), "http_server");
420 assert_eq!(camel_to_snake("JSONParser"), "json_parser");
421 assert_eq!(camel_to_snake("XMLParser"), "xml_parser");
422 assert_eq!(camel_to_snake("API"), "api");
423 assert_eq!(camel_to_snake("HTTP"), "http");
424 }
425
426 #[test]
427 fn camel_to_snake_trailing_acronym() {
428 assert_eq!(camel_to_snake("FooID"), "foo_id");
429 assert_eq!(camel_to_snake("UserAPI"), "user_api");
430 }
431
432 #[test]
433 fn camel_to_snake_digit_boundaries() {
434 assert_eq!(camel_to_snake("V2Parser"), "v2_parser");
435 assert_eq!(camel_to_snake("XMLParserV2"), "xml_parser_v2");
436 }
437
438 #[test]
439 fn camel_to_snake_already_lowercase() {
440 assert_eq!(camel_to_snake("already_snake"), "already_snake");
441 }
442
443 #[test]
444 fn camel_to_snake_empty() {
445 assert_eq!(camel_to_snake(""), "");
446 }
447
448 #[test]
449 fn camel_to_snake_single_char() {
450 assert_eq!(camel_to_snake("A"), "a");
451 assert_eq!(camel_to_snake("x"), "x");
452 }
453}