use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub trait ImportResolver: Send + Sync {
fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf>;
fn clear_cache(&self) {}
}
pub(crate) fn strip_import_quotes(raw: &str) -> &str {
raw.trim_matches(|c| c == '"' || c == '\'')
}
pub(crate) fn strip_c_family_quotes(raw: &str) -> &str {
raw.trim_matches(|c| c == '<' || c == '>' || c == '"' || c == '\'')
}
pub(crate) fn probe_relative(
raw: &str,
source_dir: &Path,
extensions: &[&str],
is_file: &dyn Fn(&Path) -> bool,
fallback: bool,
) -> Option<PathBuf> {
let base = if raw.starts_with('/') {
PathBuf::from("/")
} else {
source_dir.to_path_buf()
};
let path = base.join(raw);
for ext in extensions {
let candidate = if ext.is_empty() {
path.clone()
} else {
path.with_extension(ext.trim_start_matches('.'))
};
if is_file(&candidate) {
return Some(candidate);
}
}
fallback.then(|| path.clone())
}
pub(crate) fn resolve_js_family_import(
raw: &str,
source_dir: &Path,
extensions: &[&str],
is_file: &dyn Fn(&Path) -> bool,
) -> Option<PathBuf> {
let raw = strip_import_quotes(raw);
if raw.is_empty() {
return None;
}
if !raw.starts_with('.') && !raw.starts_with('/') {
return Some(PathBuf::from(raw));
}
probe_relative(raw, source_dir, extensions, is_file, true)
}
pub(crate) fn resolve_c_family_import(raw: &str, source_dir: &Path) -> Option<PathBuf> {
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.starts_with('<') {
return None;
}
let relative = strip_c_family_quotes(trimmed);
if relative.is_empty() {
return None;
}
let candidate = source_dir.join(relative);
let candidate = if candidate.extension().is_none() {
candidate.with_extension("h")
} else {
candidate
};
candidate.is_file().then_some(candidate)
}
pub(crate) fn python_candidate_paths(
raw: &str,
source_dir: &Path,
project_root: &Path,
) -> Option<(PathBuf, PathBuf)> {
let raw = strip_import_quotes(raw);
if raw.is_empty() {
return None;
}
if !raw.starts_with('.') {
let path = project_root.join(raw.replace('.', std::path::MAIN_SEPARATOR_STR));
let init = path.join("__init__.py");
let module = path.with_extension("py");
return Some((init, module));
}
let dots = raw.chars().take_while(|c| *c == '.').count();
let base = source_dir.ancestors().nth(dots - 1)?;
let rest = &raw[dots..];
if rest.is_empty() {
let init = base.join("__init__.py");
return Some((init.clone(), init));
}
let path = base.join(rest.replace('.', std::path::MAIN_SEPARATOR_STR));
let init = path.join("__init__.py");
let module = path.with_extension("py");
Some((init, module))
}
pub(crate) fn find_go_module(project_root: &Path) -> Option<(PathBuf, String)> {
let mut current = Some(project_root);
while let Some(dir) = current {
let go_mod = dir.join("go.mod");
if go_mod.is_file() {
if let Ok(content) = std::fs::read_to_string(&go_mod) {
for line in content.lines() {
let line = line.trim();
if let Some(module) = line.strip_prefix("module ") {
return Some((dir.to_path_buf(), module.trim().to_string()));
}
}
}
return None;
}
current = dir.parent();
}
None
}
pub struct StatelessResolver {
f: fn(&str, &Path, &Path) -> Option<PathBuf>,
}
impl StatelessResolver {
pub fn new(f: fn(&str, &Path, &Path) -> Option<PathBuf>) -> Self {
Self { f }
}
}
impl ImportResolver for StatelessResolver {
fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
(self.f)(raw, source_dir, project_root)
}
}
pub struct PythonResolver {
f: fn(&str, &Path, &Path) -> Option<PathBuf>,
exists_cache: RwLock<HashMap<PathBuf, bool>>,
}
impl PythonResolver {
pub fn new(f: fn(&str, &Path, &Path) -> Option<PathBuf>) -> Self {
Self {
f,
exists_cache: RwLock::new(HashMap::new()),
}
}
pub fn clear_cache(&self) {
let mut cache = self
.exists_cache
.write()
.unwrap_or_else(|poison| poison.into_inner());
cache.clear();
}
}
impl ImportResolver for PythonResolver {
fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
let check_exists = |path: &Path| -> bool {
let cache_val = self
.exists_cache
.read()
.ok()
.and_then(|cache| cache.get(path).copied());
if let Some(res) = cache_val {
return res;
}
let res = path.exists();
if res && let Ok(mut cache) = self.exists_cache.write() {
cache.insert(path.to_path_buf(), res);
}
res
};
if let Some((init_path, _)) = python_candidate_paths(raw, source_dir, project_root) {
let stripped = strip_import_quotes(raw);
if stripped.trim_start_matches('.').is_empty() && stripped.starts_with('.') {
return Some(init_path);
}
if check_exists(&init_path) {
return Some(init_path);
}
} else {
return None;
}
(self.f)(raw, source_dir, project_root)
}
fn clear_cache(&self) {
PythonResolver::clear_cache(self);
}
}
pub struct GoModResolver {
f: fn(&str, &Path, &Path) -> Option<PathBuf>,
cached_module: RwLock<Option<Option<(PathBuf, String)>>>,
}
impl GoModResolver {
pub fn new(f: fn(&str, &Path, &Path) -> Option<PathBuf>) -> Self {
Self {
f,
cached_module: RwLock::new(None),
}
}
pub fn clear_cache(&self) {
let mut guard = self
.cached_module
.write()
.unwrap_or_else(|poison| poison.into_inner());
*guard = None;
}
}
impl ImportResolver for GoModResolver {
fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
let raw = strip_import_quotes(raw);
if raw.is_empty() || raw.starts_with('.') {
return None;
}
let cached = self
.cached_module
.read()
.ok()
.and_then(|guard| guard.clone());
let module_info = match cached {
Some(info) => info,
None => {
let computed = find_go_module(project_root);
if let Some(found) = computed.as_ref()
&& let Ok(mut guard) = self.cached_module.write()
{
*guard = Some(Some(found.clone()));
}
computed
}
};
let matched_module = module_info.as_ref().and_then(|(dir, name)| {
raw.strip_prefix(name.as_str())
.filter(|rest| rest.is_empty() || rest.starts_with('/'))
.map(|_| (dir, name))
});
if let Some((dir, module_name)) = matched_module {
let relative = raw[module_name.len()..].trim_start_matches('/');
if relative.is_empty() {
return None;
}
return Some(dir.join(relative).with_extension("go"));
}
(self.f)(raw, source_dir, project_root)
}
fn clear_cache(&self) {
GoModResolver::clear_cache(self);
}
}
pub struct NodeResolver {
f: fn(&str, &Path, &Path) -> Option<PathBuf>,
extensions: &'static [&'static str],
is_file_cache: RwLock<HashMap<PathBuf, bool>>,
}
pub(crate) const JS_EXTS: &[&str] = &["", ".js", ".json", ".node", ".mjs", ".cjs"];
pub(crate) const TS_EXTS: &[&str] = &["", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"];
impl NodeResolver {
pub fn new(
f: fn(&str, &Path, &Path) -> Option<PathBuf>,
extensions: &'static [&'static str],
) -> Self {
Self {
f,
extensions,
is_file_cache: RwLock::new(HashMap::new()),
}
}
pub fn clear_cache(&self) {
let mut cache = self
.is_file_cache
.write()
.unwrap_or_else(|poison| poison.into_inner());
cache.clear();
}
}
impl ImportResolver for NodeResolver {
fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
let raw = strip_import_quotes(raw);
if raw.is_empty() {
return None;
}
let check_is_file = |path: &Path| -> bool {
let cache_val = self
.is_file_cache
.read()
.ok()
.and_then(|cache| cache.get(path).copied());
if let Some(res) = cache_val {
return res;
}
let res = path.is_file();
if res && let Ok(mut cache) = self.is_file_cache.write() {
cache.insert(path.to_path_buf(), res);
}
res
};
if !raw.starts_with('.') && !raw.starts_with('/') {
return (self.f)(raw, source_dir, project_root);
}
let path = if raw.starts_with('/') {
PathBuf::from("/").join(raw)
} else {
source_dir.join(raw)
};
for ext in self.extensions {
let candidate = if ext.is_empty() {
path.clone()
} else {
path.with_extension(ext.trim_start_matches('.'))
};
if check_is_file(&candidate) {
return Some(candidate);
}
}
(self.f)(raw, source_dir, project_root)
}
fn clear_cache(&self) {
NodeResolver::clear_cache(self);
}
}
pub fn make_resolver(lang: crate::language::LangId) -> Box<dyn ImportResolver> {
let f = lang.spec().import_path_resolver;
match lang {
crate::language::LangId::Python => Box::new(PythonResolver::new(f)),
crate::language::LangId::Go => Box::new(GoModResolver::new(f)),
crate::language::LangId::JavaScript => Box::new(NodeResolver::new(f, JS_EXTS)),
crate::language::LangId::TypeScript | crate::language::LangId::Tsx => {
Box::new(NodeResolver::new(f, TS_EXTS))
}
_ => Box::new(StatelessResolver::new(f)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
#[test]
fn stateless_resolver_delegates_to_fn() {
fn my_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
Some(root.join(format!("{raw}.py")))
}
let resolver = StatelessResolver::new(my_resolver);
let result = resolver.resolve("foo", Path::new("/src"), Path::new("/proj"));
assert_eq!(result, Some(PathBuf::from("/proj/foo.py")));
}
#[test]
fn stateless_resolver_returns_none_for_unresolvable() {
fn null_resolver(_raw: &str, _src: &Path, _root: &Path) -> Option<PathBuf> {
None
}
let resolver = StatelessResolver::new(null_resolver);
let result = resolver.resolve("anything", Path::new("/src"), Path::new("/proj"));
assert!(result.is_none());
}
#[test]
fn make_resolver_returns_working_resolver_for_python() {
use crate::language::LangId;
let resolver = make_resolver(LangId::Python);
let result = resolver.resolve("b", Path::new("/proj/a"), Path::new("/proj"));
let _ = result; }
#[test]
fn import_resolver_trait_is_object_safe() {
fn accepts_boxed(_resolver: &dyn ImportResolver) {}
fn null_resolver(_raw: &str, _src: &Path, _root: &Path) -> Option<PathBuf> {
None
}
let resolver = StatelessResolver::new(null_resolver);
accepts_boxed(&resolver);
}
#[test]
fn python_resolver_resolves_import() {
fn dummy_python_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
Some(root.join(format!("{raw}.py")))
}
let resolver = PythonResolver::new(dummy_python_resolver);
let result = resolver.resolve("test", Path::new("/src"), Path::new("/proj"));
assert_eq!(result, Some(PathBuf::from("/proj/test.py")));
}
#[test]
fn tsconfig_resolver_resolves_import() {
fn dummy_ts_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
Some(root.join(format!("{raw}.ts")))
}
let resolver = NodeResolver::new(dummy_ts_resolver, TS_EXTS);
let result = resolver.resolve("test", Path::new("/src"), Path::new("/proj"));
assert_eq!(result, Some(PathBuf::from("/proj/test.ts")));
}
#[test]
fn node_resolver_uses_configured_extensions() {
fn fallback(raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
Some(PathBuf::from(raw))
}
let temp = std::env::temp_dir().join("node_resolver_uses_configured_extensions");
if temp.exists() {
let _ = std::fs::remove_dir_all(&temp);
}
std::fs::create_dir_all(&temp).unwrap();
let target = temp.join("mod.json");
std::fs::write(&target, "{}").unwrap();
let js = NodeResolver::new(fallback, JS_EXTS);
assert_eq!(js.resolve("./mod", &temp, &temp), Some(target.clone()));
let ts = NodeResolver::new(fallback, TS_EXTS);
assert_eq!(
ts.resolve("./mod", &temp, &temp),
Some(PathBuf::from("./mod"))
);
let _ = std::fs::remove_dir_all(&temp);
}
#[test]
fn shared_quote_strip_helpers() {
assert_eq!(strip_import_quotes("\"react\""), "react");
assert_eq!(strip_c_family_quotes("<stdio.h>"), "stdio.h");
}
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("meta_ast_resolver_{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn touch(path: &Path) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, "").unwrap();
}
fn resolved(
lang: crate::language::LangId,
raw: &str,
source_dir: &Path,
root: &Path,
) -> Option<PathBuf> {
make_resolver(lang).resolve(raw, source_dir, root)
}
#[test]
fn python_level_two_relative_import_uses_the_parent_package() {
let root = scratch("py_level_two");
let pkg = root.join("pkg");
touch(&root.join("util.py"));
touch(&pkg.join("util.py"));
let out = resolved(crate::language::LangId::Python, "..util", &pkg, &root);
assert_eq!(out, Some(root.join("util.py")));
}
#[test]
fn python_third_party_import_is_not_a_project_path() {
let root = scratch("py_third_party");
let pkg = root.join("pkg");
touch(&pkg.join("__init__.py"));
let out = resolved(crate::language::LangId::Python, "requests", &pkg, &root);
assert_eq!(out, None);
}
#[test]
fn python_first_party_relative_import_still_resolves() {
let root = scratch("py_first_party");
let pkg = root.join("pkg");
touch(&pkg.join("util.py"));
let out = resolved(crate::language::LangId::Python, ".util", &pkg, &root);
assert_eq!(out, Some(pkg.join("util.py")));
}
#[test]
fn python_dotted_import_resolves_when_the_file_exists() {
let root = scratch("py_dotted");
touch(&root.join("svc/api.py"));
let out = resolved(crate::language::LangId::Python, "svc.api", &root, &root);
assert_eq!(out, Some(root.join("svc/api.py")));
}
#[test]
fn go_relative_import_is_rejected() {
let root = scratch("go_relative");
let out = resolved(crate::language::LangId::Go, "\"./util\"", &root, &root);
assert_eq!(out, None);
}
#[test]
fn go_module_prefix_requires_a_boundary() {
let root = scratch("go_prefix");
std::fs::write(root.join("go.mod"), "module myproject\n").unwrap();
let out = resolved(
crate::language::LangId::Go,
"\"myproject2/pkg\"",
&root,
&root,
);
assert_eq!(out, None);
}
#[test]
fn go_module_import_of_a_subpackage_resolves() {
let root = scratch("go_subpackage");
std::fs::write(root.join("go.mod"), "module myproject\n").unwrap();
let out = resolved(
crate::language::LangId::Go,
"\"myproject/internal/util\"",
&root,
&root,
);
assert_eq!(out, Some(root.join("internal/util.go")));
}
#[test]
fn c_system_include_is_not_a_project_path() {
let root = scratch("c_system_include");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
let out = resolved(crate::language::LangId::C, "<stdio.h>", &src, &root);
assert_eq!(out, None);
}
#[test]
fn c_missing_quoted_include_is_not_a_project_path() {
let root = scratch("c_missing_include");
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
let out = resolved(crate::language::LangId::C, "\"missing.h\"", &src, &root);
assert_eq!(out, None);
}
#[test]
fn c_existing_quoted_include_resolves() {
let root = scratch("c_existing_include");
let src = root.join("src");
touch(&src.join("local.h"));
let out = resolved(crate::language::LangId::C, "\"local.h\"", &src, &root);
assert_eq!(out, Some(src.join("local.h")));
}
#[test]
fn rust_crate_path_walks_all_segments() {
let root = scratch("rust_crate_path");
let src = root.join("src");
touch(&src.join("a/b.rs"));
let out = resolved(
crate::language::LangId::Rust,
"\"crate::a::b\"",
&src,
&root,
);
assert_eq!(out, Some(src.join("a/b.rs")));
}
#[test]
fn rust_self_path_resolves_within_the_module() {
let root = scratch("rust_self_path");
let src = root.join("src");
touch(&src.join("here.rs"));
let out = resolved(crate::language::LangId::Rust, "\"self::here\"", &src, &root);
assert_eq!(out, Some(src.join("here.rs")));
}
#[test]
fn go_mod_resolver_resolves_import() {
fn dummy_go_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
Some(root.join(format!("{raw}.go")))
}
let resolver = GoModResolver::new(dummy_go_resolver);
let result = resolver.resolve("test", Path::new("/src"), Path::new("/proj"));
assert_eq!(result, Some(PathBuf::from("/proj/test.go")));
}
#[test]
fn go_mod_resolver_memoizes_go_mod_file() {
let temp_dir = std::env::temp_dir().join("go_mod_resolver_memoizes_go_mod_file");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let go_mod_path = temp_dir.join("go.mod");
std::fs::write(&go_mod_path, "module myproject\n").unwrap();
let resolver = make_resolver(crate::language::LangId::Go);
let res1 = resolver.resolve("myproject/sub", &temp_dir, &temp_dir);
assert_eq!(res1, Some(temp_dir.join("sub.go")));
std::fs::remove_file(&go_mod_path).unwrap();
let res2 = resolver.resolve("myproject/other", &temp_dir, &temp_dir);
assert_eq!(res2, Some(temp_dir.join("other.go")));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn python_resolver_memoizes_exists_checks() {
let temp_dir = std::env::temp_dir().join("python_resolver_memoizes_exists_checks");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let pkg_dir = temp_dir.join("my_package");
std::fs::create_dir_all(&pkg_dir).unwrap();
let init_py = pkg_dir.join("__init__.py");
std::fs::write(&init_py, "").unwrap();
let resolver = make_resolver(crate::language::LangId::Python);
let res1 = resolver.resolve("my_package", &temp_dir, &temp_dir);
assert_eq!(res1, Some(init_py.clone()));
std::fs::remove_file(&init_py).unwrap();
let res2 = resolver.resolve("my_package", &temp_dir, &temp_dir);
assert_eq!(res2, Some(init_py));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn tsconfig_resolver_memoizes_is_file_checks() {
let temp_dir = std::env::temp_dir().join("tsconfig_resolver_memoizes_is_file_checks");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let ts_file = temp_dir.join("my_file.ts");
std::fs::write(&ts_file, "").unwrap();
let resolver = make_resolver(crate::language::LangId::TypeScript);
let res1 = resolver.resolve("./my_file", &temp_dir, &temp_dir);
assert_eq!(res1, Some(ts_file.clone()));
std::fs::remove_file(&ts_file).unwrap();
let res2 = resolver.resolve("./my_file", &temp_dir, &temp_dir);
assert_eq!(res2, Some(ts_file));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn resolver_clear_cache_invalidates_memoized_state() {
let temp_dir = std::env::temp_dir().join("resolver_clear_cache_invalidates_memoized_state");
if temp_dir.exists() {
let _ = std::fs::remove_dir_all(&temp_dir);
}
std::fs::create_dir_all(&temp_dir).unwrap();
let pkg_dir = temp_dir.join("py_pkg");
std::fs::create_dir_all(&pkg_dir).unwrap();
let init_py = pkg_dir.join("__init__.py");
std::fs::write(&init_py, "").unwrap();
let py_resolver = make_resolver(crate::language::LangId::Python);
assert_eq!(
py_resolver.resolve("py_pkg", &temp_dir, &temp_dir),
Some(init_py.clone())
);
std::fs::remove_file(&init_py).unwrap();
assert_eq!(
py_resolver.resolve("py_pkg", &temp_dir, &temp_dir),
Some(init_py.clone())
);
py_resolver.clear_cache();
assert_eq!(py_resolver.resolve("py_pkg", &temp_dir, &temp_dir), None);
let go_mod_path = temp_dir.join("go.mod");
std::fs::write(&go_mod_path, "module oldmod\n").unwrap();
let go_resolver = make_resolver(crate::language::LangId::Go);
assert_eq!(
go_resolver.resolve("oldmod/sub", &temp_dir, &temp_dir),
Some(temp_dir.join("sub.go"))
);
std::fs::write(&go_mod_path, "module newmod\n").unwrap();
assert_eq!(
go_resolver.resolve("oldmod/sub", &temp_dir, &temp_dir),
Some(temp_dir.join("sub.go"))
);
go_resolver.clear_cache();
assert_eq!(
go_resolver.resolve("oldmod/sub", &temp_dir, &temp_dir),
None
);
assert_eq!(
go_resolver.resolve("newmod/sub", &temp_dir, &temp_dir),
Some(temp_dir.join("sub.go"))
);
let ts_file = temp_dir.join("ts_file.ts");
std::fs::write(&ts_file, "").unwrap();
let ts_resolver = make_resolver(crate::language::LangId::TypeScript);
assert_eq!(
ts_resolver.resolve("./ts_file", &temp_dir, &temp_dir),
Some(ts_file.clone())
);
std::fs::remove_file(&ts_file).unwrap();
assert_eq!(
ts_resolver.resolve("./ts_file", &temp_dir, &temp_dir),
Some(ts_file)
);
ts_resolver.clear_cache();
assert_eq!(
ts_resolver.resolve("./ts_file", &temp_dir, &temp_dir),
Some(temp_dir.join("./ts_file"))
);
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn python_resolver_finds_a_module_created_after_a_miss() {
fn no_fallback(_raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
None
}
let root = std::env::temp_dir().join("resolver_python_created_after_miss");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let resolver = PythonResolver::new(no_fallback);
assert!(
resolver.resolve("pkg", &root, &root).is_none(),
"the module does not exist yet"
);
let (init, _module) = python_candidate_paths("pkg", &root, &root).unwrap();
std::fs::create_dir_all(init.parent().unwrap()).unwrap();
std::fs::write(&init, "").unwrap();
assert_eq!(
resolver.resolve("pkg", &root, &root),
Some(init),
"a module created after the first miss must resolve"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn go_resolver_sees_a_module_file_created_after_a_miss() {
fn no_fallback(_raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
None
}
let root = std::env::temp_dir().join("resolver_go_created_after_miss");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let resolver = GoModResolver::new(no_fallback);
assert!(
resolver
.resolve("example.com/mod/util", &root, &root)
.is_none(),
"there is no go.mod yet"
);
std::fs::write(root.join("go.mod"), "module example.com/mod\n\ngo 1.22\n").unwrap();
assert_eq!(
resolver.resolve("example.com/mod/util", &root, &root),
Some(root.join("util").with_extension("go")),
"a go.mod written after the first miss must change the answer"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn node_resolver_finds_a_file_created_after_a_miss() {
fn no_fallback(_raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
None
}
let root = std::env::temp_dir().join("resolver_node_created_after_miss");
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let resolver = NodeResolver::new(no_fallback, JS_EXTS);
assert!(
resolver.resolve("./util", &root, &root).is_none(),
"the file does not exist yet"
);
let created = root.join("util.js");
std::fs::write(&created, "module.exports = {};\n").unwrap();
assert_eq!(
resolver.resolve("./util", &root, &root),
Some(created),
"a file created after the first miss must resolve"
);
let _ = std::fs::remove_dir_all(&root);
}
}