#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
use std::path::Path;
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
use crate::intel::model::ImportEdge;
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
use crate::path::RelPath;
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
pub(crate) enum SpecifierResolver {
#[cfg(feature = "code-intel-js")]
Js(Box<js::JsResolver>),
#[cfg(feature = "code-intel-stack")]
Python(python::PythonResolver),
#[cfg(feature = "code-intel-stack")]
Java(java::JavaResolver),
}
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
impl SpecifierResolver {
pub(crate) fn for_language(language: &str) -> Option<Self> {
match language {
#[cfg(feature = "code-intel-js")]
"javascript" | "typescript" | "tsx" => Some(Self::Js(Box::new(js::JsResolver::new()))),
#[cfg(feature = "code-intel-stack")]
"python" => Some(Self::Python(python::PythonResolver)),
#[cfg(feature = "code-intel-stack")]
"java" => Some(Self::Java(java::JavaResolver)),
_ => None,
}
}
pub(crate) fn supports(language: &str) -> bool {
match language {
#[cfg(feature = "code-intel-js")]
"javascript" | "typescript" | "tsx" => true,
#[cfg(feature = "code-intel-stack")]
"python" | "java" => true,
_ => false,
}
}
pub(crate) fn resolve(&self, root: &Path, importer_rel: &str, import: &ImportEdge) -> Option<RelPath> {
match self {
#[cfg(feature = "code-intel-js")]
Self::Js(resolver) => resolver.resolve(root, importer_rel, import),
#[cfg(feature = "code-intel-stack")]
Self::Python(resolver) => resolver.resolve(root, importer_rel, import),
#[cfg(feature = "code-intel-stack")]
Self::Java(resolver) => resolver.resolve(root, importer_rel, import),
}
}
pub(crate) fn default_export_name(&self) -> Option<&'static str> {
match self {
#[cfg(feature = "code-intel-js")]
Self::Js(_) => Some("default"),
#[cfg(feature = "code-intel-stack")]
Self::Python(_) | Self::Java(_) => None,
}
}
}
#[cfg(any(feature = "code-intel-js", feature = "code-intel-stack"))]
fn to_repo_relative(root: &Path, target_abs: &Path) -> Option<RelPath> {
let rel = match target_abs.strip_prefix(root) {
Ok(rel) => rel.to_path_buf(),
Err(_) => {
let canonical_root = std::fs::canonicalize(root).ok()?;
target_abs.strip_prefix(&canonical_root).ok()?.to_path_buf()
}
};
let normalized = rel.to_str()?.replace('\\', "/");
Some(RelPath::from(normalized.as_str()))
}
#[cfg(feature = "code-intel-js")]
pub(crate) mod js {
use std::path::Path;
use oxc_resolver::{ResolveOptions, Resolver, TsconfigDiscovery};
use super::to_repo_relative;
use crate::intel::model::ImportEdge;
use crate::path::RelPath;
pub(crate) const RESOLVE_EXTENSIONS: &[&str] = &[".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
pub(crate) struct JsResolver {
inner: Resolver,
}
impl JsResolver {
pub(crate) fn new() -> Self {
let ext_alias = |from: &str, to: &[&str]| (from.to_string(), to.iter().map(|s| (*s).to_string()).collect());
let inner = Resolver::new(ResolveOptions {
extensions: RESOLVE_EXTENSIONS.iter().map(|e| (*e).to_string()).collect(),
extension_alias: vec![
ext_alias(".js", &[".ts", ".tsx", ".js", ".jsx"]),
ext_alias(".mjs", &[".mts", ".mjs"]),
ext_alias(".cjs", &[".cts", ".cjs"]),
],
condition_names: vec![
"node".to_string(),
"import".to_string(),
"require".to_string(),
"default".to_string(),
],
symlinks: false,
tsconfig: Some(TsconfigDiscovery::Auto),
..ResolveOptions::default()
});
Self { inner }
}
pub(crate) fn resolve(&self, root: &Path, importer_rel: &str, import: &ImportEdge) -> Option<RelPath> {
let importer_abs = root.join(importer_rel);
let resolution = self.inner.resolve_file(&importer_abs, &import.specifier).ok()?;
to_repo_relative(root, &resolution.full_path())
}
}
}
#[cfg(feature = "code-intel-stack")]
pub(crate) mod python {
use std::path::{Path, PathBuf};
use super::to_repo_relative;
use crate::intel::model::ImportEdge;
use crate::path::RelPath;
const PACKAGE_ROOTS: &[&str] = &["src", ""];
pub(crate) struct PythonResolver;
impl PythonResolver {
pub(crate) fn resolve(&self, root: &Path, importer_rel: &str, import: &ImportEdge) -> Option<RelPath> {
let specifier = import.specifier.as_str();
let dots = specifier.chars().take_while(|&c| c == '.').count();
if dots > 0 {
self.resolve_relative(root, importer_rel, specifier, dots)
} else {
self.resolve_absolute(root, specifier)
}
}
fn resolve_absolute(&self, root: &Path, specifier: &str) -> Option<RelPath> {
let rel_parts = dotted_to_parts(specifier)?;
for pkg_root in PACKAGE_ROOTS {
let mut base = root.to_path_buf();
if !pkg_root.is_empty() {
base.push(pkg_root);
if !base.is_dir() {
continue;
}
}
if let Some(hit) = module_file(root, &base, &rel_parts) {
return Some(hit);
}
}
None
}
fn resolve_relative(&self, root: &Path, importer_rel: &str, specifier: &str, dots: usize) -> Option<RelPath> {
let importer_abs = root.join(importer_rel);
let mut base = importer_abs.parent()?.to_path_buf();
for _ in 1..dots {
base = base.parent()?.to_path_buf();
if !base.starts_with(root) {
return None;
}
}
let tail = &specifier[dots..];
let parts = if tail.is_empty() {
Vec::new()
} else {
dotted_to_parts(tail)?
};
module_file(root, &base, &parts)
}
}
fn dotted_to_parts(dotted: &str) -> Option<Vec<&str>> {
let parts: Vec<&str> = dotted.split('.').collect();
if parts.iter().any(|p| p.is_empty()) {
return None;
}
Some(parts)
}
fn module_file(root: &Path, base: &Path, parts: &[&str]) -> Option<RelPath> {
let mut dir = base.to_path_buf();
if parts.is_empty() {
let init = dir.join("__init__.py");
return init.is_file().then(|| to_repo_relative(root, &init)).flatten();
}
for part in &parts[..parts.len() - 1] {
dir.push(part);
}
let last = parts[parts.len() - 1];
let module: PathBuf = dir.join(format!("{last}.py"));
if module.is_file()
&& let Some(hit) = to_repo_relative(root, &module)
{
return Some(hit);
}
let package_init = dir.join(last).join("__init__.py");
if package_init.is_file() {
return to_repo_relative(root, &package_init);
}
None
}
}
#[cfg(feature = "code-intel-stack")]
pub(crate) mod java {
use std::path::Path;
use super::to_repo_relative;
use crate::intel::model::ImportEdge;
use crate::path::RelPath;
const SOURCE_ROOTS: &[&str] = &["src/main/java", "src/test/java", "src", ""];
pub(crate) struct JavaResolver;
impl JavaResolver {
pub(crate) fn resolve(&self, root: &Path, _importer_rel: &str, import: &ImportEdge) -> Option<RelPath> {
let fqn = import.specifier.as_str();
let parts: Vec<&str> = fqn.split('.').collect();
if parts.len() < 2 || parts.iter().any(|p| p.is_empty() || *p == "*") {
return None;
}
let rel_java = format!("{}.java", parts.join("/"));
for source_root in SOURCE_ROOTS {
let mut base = root.to_path_buf();
if !source_root.is_empty() {
for segment in source_root.split('/') {
base.push(segment);
}
if !base.is_dir() {
continue;
}
}
let candidate = base.join(&rel_java);
if candidate.is_file()
&& let Some(hit) = to_repo_relative(root, &candidate)
{
return Some(hit);
}
}
None
}
}
}
#[cfg(all(test, feature = "code-intel-stack"))]
mod stack_tests {
use std::fs;
use tempfile::TempDir;
use super::*;
use crate::intel::model::ImportEdge;
fn import(specifier: &str, imported: Option<&str>) -> ImportEdge {
ImportEdge {
local: imported.unwrap_or("mod").to_string(),
specifier: specifier.to_string(),
imported: imported.map(str::to_string),
is_type: false,
local_start: 0,
}
}
fn write(dir: &Path, rel: &str, body: &str) {
let abs = dir.join(rel);
fs::create_dir_all(abs.parent().unwrap()).unwrap();
fs::write(abs, body).unwrap();
}
#[test]
fn python_absolute_module_resolves_to_py_file() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "foo/bar.py", "x = 1\n");
write(root, "foo/__init__.py", "");
let resolver = SpecifierResolver::for_language("python").expect("python resolver");
let got = resolver.resolve(root, "app/main.py", &import("foo.bar", Some("x")));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("foo/bar.py".to_string())
);
}
#[test]
fn python_absolute_module_resolves_to_package_init() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "pkg/__init__.py", "y = 2\n");
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "app/main.py", &import("pkg", Some("y")));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("pkg/__init__.py".to_string())
);
}
#[test]
fn python_absolute_module_prefers_src_layout() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "src/mypkg/util.py", "z = 3\n");
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "src/mypkg/main.py", &import("mypkg.util", Some("z")));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("src/mypkg/util.py".to_string())
);
}
#[test]
fn python_relative_single_dot_resolves_sibling() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "pkg/sibling.py", "a = 1\n");
write(root, "pkg/main.py", "");
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "pkg/main.py", &import(".sibling", Some("a")));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("pkg/sibling.py".to_string())
);
}
#[test]
fn python_relative_bare_dot_resolves_package_init() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "pkg/__init__.py", "b = 1\n");
write(root, "pkg/main.py", "");
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "pkg/main.py", &import(".", Some("b")));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("pkg/__init__.py".to_string())
);
}
#[test]
fn python_relative_double_dot_climbs_package() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "pkg/other.py", "c = 1\n");
write(root, "pkg/sub/main.py", "");
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "pkg/sub/main.py", &import("..other", Some("c")));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("pkg/other.py".to_string())
);
}
#[test]
fn python_relative_cannot_climb_above_root() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "main.py", "");
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "main.py", &import("..x", Some("x")));
assert_eq!(got, None);
}
#[test]
fn python_missing_module_resolves_none() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let resolver = SpecifierResolver::for_language("python").unwrap();
let got = resolver.resolve(root, "main.py", &import("nope.gone", Some("x")));
assert_eq!(got, None);
}
#[test]
fn java_fqn_resolves_under_maven_root() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(
root,
"src/main/java/com/example/Foo.java",
"package com.example;\nclass Foo {}\n",
);
let resolver = SpecifierResolver::for_language("java").expect("java resolver");
let got = resolver.resolve(
root,
"src/main/java/com/example/Bar.java",
&import("com.example.Foo", None),
);
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("src/main/java/com/example/Foo.java".to_string())
);
}
#[test]
fn java_fqn_resolves_at_repo_root() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "com/example/Baz.java", "package com.example;\nclass Baz {}\n");
let resolver = SpecifierResolver::for_language("java").unwrap();
let got = resolver.resolve(root, "com/example/Main.java", &import("com.example.Baz", None));
assert_eq!(
got.and_then(|r| r.as_str().map(str::to_string)),
Some("com/example/Baz.java".to_string())
);
}
#[test]
fn java_wildcard_import_resolves_none() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
write(root, "src/main/java/com/example/Foo.java", "class Foo {}\n");
let resolver = SpecifierResolver::for_language("java").unwrap();
let got = resolver.resolve(
root,
"src/main/java/com/example/Bar.java",
&import("com.example.*", None),
);
assert_eq!(got, None);
}
#[test]
fn java_missing_type_resolves_none() {
let tmp = TempDir::new().unwrap();
let root = tmp.path();
let resolver = SpecifierResolver::for_language("java").unwrap();
let got = resolver.resolve(root, "Main.java", &import("com.example.Gone", None));
assert_eq!(got, None);
}
#[test]
fn unknown_language_has_no_resolver() {
assert!(SpecifierResolver::for_language("cobol").is_none());
assert!(!SpecifierResolver::supports("cobol"));
}
#[test]
fn python_java_imports_have_no_default_export_fallback() {
assert_eq!(
SpecifierResolver::for_language("python").unwrap().default_export_name(),
None,
"Python has no default-export convention"
);
assert_eq!(
SpecifierResolver::for_language("java").unwrap().default_export_name(),
None,
"Java has no default-export convention"
);
}
}