use std::collections::{HashMap, HashSet};
use std::num::NonZeroUsize;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use lru::LruCache;
use super::cross_file_types::{ImportDef, ImportKind, ResolvedImport};
use super::module_index::ModuleIndex;
#[derive(Clone, Hash, PartialEq, Eq, Debug)]
struct CacheKey {
module: String,
names: Vec<String>,
current_file: PathBuf,
level: u8,
}
impl CacheKey {
fn new(import: &ImportDef, current_file: &Path) -> Self {
let mut names = import.names.clone();
names.sort(); Self {
module: import.module.clone(),
names,
current_file: current_file.to_path_buf(),
level: import.level,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct CacheStats {
pub hits: u64,
pub misses: u64,
pub entries: usize,
pub capacity: usize,
}
impl CacheStats {
pub fn hit_ratio(&self) -> f64 {
let total = self.hits + self.misses;
if total == 0 {
0.0
} else {
self.hits as f64 / total as f64
}
}
}
pub const DEFAULT_CACHE_SIZE: usize = 10_000;
pub struct ImportResolver<'a> {
index: &'a ModuleIndex,
cache: LruCache<CacheKey, Arc<Vec<ResolvedImport>>>,
all_cache: HashMap<PathBuf, Arc<Vec<String>>>,
cache_hits: u64,
cache_misses: u64,
}
impl<'a> ImportResolver<'a> {
pub fn new(index: &'a ModuleIndex, cache_size: usize) -> Self {
let cache_size = cache_size.max(1); Self {
index,
cache: LruCache::new(NonZeroUsize::new(cache_size).unwrap()),
all_cache: HashMap::new(),
cache_hits: 0,
cache_misses: 0,
}
}
pub fn with_default_cache(index: &'a ModuleIndex) -> Self {
Self::new(index, DEFAULT_CACHE_SIZE)
}
pub fn resolve(&mut self, import: &ImportDef, current_file: &Path) -> Vec<ResolvedImport> {
let cache_key = CacheKey::new(import, current_file);
if let Some(cached) = self.cache.get(&cache_key) {
self.cache_hits += 1;
return Arc::clone(cached).as_ref().clone();
}
self.cache_misses += 1;
let resolved = self.resolve_uncached(import, current_file);
let cached = Arc::new(resolved.clone());
self.cache.put(cache_key, cached);
resolved
}
fn resolve_uncached(&mut self, import: &ImportDef, current_file: &Path) -> Vec<ResolvedImport> {
let language = self.index.language();
let resolved_module = if import.level > 0 {
match language {
"python" => match self.resolve_relative(import, current_file) {
Some(m) => m,
None => {
return vec![];
}
},
"ruby" | "php" | "lua" | "luau" => {
if let Some(m) = self.resolve_file_relative(import, current_file) {
m
} else {
return vec![];
}
}
_ => match self.resolve_relative(import, current_file) {
Some(m) => m,
None => {
return vec![];
}
},
}
} else {
let module = &import.module;
let stripped = module
.strip_suffix(".js")
.or_else(|| module.strip_suffix(".jsx"))
.or_else(|| module.strip_suffix(".ts"))
.or_else(|| module.strip_suffix(".tsx"))
.or_else(|| module.strip_suffix(".mjs"))
.or_else(|| module.strip_suffix(".cjs"))
.or_else(|| module.strip_suffix(".rb"))
.or_else(|| module.strip_suffix(".php"))
.or_else(|| module.strip_suffix(".lua"))
.or_else(|| module.strip_suffix(".luau"));
let base = match stripped {
Some(base) => base.to_string(),
None => module.clone(),
};
if base.starts_with("./") || base.starts_with("../") {
match language {
"typescript" | "javascript" | "go" => {
match self.resolve_js_relative_path(&base, current_file) {
Some(resolved) => resolved,
None => base,
}
}
"ruby" | "php" | "lua" | "luau" | "c" | "cpp" => self
.resolve_file_path_import(&base, current_file)
.unwrap_or(base),
_ => base,
}
} else {
if language == "luau" {
if let Some(resolved) = self.resolve_luau_script_path(&base, current_file) {
resolved
} else {
base
}
} else if matches!(language, "ruby" | "php" | "lua" | "c" | "cpp")
&& looks_like_file_path(&base)
{
self.resolve_file_path_import(&base, current_file)
.unwrap_or(base)
} else {
base
}
}
};
if !self.index.is_project_module(&resolved_module) {
return vec![];
}
let module_file = match self.index.lookup(&resolved_module) {
Some(path) => path.to_path_buf(),
None => {
return vec![];
}
};
let kind = self.classify_import(import);
if import.is_wildcard() {
return self.resolve_wildcard(import, &resolved_module, &module_file);
}
let mut results = Vec::with_capacity(import.names.len());
if import.is_from {
for name in &import.names {
if language == "python" {
let candidate = if resolved_module.is_empty() {
name.clone()
} else {
format!("{}.{}", resolved_module, name)
};
if self.index.is_project_module(&candidate) {
if let Some(candidate_file) = self.index.lookup(&candidate) {
let mut original = import.clone();
original.resolved_module = Some(candidate.clone());
let resolved = ResolvedImport {
original,
resolved_file: Some(candidate_file.to_path_buf()),
resolved_name: Some(name.clone()),
is_external: false,
confidence: self.compute_confidence(kind),
};
results.push(resolved);
continue;
}
}
}
let mut original = import.clone();
original.resolved_module = Some(resolved_module.clone());
let resolved = ResolvedImport {
original,
resolved_file: Some(module_file.clone()),
resolved_name: Some(name.clone()),
is_external: false,
confidence: self.compute_confidence(kind),
};
results.push(resolved);
}
} else {
let mut original = import.clone();
original.resolved_module = Some(resolved_module.clone());
let resolved = ResolvedImport {
original,
resolved_file: Some(module_file.clone()),
resolved_name: Some(resolved_module.clone()),
is_external: false,
confidence: self.compute_confidence(kind),
};
results.push(resolved);
}
results
}
fn resolve_js_relative_path(&self, module: &str, current_file: &Path) -> Option<String> {
let project_root = self.index.project_root();
let language = self.index.language();
let canonical_file =
dunce::canonicalize(current_file).unwrap_or_else(|_| current_file.to_path_buf());
let rel_current = canonical_file.strip_prefix(project_root).ok()?;
let from_dir = rel_current
.parent()
.map(|p| p.to_string_lossy().replace('\\', "/"))
.unwrap_or_default();
let resolved = if let Some(rest) = module.strip_prefix("./") {
if from_dir.is_empty() {
rest.to_string()
} else {
format!("{}/{}", from_dir, rest)
}
} else if module.starts_with("../") {
let parts: Vec<&str> = if from_dir.is_empty() {
vec![]
} else {
from_dir.split('/').collect()
};
let mut dir_parts = parts;
let import_segments: Vec<&str> = module.split('/').collect();
let mut remaining = &import_segments[..];
while !remaining.is_empty() && remaining[0] == ".." {
remaining = &remaining[1..];
if dir_parts.is_empty() {
return None;
}
dir_parts.pop();
}
let rest: Vec<&str> = remaining.to_vec();
let mut all_parts = dir_parts;
all_parts.extend(rest);
all_parts.join("/")
} else {
return None;
};
match language {
"typescript" | "javascript" => Some(format!("./{}", resolved)),
"go" => Some(resolved),
_ => Some(format!("./{}", resolved)),
}
}
fn resolve_file_path_import(&self, module: &str, current_file: &Path) -> Option<String> {
let language = self.index.language();
let module_path = module.trim_start_matches("./");
let mut candidates = Vec::new();
if let Some(dir) = current_file.parent() {
candidates.extend(self.file_candidates_for_module(dir, module_path, language));
}
let root = self.index.project_root();
candidates.extend(self.file_candidates_for_module(root, module_path, language));
for candidate in candidates {
if let Some(resolved) = self.index.reverse_lookup(&candidate) {
return Some(resolved.to_string());
}
}
None
}
fn resolve_file_relative(&self, import: &ImportDef, current_file: &Path) -> Option<String> {
let mut module = import.module.clone();
if import.level > 1 && !module.starts_with("../") {
let prefix = "../".repeat((import.level - 1) as usize);
module = format!("{}{}", prefix, module);
}
self.resolve_file_path_import(&module, current_file)
}
fn resolve_luau_script_path(&self, module: &str, current_file: &Path) -> Option<String> {
if !module.starts_with("script") {
return None;
}
let mut parts = module.split('.').collect::<Vec<_>>();
if parts.is_empty() || parts[0] != "script" {
return None;
}
parts.remove(0);
let mut base_dir = current_file.parent()?;
while !parts.is_empty() && parts[0] == "Parent" {
base_dir = base_dir.parent()?;
parts.remove(0);
}
if parts.is_empty() {
return None;
}
let rel = parts.join("/");
let language = self.index.language();
let candidates = self.file_candidates_for_module(base_dir, &rel, language);
for candidate in candidates {
if let Some(resolved) = self.index.reverse_lookup(&candidate) {
return Some(resolved.to_string());
}
}
None
}
fn file_candidates_for_module(
&self,
base_dir: &Path,
module: &str,
language: &str,
) -> Vec<PathBuf> {
let mut candidates = Vec::new();
let module_path = Path::new(module);
if module_path.extension().is_some() {
candidates.push(base_dir.join(module_path));
return candidates;
}
for ext in language_extensions(language) {
candidates.push(base_dir.join(module_path).with_extension(ext));
}
candidates
}
pub fn resolve_relative(&self, import: &ImportDef, current_file: &Path) -> Option<String> {
if import.level == 0 {
return Some(import.module.clone());
}
let current_module = self.index.reverse_lookup(current_file)?;
let is_init = current_file
.file_name()
.map(|n| n == "__init__.py")
.unwrap_or(false);
let parts: Vec<&str> = current_module.split('.').collect();
let package_depth = if is_init {
parts.len()
} else {
parts.len() - 1
};
let levels_up = import.level as usize - 1;
if levels_up > package_depth {
return None;
}
let base_parts = &parts[..package_depth - levels_up];
let mut result_parts: Vec<&str> = base_parts.to_vec();
if !import.module.is_empty() {
for part in import.module.split('.') {
result_parts.push(part);
}
}
if result_parts.is_empty() {
return None;
}
Some(result_parts.join("."))
}
fn resolve_wildcard(
&mut self,
import: &ImportDef,
_resolved_module: &str,
module_file: &Path,
) -> Vec<ResolvedImport> {
let names = self.expand_wildcard(module_file);
names
.iter()
.map(|name| {
let mut modified_import = import.clone();
modified_import.names = vec![name.clone()];
ResolvedImport {
original: modified_import,
resolved_file: Some(module_file.to_path_buf()),
resolved_name: Some(name.clone()),
is_external: false,
confidence: 0.4, }
})
.collect()
}
fn expand_wildcard(&mut self, module_file: &Path) -> Arc<Vec<String>> {
if let Some(cached) = self.all_cache.get(module_file) {
return Arc::clone(cached);
}
let names = self.parse_all(module_file);
let cached = Arc::new(names);
self.all_cache
.insert(module_file.to_path_buf(), Arc::clone(&cached));
cached
}
fn parse_all(&self, module_file: &Path) -> Vec<String> {
let content = match std::fs::read_to_string(module_file) {
Ok(c) => c,
Err(_) => return vec![],
};
let mut names = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("__all__") && trimmed.contains('=') {
if let Some(bracket_start) = trimmed.find('[') {
if let Some(bracket_end) = trimmed.find(']') {
let list_content = &trimmed[bracket_start + 1..bracket_end];
for item in list_content.split(',') {
let item = item.trim();
let name = item
.trim_matches(|c| c == '"' || c == '\'' || c == ' ')
.to_string();
if !name.is_empty() {
names.push(name);
}
}
break;
}
}
}
}
names
}
fn classify_import(&self, import: &ImportDef) -> ImportKind {
if import.is_type_checking {
ImportKind::TypeOnly
} else if import.is_wildcard() {
ImportKind::Wildcard
} else if import.level > 0 {
ImportKind::Relative
} else {
ImportKind::Absolute
}
}
fn compute_confidence(&self, kind: ImportKind) -> f32 {
match kind {
ImportKind::Absolute => 1.0,
ImportKind::Relative => 0.9,
ImportKind::TypeOnly => 1.0,
ImportKind::Wildcard => 0.4,
}
}
pub fn clear_cache(&mut self) {
self.cache.clear();
self.all_cache.clear();
self.cache_hits = 0;
self.cache_misses = 0;
}
pub fn cache_stats(&self) -> CacheStats {
CacheStats {
hits: self.cache_hits,
misses: self.cache_misses,
entries: self.cache.len(),
capacity: self.cache.cap().get(),
}
}
pub fn module_index(&self) -> &ModuleIndex {
self.index
}
}
pub const DEFAULT_MAX_DEPTH: usize = 10;
#[derive(Debug, Clone, PartialEq)]
pub struct TracedReExport {
pub definition_file: PathBuf,
pub qualified_name: String,
pub depth: usize,
}
pub struct ReExportTracer<'a> {
index: &'a ModuleIndex,
cache: HashMap<(String, String), Option<TracedReExport>>,
cache_hits: usize,
cache_misses: usize,
}
impl<'a> ReExportTracer<'a> {
pub fn new(index: &'a ModuleIndex) -> Self {
Self {
index,
cache: HashMap::new(),
cache_hits: 0,
cache_misses: 0,
}
}
pub fn trace(
&mut self,
module_path: &str,
name: &str,
max_depth: usize,
) -> Option<TracedReExport> {
let cache_key = (module_path.to_string(), name.to_string());
if let Some(cached) = self.cache.get(&cache_key) {
self.cache_hits += 1;
return cached.clone();
}
self.cache_misses += 1;
let mut visited = std::collections::HashSet::new();
let result = self.trace_internal(module_path, name, max_depth, &mut visited);
self.cache.insert(cache_key, result.clone());
result
}
fn trace_internal(
&self,
module_path: &str,
name: &str,
max_depth: usize,
visited: &mut std::collections::HashSet<(String, String)>,
) -> Option<TracedReExport> {
let key = (module_path.to_string(), name.to_string());
if visited.contains(&key) {
return None;
}
if visited.len() >= max_depth {
return None;
}
visited.insert(key);
let module_file = self.index.lookup(module_path)?;
let is_package = module_file
.file_name()
.map(|n| n == "__init__.py")
.unwrap_or(false);
if !is_package {
return Some(TracedReExport {
definition_file: module_file.to_path_buf(),
qualified_name: name.to_string(),
depth: visited.len(),
});
}
let reexports = self.parse_init_reexports(module_file);
if let Some((source_module, source_name)) = reexports.get(name) {
return self.trace_internal(source_module, source_name, max_depth, visited);
}
if self.is_defined_locally(module_file, name) {
return Some(TracedReExport {
definition_file: module_file.to_path_buf(),
qualified_name: name.to_string(),
depth: visited.len(),
});
}
None
}
fn parse_init_reexports(&self, init_file: &Path) -> HashMap<String, (String, String)> {
let mut reexports = HashMap::new();
let content = match std::fs::read_to_string(init_file) {
Ok(c) => c,
Err(_) => return reexports,
};
let export_filter = parse_dunder_all(&content);
let package_module = match self.index.reverse_lookup(init_file) {
Some(m) => m.to_string(),
None => return reexports,
};
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("from ") {
continue;
}
if let Some(import_pos) = trimmed.find(" import ") {
let from_part = &trimmed[5..import_pos].trim();
let import_part = &trimmed[import_pos + 8..].trim();
if from_part.starts_with('.') {
let dots = from_part.chars().take_while(|c| *c == '.').count();
let module_rest = from_part[dots..].trim();
let base_module = if dots <= 1 {
package_module.clone()
} else {
let mut parts: Vec<&str> = package_module.split('.').collect();
let levels_up = dots - 1;
if parts.len() >= levels_up {
parts.truncate(parts.len() - levels_up);
}
parts.join(".")
};
for item in import_part.split(',') {
let item = item.trim();
let (original_name, alias) = if let Some(as_pos) = item.find(" as ") {
let original = item[..as_pos].trim();
let alias = item[as_pos + 4..].trim();
(original, alias)
} else {
(item, item)
};
let source_module = if module_rest.is_empty() {
let candidate = if base_module.is_empty() {
original_name.to_string()
} else {
format!("{}.{}", base_module, original_name)
};
if self.index.is_project_module(&candidate) {
candidate
} else {
base_module.clone()
}
} else if base_module.is_empty() {
module_rest.to_string()
} else {
format!("{}.{}", base_module, module_rest)
};
if !export_filter.is_empty() && !export_filter.contains(alias) {
continue;
}
reexports.insert(
alias.to_string(),
(source_module.clone(), original_name.to_string()),
);
}
}
}
}
reexports
}
fn is_defined_locally(&self, file: &Path, name: &str) -> bool {
let content = match std::fs::read_to_string(file) {
Ok(c) => c,
Err(_) => return false,
};
let class_pattern = format!("class {}:", name);
let class_pattern2 = format!("class {}(", name);
let def_pattern = format!("def {}(", name);
let assign_pattern = format!("{} =", name);
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with(&class_pattern)
|| trimmed.starts_with(&class_pattern2)
|| trimmed.starts_with(&def_pattern)
|| trimmed.starts_with(&assign_pattern)
{
return true;
}
}
false
}
pub fn clear_cache(&mut self) {
self.cache.clear();
self.cache_hits = 0;
self.cache_misses = 0;
}
pub fn cache_stats(&self) -> (usize, usize) {
(self.cache_hits, self.cache_misses)
}
}
fn parse_dunder_all(content: &str) -> HashSet<String> {
let mut exports = HashSet::new();
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.starts_with("__all__") {
continue;
}
let (_, rhs) = match trimmed.split_once('=') {
Some(parts) => parts,
None => continue,
};
let rhs = rhs.trim();
let inner = if (rhs.starts_with('[') && rhs.ends_with(']'))
|| (rhs.starts_with('(') && rhs.ends_with(')'))
{
&rhs[1..rhs.len() - 1]
} else {
continue;
};
for item in inner.split(',') {
let name = item.trim().trim_matches('"').trim_matches('\'').trim();
if !name.is_empty() {
exports.insert(name.to_string());
}
}
}
exports
}
fn looks_like_file_path(module: &str) -> bool {
module.contains('/')
|| module.contains('\\')
|| module.ends_with(".h")
|| module.ends_with(".hpp")
}
fn language_extensions(language: &str) -> &'static [&'static str] {
match language {
"ruby" => &["rb"],
"php" => &["php"],
"lua" => &["lua"],
"luau" => &["luau", "lua"],
"c" => &["c", "h"],
"cpp" => &["cpp", "cc", "cxx", "hpp", "hh", "hxx", "h"],
_ => &[],
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_project() -> (TempDir, ModuleIndex) {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("pkg/sub")).unwrap();
std::fs::write(
root.join("pkg/__init__.py"),
"from .module import MyClass\n__all__ = ['MyClass']\n",
)
.unwrap();
std::fs::write(
root.join("pkg/module.py"),
"class MyClass:\n pass\n\ndef helper():\n pass\n",
)
.unwrap();
std::fs::write(
root.join("pkg/sub/__init__.py"),
"from .impl import SubClass\n",
)
.unwrap();
std::fs::write(root.join("pkg/sub/impl.py"), "class SubClass:\n pass\n").unwrap();
std::fs::write(
root.join("main.py"),
"from pkg import MyClass\nfrom pkg.module import helper\n",
)
.unwrap();
let index = ModuleIndex::build(root, "python").unwrap();
(temp, index)
}
#[test]
fn test_resolve_absolute_import() {
let (_temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("pkg.module", vec!["MyClass".to_string()]);
let resolved = resolver.resolve(&import, Path::new("main.py"));
assert_eq!(resolved.len(), 1);
assert!(!resolved[0].is_external);
assert_eq!(resolved[0].resolved_name, Some("MyClass".to_string()));
}
#[test]
fn test_resolve_relative_import() {
let (temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::relative_import("module", vec!["MyClass".to_string()], 1);
let current_file = temp.path().join("pkg/__init__.py");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(resolved.len(), 1);
assert!(!resolved[0].is_external);
}
#[test]
fn test_resolve_relative_import_level_2() {
let (temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::relative_import("module", vec!["MyClass".to_string()], 2);
let current_file = temp.path().join("pkg/sub/impl.py");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(resolved.len(), 1);
assert!(!resolved[0].is_external);
}
#[test]
fn test_resolve_external_module() {
let (_temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::simple_import("os");
let resolved = resolver.resolve(&import, Path::new("main.py"));
assert!(resolved.is_empty()); }
#[test]
fn test_resolve_wildcard() {
let (temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::wildcard_import("pkg");
let current_file = temp.path().join("main.py");
let resolved = resolver.resolve(&import, ¤t_file);
let names: Vec<_> = resolved
.iter()
.filter_map(|r| r.resolved_name.as_ref())
.collect();
assert!(names.contains(&&"MyClass".to_string()));
}
#[test]
fn test_cache_hit() {
let (_temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("pkg.module", vec!["MyClass".to_string()]);
let _ = resolver.resolve(&import, Path::new("main.py"));
assert_eq!(resolver.cache_stats().hits, 0);
assert_eq!(resolver.cache_stats().misses, 1);
let _ = resolver.resolve(&import, Path::new("main.py"));
assert_eq!(resolver.cache_stats().hits, 1);
assert_eq!(resolver.cache_stats().misses, 1);
}
#[test]
fn test_resolve_relative_module_basic() {
let (temp, index) = create_test_project();
let resolver = ImportResolver::new(&index, 100);
let import = ImportDef::relative_import("module", vec![], 1);
let current_file = temp.path().join("pkg/__init__.py");
let result = resolver.resolve_relative(&import, ¤t_file);
assert!(result.is_some());
assert_eq!(result.unwrap(), "pkg.module");
}
#[test]
fn test_resolve_relative_beyond_root() {
let (temp, index) = create_test_project();
let resolver = ImportResolver::new(&index, 100);
let import = ImportDef::relative_import("X", vec![], 10);
let current_file = temp.path().join("pkg/module.py");
let result = resolver.resolve_relative(&import, ¤t_file);
assert!(result.is_none());
}
#[test]
fn test_reexport_tracer_single() {
let (_temp, index) = create_test_project();
let mut tracer = ReExportTracer::new(&index);
let result = tracer.trace("pkg", "MyClass", 10);
assert!(result.is_some());
let traced = result.unwrap();
assert!(traced.definition_file.ends_with("module.py"));
assert_eq!(traced.qualified_name, "MyClass");
}
#[test]
fn test_reexport_tracer_defined_in_init() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("pkg")).unwrap();
std::fs::write(
root.join("pkg/__init__.py"),
"class LocalClass:\n pass\n",
)
.unwrap();
let index = ModuleIndex::build(root, "python").unwrap();
let mut tracer = ReExportTracer::new(&index);
let result = tracer.trace("pkg", "LocalClass", 10);
assert!(result.is_some());
let traced = result.unwrap();
assert!(traced.definition_file.ends_with("__init__.py"));
}
#[test]
fn test_reexport_tracer_not_found() {
let (_temp, index) = create_test_project();
let mut tracer = ReExportTracer::new(&index);
let result = tracer.trace("pkg", "NonExistent", 10);
assert!(result.is_none());
}
#[test]
fn test_reexport_tracer_external() {
let (_temp, index) = create_test_project();
let mut tracer = ReExportTracer::new(&index);
let result = tracer.trace("os", "path", 10);
assert!(result.is_none()); }
#[test]
fn test_reexport_tracer_chain() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("pkg/sub")).unwrap();
std::fs::write(root.join("pkg/__init__.py"), "from .sub import DeepClass\n").unwrap();
std::fs::write(
root.join("pkg/sub/__init__.py"),
"from .impl import DeepClass\n",
)
.unwrap();
std::fs::write(root.join("pkg/sub/impl.py"), "class DeepClass:\n pass\n").unwrap();
let index = ModuleIndex::build(root, "python").unwrap();
let mut tracer = ReExportTracer::new(&index);
let result = tracer.trace("pkg", "DeepClass", 10);
assert!(result.is_some());
let traced = result.unwrap();
assert!(traced.definition_file.ends_with("impl.py"));
assert_eq!(traced.qualified_name, "DeepClass");
assert!(traced.depth >= 2); }
#[test]
fn test_reexport_tracer_circular() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("pkg/a")).unwrap();
std::fs::create_dir_all(root.join("pkg/b")).unwrap();
std::fs::write(root.join("pkg/__init__.py"), "").unwrap();
std::fs::write(root.join("pkg/a/__init__.py"), "from ..b import X\n").unwrap();
std::fs::write(root.join("pkg/b/__init__.py"), "from ..a import X\n").unwrap();
let index = ModuleIndex::build(root, "python").unwrap();
let mut tracer = ReExportTracer::new(&index);
let result = tracer.trace("pkg.a", "X", 10);
assert!(result.is_none());
}
#[test]
fn test_reexport_tracer_max_depth_exceeded() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("pkg/a/b/c/d/e")).unwrap();
std::fs::write(root.join("pkg/__init__.py"), "from .a import X\n").unwrap();
std::fs::write(root.join("pkg/a/__init__.py"), "from .b import X\n").unwrap();
std::fs::write(root.join("pkg/a/b/__init__.py"), "from .c import X\n").unwrap();
std::fs::write(root.join("pkg/a/b/c/__init__.py"), "from .d import X\n").unwrap();
std::fs::write(root.join("pkg/a/b/c/d/__init__.py"), "from .e import X\n").unwrap();
std::fs::write(
root.join("pkg/a/b/c/d/e/__init__.py"),
"class X:\n pass\n",
)
.unwrap();
let index = ModuleIndex::build(root, "python").unwrap();
let mut tracer1 = ReExportTracer::new(&index);
let result = tracer1.trace("pkg", "X", 3);
assert!(result.is_none());
let mut tracer2 = ReExportTracer::new(&index);
let result = tracer2.trace("pkg", "X", 10);
assert!(result.is_some());
let traced = result.unwrap();
assert!(traced.definition_file.ends_with("__init__.py"));
assert_eq!(traced.qualified_name, "X");
}
#[test]
fn test_reexport_tracer_cache_stats() {
let (_temp, index) = create_test_project();
let mut tracer = ReExportTracer::new(&index);
let (hits, misses) = tracer.cache_stats();
assert_eq!(hits, 0);
assert_eq!(misses, 0);
let _ = tracer.trace("pkg", "MyClass", 10);
let (hits, misses) = tracer.cache_stats();
assert_eq!(hits, 0);
assert_eq!(misses, 1);
let _ = tracer.trace("pkg", "MyClass", 10);
let (hits, misses) = tracer.cache_stats();
assert_eq!(hits, 1);
assert_eq!(misses, 1);
}
fn create_ts_test_project() -> (TempDir, ModuleIndex) {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("v4/core")).unwrap();
std::fs::create_dir_all(root.join("utils")).unwrap();
std::fs::write(
root.join("v4/core/errors.ts"),
"export class ZodError {}\nexport function formatError() {}\n",
)
.unwrap();
std::fs::write(
root.join("v4/core/parse.ts"),
"import { ZodError } from './errors';\nexport function parse() {}\n",
)
.unwrap();
std::fs::write(
root.join("v4/core/schemas.ts"),
"import { parse } from './parse';\nimport { formatError } from './errors';\n",
)
.unwrap();
std::fs::write(
root.join("v4/core/index.ts"),
"export { ZodError } from './errors';\nexport { parse } from './parse';\n",
)
.unwrap();
std::fs::write(
root.join("utils/helpers.ts"),
"export function helper() {}\n",
)
.unwrap();
std::fs::write(
root.join("utils/format.ts"),
"import { helper } from './helpers';\nexport function format() {}\n",
)
.unwrap();
std::fs::write(
root.join("main.ts"),
"import { ZodError } from './v4/core/errors';\nimport { helper } from './utils/helpers';\n",
)
.unwrap();
let index = ModuleIndex::build(root, "typescript").unwrap();
(temp, index)
}
#[test]
fn test_resolve_ts_dot_slash_import_same_directory() {
let (temp, index) = create_ts_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("./errors", vec!["ZodError".to_string()]);
let current_file = temp.path().join("v4/core/parse.ts");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(
resolved.len(),
1,
"Should resolve ./errors from v4/core/parse.ts"
);
assert!(!resolved[0].is_external);
assert_eq!(resolved[0].resolved_name, Some("ZodError".to_string()));
}
#[test]
fn test_resolve_ts_dot_slash_import_from_root() {
let (temp, index) = create_ts_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("./v4/core/errors", vec!["ZodError".to_string()]);
let current_file = temp.path().join("main.ts");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(
resolved.len(),
1,
"Should resolve ./v4/core/errors from main.ts"
);
assert!(!resolved[0].is_external);
}
#[test]
fn test_resolve_ts_dot_dot_slash_import() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("src/sub")).unwrap();
std::fs::create_dir_all(root.join("src/utils")).unwrap();
std::fs::write(
root.join("src/utils/helpers.ts"),
"export function help() {}\n",
)
.unwrap();
std::fs::write(
root.join("src/sub/consumer.ts"),
"import { help } from '../utils/helpers';\n",
)
.unwrap();
let index = ModuleIndex::build(root, "typescript").unwrap();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("../utils/helpers", vec!["help".to_string()]);
let current_file = root.join("src/sub/consumer.ts");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(
resolved.len(),
1,
"Should resolve ../utils/helpers from src/sub/consumer.ts"
);
assert!(!resolved[0].is_external);
assert_eq!(resolved[0].resolved_name, Some("help".to_string()));
}
#[test]
fn test_resolve_ts_dot_slash_with_js_extension() {
let (temp, index) = create_ts_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("./errors.js", vec!["ZodError".to_string()]);
let current_file = temp.path().join("v4/core/parse.ts");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(
resolved.len(),
1,
"Should resolve ./errors.js from v4/core/parse.ts (strip .js)"
);
assert!(!resolved[0].is_external);
}
#[test]
fn test_resolve_ts_preserves_python_behavior() {
let (_temp, index) = create_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("pkg.module", vec!["MyClass".to_string()]);
let resolved = resolver.resolve(&import, Path::new("main.py"));
assert_eq!(resolved.len(), 1);
assert!(!resolved[0].is_external);
assert_eq!(resolved[0].resolved_name, Some("MyClass".to_string()));
}
#[test]
fn test_resolve_ts_multiple_dot_dot_levels() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("a/b/c")).unwrap();
std::fs::write(root.join("a/target.ts"), "export function target() {}\n").unwrap();
std::fs::write(
root.join("a/b/c/deep.ts"),
"import { target } from '../../target';\n",
)
.unwrap();
let index = ModuleIndex::build(root, "typescript").unwrap();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("../../target", vec!["target".to_string()]);
let current_file = root.join("a/b/c/deep.ts");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(
resolved.len(),
1,
"Should resolve ../../target from a/b/c/deep.ts"
);
assert!(!resolved[0].is_external);
}
#[test]
fn test_resolve_ts_bare_module_not_affected() {
let (temp, index) = create_ts_test_project();
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("react", vec!["useState".to_string()]);
let current_file = temp.path().join("main.ts");
let resolved = resolver.resolve(&import, ¤t_file);
assert!(
resolved.is_empty(),
"Bare module 'react' should be external"
);
}
#[test]
fn test_resolve_go_dot_slash_import() {
let temp = TempDir::new().unwrap();
let root = temp.path();
std::fs::create_dir_all(root.join("cmd")).unwrap();
std::fs::create_dir_all(root.join("pkg/utils")).unwrap();
std::fs::write(root.join("cmd/main.go"), "package main\nimport \"./pkg\"\n").unwrap();
std::fs::write(
root.join("pkg/handler.go"),
"package pkg\nimport \"./utils\"\n",
)
.unwrap();
std::fs::write(
root.join("pkg/utils/helpers.go"),
"package utils\nfunc Help() {}\n",
)
.unwrap();
let index = ModuleIndex::build(root, "go").unwrap();
assert!(
index.lookup("cmd").is_some(),
"Go should index 'cmd' module"
);
assert!(
index.lookup("pkg").is_some(),
"Go should index 'pkg' module"
);
assert!(
index.lookup("pkg/utils").is_some(),
"Go should index 'pkg/utils' module"
);
let mut resolver = ImportResolver::new(&index, 100);
let import = ImportDef::from_import("./utils", vec!["Help".to_string()]);
let current_file = root.join("pkg/handler.go");
let resolved = resolver.resolve(&import, ¤t_file);
assert_eq!(
resolved.len(),
1,
"Should resolve ./utils from pkg/handler.go to pkg/utils"
);
assert!(!resolved[0].is_external);
}
}