use std::path::Path;
use super::{FileIndex, LanguageResolver};
use crate::analysis::parser::ImportStatement;
use crate::analysis::walker::Language;
pub struct TypeScriptResolver;
impl LanguageResolver for TypeScriptResolver {
fn resolve(
&self,
import: &ImportStatement,
importing_file: &str,
file_index: &FileIndex,
) -> Option<String> {
resolve_ts_js(&import.path, importing_file, file_index)
}
fn language(&self) -> Language {
Language::TypeScript
}
fn name(&self) -> &'static str {
"typescript"
}
}
fn resolve_ts_js(
import_path: &str,
importing_file: &str,
file_index: &FileIndex,
) -> Option<String> {
let clean = import_path.trim_matches(|c| c == '\'' || c == '"');
let parent = Path::new(importing_file)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let resolved = if parent.is_empty() {
clean.strip_prefix("./").unwrap_or(clean).to_string()
} else {
let stripped = clean.strip_prefix("./").unwrap_or(clean);
normalize_path(&format!("{parent}/{stripped}"))
};
if file_index.contains(&resolved) {
return Some(resolved);
}
for ext in &[".ts", ".tsx", ".js", ".jsx"] {
let with_ext = format!("{resolved}{ext}");
if file_index.contains(&with_ext) {
return Some(with_ext);
}
}
for ext in &[".ts", ".tsx", ".js", ".jsx"] {
let index = format!("{resolved}/index{ext}");
if file_index.contains(&index) {
return Some(index);
}
}
None
}
fn normalize_path(path: &str) -> String {
let mut parts: Vec<&str> = Vec::new();
for segment in path.split('/') {
match segment {
".." => {
parts.pop();
}
"." | "" => {}
s => parts.push(s),
}
}
parts.join("/")
}
#[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::Relative, 1)
}
#[test]
fn relative_import_resolves() {
let file_index = idx(&["src/app.ts", "src/utils.ts"]);
let resolver = TypeScriptResolver;
let result = resolver.resolve(&import("./utils"), "src/app.ts", &file_index);
assert_eq!(result, Some("src/utils.ts".into()));
}
#[test]
fn parent_dir_resolves() {
let file_index = idx(&["src/components/button.tsx", "src/utils.ts"]);
let resolver = TypeScriptResolver;
let result = resolver.resolve(
&import("../utils"),
"src/components/button.tsx",
&file_index,
);
assert_eq!(result, Some("src/utils.ts".into()));
}
#[test]
fn index_file_resolves() {
let file_index = idx(&["src/app.ts", "src/components/index.ts"]);
let resolver = TypeScriptResolver;
let result = resolver.resolve(&import("./components"), "src/app.ts", &file_index);
assert_eq!(result, Some("src/components/index.ts".into()));
}
#[test]
fn exact_extension_resolves() {
let file_index = idx(&["lib/index.js", "lib/helpers.js"]);
let resolver = TypeScriptResolver;
let result = resolver.resolve(&import("./helpers"), "lib/index.js", &file_index);
assert_eq!(result, Some("lib/helpers.js".into()));
}
#[test]
fn unresolvable_returns_none() {
let file_index = idx(&["src/app.ts"]);
let resolver = TypeScriptResolver;
let result = resolver.resolve(&import("./nonexistent"), "src/app.ts", &file_index);
assert_eq!(result, None);
}
#[test]
fn normalize_path_resolves_parent_refs() {
assert_eq!(normalize_path("src/components/../utils"), "src/utils");
assert_eq!(normalize_path("a/b/c/../../d"), "a/d");
assert_eq!(normalize_path("./foo/bar"), "foo/bar");
}
}