pub mod c;
pub mod cpp;
pub mod elixir;
pub mod go;
pub mod haskell;
pub mod java;
pub mod python;
pub mod ruby;
pub mod rust;
pub mod scala;
pub mod typescript;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::PathBuf;
use crate::analysis::parser::import::ImportKind;
use crate::analysis::parser::ImportStatement;
use crate::analysis::walker::Language;
pub trait LanguageResolver: Send + Sync {
fn resolve(
&self,
import: &ImportStatement,
importing_file: &str,
file_index: &FileIndex,
) -> Option<String>;
fn language(&self) -> Language;
fn name(&self) -> &'static str;
}
pub struct FileIndex {
files: HashSet<String>,
root: Option<PathBuf>,
crate_roots: Vec<String>,
workspace_members: HashMap<String, String>,
scala_source_roots: Vec<String>,
ruby_autoload_roots: Vec<String>,
ruby_lib_roots: Vec<String>,
}
impl FileIndex {
pub fn new(paths: impl IntoIterator<Item = String>) -> Self {
Self {
files: paths.into_iter().collect(),
root: None,
crate_roots: Vec::new(),
workspace_members: HashMap::new(),
scala_source_roots: Vec::new(),
ruby_autoload_roots: Vec::new(),
ruby_lib_roots: Vec::new(),
}
}
pub fn new_with_root(root: PathBuf, paths: impl IntoIterator<Item = String>) -> Self {
Self {
files: paths.into_iter().collect(),
root: Some(root),
crate_roots: Vec::new(),
workspace_members: HashMap::new(),
scala_source_roots: Vec::new(),
ruby_autoload_roots: Vec::new(),
ruby_lib_roots: Vec::new(),
}
}
pub fn set_crate_roots(&mut self, mut roots: Vec<String>) {
roots.sort_by_key(|b| std::cmp::Reverse(b.len()));
self.crate_roots = roots;
}
pub fn crate_root_for(&self, file_path: &str) -> Option<&str> {
self.crate_roots
.iter()
.find(|root| file_path.starts_with(root.as_str()))
.map(|s| s.as_str())
}
pub fn set_workspace_members(&mut self, members: HashMap<String, String>) {
self.workspace_members = members;
}
pub fn workspace_member_root(&self, crate_name: &str) -> Option<&str> {
self.workspace_members.get(crate_name).map(|s| s.as_str())
}
pub fn has_workspace_members(&self) -> bool {
!self.workspace_members.is_empty()
}
pub fn set_scala_source_roots(&mut self, roots: Vec<String>) {
self.scala_source_roots = roots;
}
pub fn scala_source_roots(&self) -> &[String] {
&self.scala_source_roots
}
pub fn set_ruby_autoload_roots(&mut self, roots: Vec<String>) {
self.ruby_autoload_roots = roots;
}
pub fn ruby_autoload_roots(&self) -> &[String] {
&self.ruby_autoload_roots
}
pub fn set_ruby_lib_roots(&mut self, roots: Vec<String>) {
self.ruby_lib_roots = roots;
}
pub fn ruby_lib_roots(&self) -> &[String] {
&self.ruby_lib_roots
}
pub fn read_file(&self, rel_path: &str) -> Option<String> {
let root = self.root.as_ref()?;
std::fs::read_to_string(root.join(rel_path)).ok()
}
pub fn contains(&self, path: &str) -> bool {
self.files.contains(path)
}
pub fn files_with_prefix(&self, prefix: &str) -> Vec<&String> {
self.files
.iter()
.filter(|f| f.starts_with(prefix))
.collect()
}
pub fn files_with_stem(&self, stem: &str) -> Vec<&String> {
self.files
.iter()
.filter(|f| {
std::path::Path::new(f.as_str())
.file_stem()
.and_then(|s| s.to_str())
== Some(stem)
})
.collect()
}
}
pub struct ResolverRegistry {
resolvers: HashMap<Language, Box<dyn LanguageResolver>>,
}
impl ResolverRegistry {
pub fn new() -> Self {
let mut resolvers: HashMap<Language, Box<dyn LanguageResolver>> = HashMap::new();
resolvers.insert(Language::Rust, Box::new(rust::RustResolver));
resolvers.insert(Language::Python, Box::new(python::PythonResolver));
resolvers.insert(
Language::TypeScript,
Box::new(typescript::TypeScriptResolver),
);
resolvers.insert(
Language::JavaScript,
Box::new(typescript::TypeScriptResolver),
);
resolvers.insert(Language::Go, Box::new(go::GoResolver));
resolvers.insert(Language::Java, Box::new(java::JavaResolver));
resolvers.insert(Language::C, Box::new(c::CResolver));
resolvers.insert(Language::Cpp, Box::new(cpp::CppResolver));
resolvers.insert(Language::Ruby, Box::new(ruby::RubyResolver));
resolvers.insert(Language::Scala, Box::new(scala::ScalaResolver));
resolvers.insert(Language::Elixir, Box::new(elixir::ElixirResolver));
resolvers.insert(Language::Haskell, Box::new(haskell::HaskellResolver));
Self { resolvers }
}
pub fn resolve(
&self,
import: &ImportStatement,
importing_file: &str,
language: Language,
file_index: &FileIndex,
) -> Option<String> {
if import.kind == ImportKind::External {
return None;
}
self.resolvers
.get(&language)?
.resolve(import, importing_file, file_index)
}
}
impl Default for ResolverRegistry {
fn default() -> Self {
Self::new()
}
}
pub(crate) fn camel_to_snake(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
let chars: Vec<char> = s.chars().collect();
for (i, &c) in chars.iter().enumerate() {
if c.is_uppercase() {
if i > 0 {
let prev = chars[i - 1];
let next_is_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());
if prev.is_lowercase()
|| prev.is_ascii_digit()
|| (prev.is_uppercase() && next_is_lower)
{
result.push('_');
}
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_index_contains() {
let idx = FileIndex::new(vec!["src/main.rs".into(), "src/lib.rs".into()]);
assert!(idx.contains("src/main.rs"));
assert!(!idx.contains("src/foo.rs"));
}
#[test]
fn file_index_prefix() {
let idx = FileIndex::new(vec![
"src/store/db.rs".into(),
"src/store/mod.rs".into(),
"src/main.rs".into(),
]);
let results = idx.files_with_prefix("src/store/");
assert_eq!(results.len(), 2);
}
#[test]
fn file_index_stem() {
let idx = FileIndex::new(vec![
"src/utils.rs".into(),
"lib/utils.py".into(),
"src/main.rs".into(),
]);
let results = idx.files_with_stem("utils");
assert_eq!(results.len(), 2);
}
#[test]
fn registry_skips_external() {
let registry = ResolverRegistry::new();
let idx = FileIndex::new(vec!["src/main.rs".into()]);
let import = ImportStatement::new("react", ImportKind::External, 1);
assert_eq!(
registry.resolve(&import, "src/app.ts", Language::TypeScript, &idx),
None
);
}
#[test]
fn registry_returns_none_for_unregistered_language() {
let registry = ResolverRegistry::new();
let idx = FileIndex::new(vec!["main.go".into()]);
let import = ImportStatement::new("fmt", ImportKind::Normal, 1);
assert_eq!(
registry.resolve(&import, "main.go", Language::Go, &idx),
None
);
}
#[test]
fn camel_to_snake_simple_word() {
assert_eq!(camel_to_snake("User"), "user");
assert_eq!(camel_to_snake("Router"), "router");
}
#[test]
fn camel_to_snake_multi_word() {
assert_eq!(camel_to_snake("UserNotification"), "user_notification");
assert_eq!(camel_to_snake("MyApp"), "my_app");
assert_eq!(
camel_to_snake("ApplicationController"),
"application_controller"
);
}
#[test]
fn camel_to_snake_acronyms() {
assert_eq!(camel_to_snake("HTTPServer"), "http_server");
assert_eq!(camel_to_snake("JSONParser"), "json_parser");
assert_eq!(camel_to_snake("XMLParser"), "xml_parser");
assert_eq!(camel_to_snake("API"), "api");
assert_eq!(camel_to_snake("HTTP"), "http");
}
#[test]
fn camel_to_snake_trailing_acronym() {
assert_eq!(camel_to_snake("FooID"), "foo_id");
assert_eq!(camel_to_snake("UserAPI"), "user_api");
}
#[test]
fn camel_to_snake_digit_boundaries() {
assert_eq!(camel_to_snake("V2Parser"), "v2_parser");
assert_eq!(camel_to_snake("XMLParserV2"), "xml_parser_v2");
}
#[test]
fn camel_to_snake_already_lowercase() {
assert_eq!(camel_to_snake("already_snake"), "already_snake");
}
#[test]
fn camel_to_snake_empty() {
assert_eq!(camel_to_snake(""), "");
}
#[test]
fn camel_to_snake_single_char() {
assert_eq!(camel_to_snake("A"), "a");
assert_eq!(camel_to_snake("x"), "x");
}
}