use super::{camel_to_snake, FileIndex, LanguageResolver};
use crate::analysis::parser::ImportStatement;
use crate::analysis::walker::Language;
pub struct ElixirResolver;
impl LanguageResolver for ElixirResolver {
fn resolve(
&self,
import: &ImportStatement,
_importing_file: &str,
file_index: &FileIndex,
) -> Option<String> {
resolve_elixir(&import.path, file_index)
}
fn language(&self) -> Language {
Language::Elixir
}
fn name(&self) -> &'static str {
"elixir"
}
}
fn resolve_elixir(module_path: &str, file_index: &FileIndex) -> Option<String> {
if is_elixir_stdlib(module_path) {
return None;
}
let segments: Vec<String> = module_path.split('.').map(camel_to_snake).collect();
let rel = segments.join("/");
let lib_ex = format!("lib/{rel}.ex");
if file_index.contains(&lib_ex) {
return Some(lib_ex);
}
let lib_exs = format!("lib/{rel}.exs");
if file_index.contains(&lib_exs) {
return Some(lib_exs);
}
let direct_ex = format!("{rel}.ex");
if file_index.contains(&direct_ex) {
return Some(direct_ex);
}
None
}
fn is_elixir_stdlib(module: &str) -> bool {
let first = module.split('.').next().unwrap_or(module);
matches!(
first,
"Absinthe"
| "Access"
| "Agent"
| "Application"
| "Atom"
| "Base"
| "Bitwise"
| "Broadway"
| "Code"
| "Collectable"
| "Date"
| "DateTime"
| "DynamicSupervisor"
| "Ecto"
| "Enum"
| "Enumerable"
| "ETS"
| "Exception"
| "ExUnit"
| "File"
| "Finch"
| "Float"
| "Flow"
| "GenServer"
| "GenStage"
| "HTTPoison"
| "IEx"
| "IO"
| "Inspect"
| "Integer"
| "Jason"
| "Kernel"
| "Keyword"
| "List"
| "LiveBook"
| "LiveView"
| "Logger"
| "Macro"
| "Map"
| "MapSet"
| "Mix"
| "Module"
| "NaiveDateTime"
| "Node"
| "Oban"
| "Path"
| "Phoenix"
| "Plug"
| "Poison"
| "Port"
| "Process"
| "Protocol"
| "Range"
| "Regex"
| "Registry"
| "Stream"
| "String"
| "Supervisor"
| "System"
| "Task"
| "Time"
| "Tuple"
| "URI"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::parser::import::ImportKind;
fn idx(paths: &[&str]) -> FileIndex {
FileIndex::new(paths.iter().map(|s| s.to_string()))
}
fn import(path: &str) -> ImportStatement {
ImportStatement::new(path, ImportKind::Normal, 1)
}
#[test]
fn camel_to_snake_simple() {
assert_eq!(camel_to_snake("MyModule"), "my_module");
}
#[test]
fn camel_to_snake_acronym_start() {
assert_eq!(camel_to_snake("HTTPServer"), "http_server");
}
#[test]
fn camel_to_snake_acronym_only() {
assert_eq!(camel_to_snake("HTTP"), "http");
}
#[test]
fn camel_to_snake_mixed() {
assert_eq!(camel_to_snake("XMLParserV2"), "xml_parser_v2");
}
#[test]
fn camel_to_snake_single_word() {
assert_eq!(camel_to_snake("User"), "user");
}
#[test]
fn camel_to_snake_my_app() {
assert_eq!(camel_to_snake("MyApp"), "my_app");
}
#[test]
fn camel_to_snake_router() {
assert_eq!(camel_to_snake("Router"), "router");
}
#[test]
fn stdlib_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Enum"), "lib/my_app.ex", &file_index),
None
);
assert_eq!(
ElixirResolver.resolve(&import("GenServer"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn phoenix_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Phoenix.Router"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn ecto_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Ecto.Schema"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn plug_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Plug.Conn"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn absinthe_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Absinthe.Schema"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn broadway_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Broadway"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn oban_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Oban.Worker"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn ex_unit_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("ExUnit.Case"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn mix_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Mix.Task"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn jason_skipped() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Jason"), "lib/my_app.ex", &file_index),
None
);
}
#[test]
fn local_module_resolves() {
let file_index = idx(&["lib/my_app/router.ex"]);
let result = ElixirResolver.resolve(&import("MyApp.Router"), "lib/my_app.ex", &file_index);
assert_eq!(result, Some("lib/my_app/router.ex".into()));
}
#[test]
fn single_segment_local_resolves() {
let file_index = idx(&["lib/my_app.ex"]);
let result = ElixirResolver.resolve(&import("MyApp"), "lib/other.ex", &file_index);
assert_eq!(result, Some("lib/my_app.ex".into()));
}
#[test]
fn acronym_module_resolves() {
let file_index = idx(&["lib/my_app/http_server.ex"]);
let result =
ElixirResolver.resolve(&import("MyApp.HTTPServer"), "lib/my_app.ex", &file_index);
assert_eq!(result, Some("lib/my_app/http_server.ex".into()));
}
#[test]
fn xml_parser_module_resolves() {
let file_index = idx(&["lib/my_app/xml_parser.ex"]);
let result =
ElixirResolver.resolve(&import("MyApp.XMLParser"), "lib/my_app.ex", &file_index);
assert_eq!(result, Some("lib/my_app/xml_parser.ex".into()));
}
#[test]
fn nonexistent_returns_none() {
let file_index = idx(&["lib/my_app.ex"]);
assert_eq!(
ElixirResolver.resolve(&import("Missing.Module"), "lib/my_app.ex", &file_index),
None
);
}
}