use std::path::{Path, PathBuf};
use super::build::{Lang, SuffixIndex};
#[derive(Debug, Clone)]
pub struct ResolveCtx<'a> {
pub from_file: &'a Path,
pub lang: Lang,
pub alias_prefix: Option<&'a str>,
pub path_aliases: &'a [(String, String)],
pub php_psr4: &'a [(String, String)],
}
impl<'a> ResolveCtx<'a> {
#[allow(dead_code)]
pub fn new(from_file: &'a Path, lang: Lang) -> Self {
Self {
from_file,
lang,
alias_prefix: None,
path_aliases: &[],
php_psr4: &[],
}
}
}
pub fn resolve(spec: &str, ctx: &ResolveCtx<'_>, idx: &SuffixIndex) -> Option<PathBuf> {
let spec = spec.trim();
if spec.is_empty() {
return None;
}
if ctx.lang == Lang::Rust {
let resolve_with_fallback =
|key: String| -> Option<PathBuf> {
if let Some(p) = pick_closest(idx.lookup(&key), ctx.from_file) {
return Some(p);
}
let mut parts: Vec<&str> = key.split('/').collect();
while parts.len() > 1 {
parts.pop();
let trimmed = parts.join("/");
if let Some(p) = pick_closest(idx.lookup(&trimmed), ctx.from_file) {
return Some(p);
}
}
None
};
if let Some(rest) = spec.strip_prefix("crate::") {
return resolve_with_fallback(rest.replace("::", "/"));
}
if let Some(rest) = spec.strip_prefix("self::") {
let key = rest.replace("::", "/");
return resolve_relative(&key, ctx, idx, 0);
}
let mut s = spec;
let mut up = 0usize;
while let Some(rest) = s.strip_prefix("super::") {
s = rest;
up += 1;
}
if up > 0 {
let key = s.replace("::", "/");
return resolve_relative(&key, ctx, idx, up);
}
return resolve_with_fallback(spec.replace("::", "/"));
}
if spec.starts_with("./") || spec.starts_with("../") {
return resolve_relative_path(spec, ctx, idx);
}
if (matches!(
ctx.lang,
Lang::TypeScript | Lang::Tsx | Lang::JavaScript
)) && (spec.starts_with('.'))
{
return resolve_relative_path(spec, ctx, idx);
}
if matches!(
ctx.lang,
Lang::TypeScript | Lang::Tsx | Lang::JavaScript
) {
for (prefix, replacement) in ctx.path_aliases {
if let Some(rest) = spec.strip_prefix(prefix.as_str()) {
let combined = format!("{}{}", replacement, rest);
let key = combined.trim_start_matches("./").to_string();
if let Some(p) = pick_closest(idx.lookup(&key), ctx.from_file) {
return Some(p);
}
}
}
}
if ctx.lang == Lang::Go {
if let Some(prefix) = ctx.alias_prefix {
let trimmed = spec.trim_matches('"');
let prefix_slash = format!("{}/", prefix);
if trimmed == prefix {
return None; }
if let Some(rest) = trimmed.strip_prefix(&prefix_slash) {
if let Some(found) = find_dir_file(idx, rest) {
return Some(found);
}
return None;
}
return None;
}
return None;
}
if matches!(
ctx.lang,
Lang::Java | Lang::Kotlin | Lang::CSharp | Lang::Scala
) {
let key = spec.replace('.', "/");
if let Some(p) = pick_closest(idx.lookup(&key), ctx.from_file) {
return Some(p);
}
if let Some((parent, _)) = key.rsplit_once('/') {
if let Some(p) = pick_closest(idx.lookup(parent), ctx.from_file) {
return Some(p);
}
}
return None;
}
if ctx.lang == Lang::Php {
for (prefix, dir) in ctx.php_psr4 {
if let Some(rest) = spec.strip_prefix(prefix.as_str()) {
let key = if dir.is_empty() {
rest.to_string()
} else {
format!("{}/{}", dir.trim_end_matches('/'), rest)
};
if let Some(p) = pick_closest(idx.lookup(&key), ctx.from_file) {
return Some(p);
}
let abs = idx.root.join(format!("{}.php", key));
if abs.is_file() {
return Some(abs);
}
}
}
if let Some(p) = pick_closest(idx.lookup(spec), ctx.from_file) {
return Some(p);
}
if let Some(last) = spec.rsplit('/').next() {
if last != spec {
if let Some(p) = pick_closest(idx.lookup(last), ctx.from_file) {
return Some(p);
}
}
}
return None;
}
if ctx.lang == Lang::Cpp {
return None;
}
if ctx.lang == Lang::Ruby {
return None;
}
let key = spec.replace('.', "/");
pick_closest(idx.lookup(&key), ctx.from_file)
}
fn resolve_relative_path(
spec: &str,
ctx: &ResolveCtx<'_>,
idx: &SuffixIndex,
) -> Option<PathBuf> {
let parent = ctx.from_file.parent()?;
let mut cur = parent.to_path_buf();
let mut remaining = spec;
while let Some(rest) = remaining.strip_prefix("../") {
cur = cur.parent()?.to_path_buf();
remaining = rest;
}
while let Some(rest) = remaining.strip_prefix("./") {
remaining = rest;
}
let target = cur.join(remaining);
if target.is_file() {
return Some(target);
}
if matches!(
ctx.lang,
Lang::TypeScript | Lang::Tsx | Lang::JavaScript
) {
let ext_order: &[&str] = &[
".ts", ".tsx", ".mts", ".cts", ".d.ts", ".js", ".jsx", ".mjs", ".cjs", ".json",
];
for e in ext_order {
let p = with_ext(&target, e);
if p.is_file() {
return Some(p);
}
}
for e in ext_order {
let p = target.join(format!("index{}", e));
if p.is_file() {
return Some(p);
}
}
}
if ctx.lang == Lang::Python {
for e in [".py", ".pyi"] {
let p = with_ext(&target, e);
if p.is_file() {
return Some(p);
}
}
let init = target.join("__init__.py");
if init.is_file() {
return Some(init);
}
if let Some(parent) = target.parent() {
for e in [".py", ".pyi"] {
let p = with_ext(parent, e);
if p.is_file() {
return Some(p);
}
}
let init_parent = parent.join("__init__.py");
if init_parent.is_file() {
return Some(init_parent);
}
}
}
let rel = match target.strip_prefix(&idx.root) {
Ok(r) => r
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/"),
Err(_) => target.display().to_string(),
};
pick_closest(idx.lookup(&rel), ctx.from_file)
}
fn with_ext(p: &Path, ext: &str) -> PathBuf {
let mut s = p.as_os_str().to_string_lossy().into_owned();
s.push_str(ext);
PathBuf::from(s)
}
fn resolve_relative(
key: &str,
ctx: &ResolveCtx<'_>,
idx: &SuffixIndex,
ascend: usize,
) -> Option<PathBuf> {
let mut parent = ctx.from_file.parent()?.to_path_buf();
for _ in 0..ascend {
parent = parent.parent()?.to_path_buf();
}
let target = parent.join(key);
let candidates = [
with_ext(&target, ".rs"),
target.join("mod.rs"),
target.clone(),
];
for c in candidates {
if c.is_file() {
return Some(c);
}
}
let rel = match target.strip_prefix(&idx.root) {
Ok(r) => r
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("/"),
Err(_) => return None,
};
pick_closest(idx.lookup(&rel), ctx.from_file)
}
fn find_dir_file(idx: &SuffixIndex, rel_dir: &str) -> Option<PathBuf> {
let dir_abs = idx.root.join(rel_dir);
let mut best: Option<PathBuf> = None;
for f in idx.by_file.keys() {
if f.starts_with(&dir_abs) && f.parent() == Some(dir_abs.as_path()) {
match &best {
None => best = Some(f.clone()),
Some(prev) => {
if f < prev {
best = Some(f.clone());
}
}
}
}
}
best
}
fn pick_closest(candidates: Option<&[PathBuf]>, from_file: &Path) -> Option<PathBuf> {
let cands = candidates?;
if cands.is_empty() {
return None;
}
if cands.len() == 1 {
return Some(cands[0].clone());
}
let from_segs: Vec<String> = from_file
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
let mut best: Option<(usize, &PathBuf)> = None;
for c in cands {
let segs: Vec<String> = c
.components()
.map(|c| c.as_os_str().to_string_lossy().into_owned())
.collect();
let common = from_segs
.iter()
.zip(segs.iter())
.take_while(|(a, b)| a == b)
.count();
match best {
None => best = Some((common, c)),
Some((prev_common, prev_c)) => {
if common > prev_common || (common == prev_common && c < prev_c) {
best = Some((common, c));
}
}
}
}
best.map(|(_, p)| p.clone())
}