use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::snapshot::FileEntry;
pub type RawImports = HashMap<PathBuf, Vec<String>>;
pub fn resolve_imports(
raw_imports: &RawImports,
files: &[FileEntry],
) -> HashMap<PathBuf, Vec<PathBuf>> {
let known: HashSet<&PathBuf> = files.iter().map(|f| &f.path).collect();
raw_imports
.iter()
.filter_map(|(source_path, imports)| {
let resolved: Vec<PathBuf> = imports
.iter()
.filter_map(|raw| resolve_single_import(raw, source_path, &known))
.collect();
if resolved.is_empty() {
None
} else {
Some((source_path.clone(), resolved))
}
})
.collect()
}
fn resolve_single_import(raw: &str, source: &Path, known: &HashSet<&PathBuf>) -> Option<PathBuf> {
let ext = source.extension().and_then(|e| e.to_str()).unwrap_or("");
let candidates = match ext {
"rs" => resolve_rust_import(raw),
"js" | "jsx" | "mjs" | "cjs" => resolve_js_import(raw, source),
"ts" | "tsx" => resolve_ts_import(raw, source),
"py" => resolve_python_import(raw),
"go" => resolve_go_import(raw, source),
"java" => resolve_java_import(raw),
"cs" => resolve_csharp_import(raw),
_ => Vec::new(),
};
candidates
.into_iter()
.map(|c| normalize_path(&c))
.find(|c| known.contains(c))
}
fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
path.components().fold(PathBuf::new(), |mut acc, comp| {
match comp {
Component::CurDir => {}
Component::ParentDir => {
acc.pop();
}
other => acc.push(other),
}
acc
})
}
fn resolve_rust_import(raw: &str) -> Vec<PathBuf> {
let path_part = raw
.strip_prefix("crate::")
.or_else(|| raw.strip_prefix("self::"))
.unwrap_or(raw);
let path_part = path_part
.split("::{")
.next()
.unwrap_or(path_part)
.trim_end_matches("::*");
let segments = path_part.replace("::", "/");
vec![
PathBuf::from(format!("src/{}.rs", segments)),
PathBuf::from(format!("src/{}/mod.rs", segments)),
]
}
fn resolve_js_import(raw: &str, source: &Path) -> Vec<PathBuf> {
if !raw.starts_with('.') {
return Vec::new(); }
let base = source.parent().unwrap_or_else(|| Path::new(""));
let resolved = base.join(raw);
vec![
resolved.with_extension("js"),
resolved.with_extension("jsx"),
resolved.with_extension("mjs"),
resolved.join("index.js"),
]
}
fn resolve_ts_import(raw: &str, source: &Path) -> Vec<PathBuf> {
if !raw.starts_with('.') {
return Vec::new();
}
let base = source.parent().unwrap_or_else(|| Path::new(""));
let resolved = base.join(raw);
vec![
resolved.with_extension("ts"),
resolved.with_extension("tsx"),
resolved.with_extension("js"),
resolved.join("index.ts"),
resolved.join("index.tsx"),
resolved.join("index.js"),
]
}
fn resolve_python_import(raw: &str) -> Vec<PathBuf> {
let segments = raw.replace('.', "/");
vec![
PathBuf::from(format!("{}.py", segments)),
PathBuf::from(format!("{}/__init__.py", segments)),
]
}
fn resolve_go_import(raw: &str, source: &Path) -> Vec<PathBuf> {
let last = raw.rsplit('/').next().unwrap_or(raw);
let base = source.parent().unwrap_or_else(|| Path::new(""));
vec![base.join(last).join("*.go")]
}
fn resolve_java_import(raw: &str) -> Vec<PathBuf> {
let segments = raw.replace('.', "/");
vec![
PathBuf::from(format!("{}.java", segments)),
PathBuf::from(format!("src/main/java/{}.java", segments)),
]
}
fn resolve_csharp_import(raw: &str) -> Vec<PathBuf> {
let segments = raw.replace('.', "/");
vec![PathBuf::from(format!("{}.cs", segments))]
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(path: &str) -> FileEntry {
FileEntry {
path: PathBuf::from(path),
size_bytes: 100,
is_binary: false,
depth: 1,
blob_oid: String::new(),
}
}
fn raw(source: &str, imports: Vec<&str>) -> RawImports {
let mut m = RawImports::new();
m.insert(
PathBuf::from(source),
imports.into_iter().map(String::from).collect(),
);
m
}
#[test]
fn ts_relative_import_resolves_to_normalized_path() {
let files = vec![
entry("dashboard/src/App.tsx"),
entry("dashboard/src/pages/Landing.tsx"),
];
let graph = resolve_imports(
&raw("dashboard/src/App.tsx", vec!["./pages/Landing"]),
&files,
);
let targets = &graph[&PathBuf::from("dashboard/src/App.tsx")];
assert_eq!(
targets[0].to_string_lossy(),
"dashboard/src/pages/Landing.tsx",
"resolved path must not contain a ./ segment"
);
}
#[test]
fn ts_parent_import_resolves_across_directories() {
let files = vec![entry("src/a/b.ts"), entry("src/shared/util.ts")];
let graph = resolve_imports(&raw("src/a/b.ts", vec!["../shared/util"]), &files);
let targets = graph
.get(&PathBuf::from("src/a/b.ts"))
.expect("parent-directory import must resolve");
assert_eq!(targets[0].to_string_lossy(), "src/shared/util.ts");
}
#[test]
fn js_relative_import_resolves_to_normalized_path() {
let files = vec![entry("web/main.js"), entry("web/lib/api.js")];
let graph = resolve_imports(&raw("web/main.js", vec!["./lib/api"]), &files);
let targets = &graph[&PathBuf::from("web/main.js")];
assert_eq!(targets[0].to_string_lossy(), "web/lib/api.js");
}
}