1use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::sync::RwLock;
10
11pub trait ImportResolver: Send + Sync {
17 fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf>;
18
19 fn clear_cache(&self) {}
21}
22
23pub(crate) fn strip_import_quotes(raw: &str) -> &str {
25 raw.trim_matches(|c| c == '"' || c == '\'')
26}
27
28pub(crate) fn strip_c_family_quotes(raw: &str) -> &str {
30 raw.trim_matches(|c| c == '<' || c == '>' || c == '"' || c == '\'')
31}
32
33pub(crate) fn probe_relative(
35 raw: &str,
36 source_dir: &Path,
37 extensions: &[&str],
38 is_file: &dyn Fn(&Path) -> bool,
39 fallback: bool,
40) -> Option<PathBuf> {
41 let base = if raw.starts_with('/') {
42 PathBuf::from("/")
43 } else {
44 source_dir.to_path_buf()
45 };
46 let path = base.join(raw);
47 for ext in extensions {
48 let candidate = if ext.is_empty() {
49 path.clone()
50 } else {
51 path.with_extension(ext.trim_start_matches('.'))
52 };
53 if is_file(&candidate) {
54 return Some(candidate);
55 }
56 }
57 fallback.then(|| path.clone())
58}
59
60pub(crate) fn resolve_js_family_import(
65 raw: &str,
66 source_dir: &Path,
67 extensions: &[&str],
68 is_file: &dyn Fn(&Path) -> bool,
69) -> Option<PathBuf> {
70 let raw = strip_import_quotes(raw);
71 if raw.is_empty() {
72 return None;
73 }
74 if !raw.starts_with('.') && !raw.starts_with('/') {
75 return Some(PathBuf::from(raw));
76 }
77 probe_relative(raw, source_dir, extensions, is_file, true)
78}
79
80pub(crate) fn resolve_c_family_import(raw: &str, source_dir: &Path) -> Option<PathBuf> {
86 let trimmed = raw.trim();
87 if trimmed.is_empty() || trimmed.starts_with('<') {
88 return None;
89 }
90 let relative = strip_c_family_quotes(trimmed);
91 if relative.is_empty() {
92 return None;
93 }
94 let candidate = source_dir.join(relative);
95 let candidate = if candidate.extension().is_none() {
96 candidate.with_extension("h")
97 } else {
98 candidate
99 };
100 candidate.is_file().then_some(candidate)
101}
102
103pub(crate) fn python_candidate_paths(
108 raw: &str,
109 source_dir: &Path,
110 project_root: &Path,
111) -> Option<(PathBuf, PathBuf)> {
112 let raw = strip_import_quotes(raw);
113 if raw.is_empty() {
114 return None;
115 }
116 if !raw.starts_with('.') {
117 let path = project_root.join(raw.replace('.', std::path::MAIN_SEPARATOR_STR));
118 let init = path.join("__init__.py");
119 let module = path.with_extension("py");
120 return Some((init, module));
121 }
122
123 let dots = raw.chars().take_while(|c| *c == '.').count();
124 let base = source_dir.ancestors().nth(dots - 1)?;
125 let rest = &raw[dots..];
126 if rest.is_empty() {
127 let init = base.join("__init__.py");
128 return Some((init.clone(), init));
129 }
130 let path = base.join(rest.replace('.', std::path::MAIN_SEPARATOR_STR));
131 let init = path.join("__init__.py");
132 let module = path.with_extension("py");
133 Some((init, module))
134}
135
136pub(crate) fn find_go_module(project_root: &Path) -> Option<(PathBuf, String)> {
138 let mut current = Some(project_root);
139 while let Some(dir) = current {
140 let go_mod = dir.join("go.mod");
141 if go_mod.is_file() {
142 if let Ok(content) = std::fs::read_to_string(&go_mod) {
143 for line in content.lines() {
144 let line = line.trim();
145 if let Some(module) = line.strip_prefix("module ") {
146 return Some((dir.to_path_buf(), module.trim().to_string()));
147 }
148 }
149 }
150 return None;
151 }
152 current = dir.parent();
153 }
154 None
155}
156
157pub struct StatelessResolver {
162 f: fn(&str, &Path, &Path) -> Option<PathBuf>,
163}
164
165impl StatelessResolver {
166 pub fn new(f: fn(&str, &Path, &Path) -> Option<PathBuf>) -> Self {
167 Self { f }
168 }
169}
170
171impl ImportResolver for StatelessResolver {
172 fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
173 (self.f)(raw, source_dir, project_root)
174 }
175}
176
177pub struct PythonResolver {
182 f: fn(&str, &Path, &Path) -> Option<PathBuf>,
183 exists_cache: RwLock<HashMap<PathBuf, bool>>,
184}
185
186impl PythonResolver {
187 pub fn new(f: fn(&str, &Path, &Path) -> Option<PathBuf>) -> Self {
188 Self {
189 f,
190 exists_cache: RwLock::new(HashMap::new()),
191 }
192 }
193
194 pub fn clear_cache(&self) {
195 let mut cache = self
196 .exists_cache
197 .write()
198 .unwrap_or_else(|poison| poison.into_inner());
199 cache.clear();
200 }
201}
202
203impl ImportResolver for PythonResolver {
204 fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
205 let check_exists = |path: &Path| -> bool {
206 let cache_val = self
207 .exists_cache
208 .read()
209 .ok()
210 .and_then(|cache| cache.get(path).copied());
211 if let Some(res) = cache_val {
212 return res;
213 }
214 let res = path.exists();
215 if res && let Ok(mut cache) = self.exists_cache.write() {
216 cache.insert(path.to_path_buf(), res);
217 }
218 res
219 };
220
221 if let Some((init_path, _)) = python_candidate_paths(raw, source_dir, project_root) {
222 let stripped = strip_import_quotes(raw);
223 if stripped.trim_start_matches('.').is_empty() && stripped.starts_with('.') {
224 return Some(init_path);
225 }
226 if check_exists(&init_path) {
227 return Some(init_path);
228 }
229 } else {
230 return None;
231 }
232
233 (self.f)(raw, source_dir, project_root)
234 }
235
236 fn clear_cache(&self) {
237 PythonResolver::clear_cache(self);
238 }
239}
240
241pub struct GoModResolver {
243 f: fn(&str, &Path, &Path) -> Option<PathBuf>,
244 cached_module: RwLock<Option<Option<(PathBuf, String)>>>,
245}
246
247impl GoModResolver {
248 pub fn new(f: fn(&str, &Path, &Path) -> Option<PathBuf>) -> Self {
249 Self {
250 f,
251 cached_module: RwLock::new(None),
252 }
253 }
254
255 pub fn clear_cache(&self) {
256 let mut guard = self
257 .cached_module
258 .write()
259 .unwrap_or_else(|poison| poison.into_inner());
260 *guard = None;
261 }
262}
263
264impl ImportResolver for GoModResolver {
265 fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
266 let raw = strip_import_quotes(raw);
267 if raw.is_empty() || raw.starts_with('.') {
268 return None;
270 }
271
272 let cached = self
273 .cached_module
274 .read()
275 .ok()
276 .and_then(|guard| guard.clone());
277 let module_info = match cached {
278 Some(info) => info,
279 None => {
280 let computed = find_go_module(project_root);
283 if let Some(found) = computed.as_ref()
284 && let Ok(mut guard) = self.cached_module.write()
285 {
286 *guard = Some(Some(found.clone()));
287 }
288 computed
289 }
290 };
291
292 let matched_module = module_info.as_ref().and_then(|(dir, name)| {
293 raw.strip_prefix(name.as_str())
294 .filter(|rest| rest.is_empty() || rest.starts_with('/'))
295 .map(|_| (dir, name))
296 });
297 if let Some((dir, module_name)) = matched_module {
298 let relative = raw[module_name.len()..].trim_start_matches('/');
299 if relative.is_empty() {
300 return None;
302 }
303 return Some(dir.join(relative).with_extension("go"));
304 }
305
306 (self.f)(raw, source_dir, project_root)
307 }
308
309 fn clear_cache(&self) {
310 GoModResolver::clear_cache(self);
311 }
312}
313
314pub struct NodeResolver {
319 f: fn(&str, &Path, &Path) -> Option<PathBuf>,
320 extensions: &'static [&'static str],
321 is_file_cache: RwLock<HashMap<PathBuf, bool>>,
322}
323
324pub(crate) const JS_EXTS: &[&str] = &["", ".js", ".json", ".node", ".mjs", ".cjs"];
326pub(crate) const TS_EXTS: &[&str] = &["", ".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"];
328
329impl NodeResolver {
330 pub fn new(
331 f: fn(&str, &Path, &Path) -> Option<PathBuf>,
332 extensions: &'static [&'static str],
333 ) -> Self {
334 Self {
335 f,
336 extensions,
337 is_file_cache: RwLock::new(HashMap::new()),
338 }
339 }
340
341 pub fn clear_cache(&self) {
342 let mut cache = self
343 .is_file_cache
344 .write()
345 .unwrap_or_else(|poison| poison.into_inner());
346 cache.clear();
347 }
348}
349
350impl ImportResolver for NodeResolver {
351 fn resolve(&self, raw: &str, source_dir: &Path, project_root: &Path) -> Option<PathBuf> {
352 let raw = strip_import_quotes(raw);
353 if raw.is_empty() {
354 return None;
355 }
356
357 let check_is_file = |path: &Path| -> bool {
358 let cache_val = self
359 .is_file_cache
360 .read()
361 .ok()
362 .and_then(|cache| cache.get(path).copied());
363 if let Some(res) = cache_val {
364 return res;
365 }
366 let res = path.is_file();
367 if res && let Ok(mut cache) = self.is_file_cache.write() {
368 cache.insert(path.to_path_buf(), res);
369 }
370 res
371 };
372
373 if !raw.starts_with('.') && !raw.starts_with('/') {
374 return (self.f)(raw, source_dir, project_root);
375 }
376
377 let path = if raw.starts_with('/') {
378 PathBuf::from("/").join(raw)
379 } else {
380 source_dir.join(raw)
381 };
382
383 for ext in self.extensions {
384 let candidate = if ext.is_empty() {
385 path.clone()
386 } else {
387 path.with_extension(ext.trim_start_matches('.'))
388 };
389 if check_is_file(&candidate) {
390 return Some(candidate);
391 }
392 }
393
394 (self.f)(raw, source_dir, project_root)
395 }
396
397 fn clear_cache(&self) {
398 NodeResolver::clear_cache(self);
399 }
400}
401
402pub fn make_resolver(lang: crate::language::LangId) -> Box<dyn ImportResolver> {
408 let f = lang.spec().import_path_resolver;
409 match lang {
410 crate::language::LangId::Python => Box::new(PythonResolver::new(f)),
411 crate::language::LangId::Go => Box::new(GoModResolver::new(f)),
412 crate::language::LangId::JavaScript => Box::new(NodeResolver::new(f, JS_EXTS)),
413 crate::language::LangId::TypeScript | crate::language::LangId::Tsx => {
414 Box::new(NodeResolver::new(f, TS_EXTS))
415 }
416 _ => Box::new(StatelessResolver::new(f)),
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use std::path::{Path, PathBuf};
424
425 #[test]
426 fn stateless_resolver_delegates_to_fn() {
427 fn my_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
429 Some(root.join(format!("{raw}.py")))
430 }
431 let resolver = StatelessResolver::new(my_resolver);
432 let result = resolver.resolve("foo", Path::new("/src"), Path::new("/proj"));
433 assert_eq!(result, Some(PathBuf::from("/proj/foo.py")));
434 }
435
436 #[test]
437 fn stateless_resolver_returns_none_for_unresolvable() {
438 fn null_resolver(_raw: &str, _src: &Path, _root: &Path) -> Option<PathBuf> {
439 None
440 }
441 let resolver = StatelessResolver::new(null_resolver);
442 let result = resolver.resolve("anything", Path::new("/src"), Path::new("/proj"));
443 assert!(result.is_none());
444 }
445
446 #[test]
447 fn make_resolver_returns_working_resolver_for_python() {
448 use crate::language::LangId;
449 let resolver = make_resolver(LangId::Python);
450 let result = resolver.resolve("b", Path::new("/proj/a"), Path::new("/proj"));
452 let _ = result; }
455
456 #[test]
457 fn import_resolver_trait_is_object_safe() {
458 fn accepts_boxed(_resolver: &dyn ImportResolver) {}
460
461 fn null_resolver(_raw: &str, _src: &Path, _root: &Path) -> Option<PathBuf> {
462 None
463 }
464 let resolver = StatelessResolver::new(null_resolver);
465 accepts_boxed(&resolver);
466 }
467
468 #[test]
469 fn python_resolver_resolves_import() {
470 fn dummy_python_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
471 Some(root.join(format!("{raw}.py")))
472 }
473 let resolver = PythonResolver::new(dummy_python_resolver);
474 let result = resolver.resolve("test", Path::new("/src"), Path::new("/proj"));
475 assert_eq!(result, Some(PathBuf::from("/proj/test.py")));
476 }
477
478 #[test]
479 fn tsconfig_resolver_resolves_import() {
480 fn dummy_ts_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
481 Some(root.join(format!("{raw}.ts")))
482 }
483 let resolver = NodeResolver::new(dummy_ts_resolver, TS_EXTS);
484 let result = resolver.resolve("test", Path::new("/src"), Path::new("/proj"));
485 assert_eq!(result, Some(PathBuf::from("/proj/test.ts")));
486 }
487
488 #[test]
489 fn node_resolver_uses_configured_extensions() {
490 fn fallback(raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
491 Some(PathBuf::from(raw))
492 }
493 let temp = std::env::temp_dir().join("node_resolver_uses_configured_extensions");
494 if temp.exists() {
495 let _ = std::fs::remove_dir_all(&temp);
496 }
497 std::fs::create_dir_all(&temp).unwrap();
498 let target = temp.join("mod.json");
499 std::fs::write(&target, "{}").unwrap();
500
501 let js = NodeResolver::new(fallback, JS_EXTS);
502 assert_eq!(js.resolve("./mod", &temp, &temp), Some(target.clone()));
503
504 let ts = NodeResolver::new(fallback, TS_EXTS);
505 assert_eq!(
506 ts.resolve("./mod", &temp, &temp),
507 Some(PathBuf::from("./mod"))
508 );
509
510 let _ = std::fs::remove_dir_all(&temp);
511 }
512
513 #[test]
514 fn shared_quote_strip_helpers() {
515 assert_eq!(strip_import_quotes("\"react\""), "react");
516 assert_eq!(strip_c_family_quotes("<stdio.h>"), "stdio.h");
517 }
518
519 fn scratch(name: &str) -> PathBuf {
520 let dir = std::env::temp_dir().join(format!("meta_ast_resolver_{name}"));
521 let _ = std::fs::remove_dir_all(&dir);
522 std::fs::create_dir_all(&dir).unwrap();
523 dir
524 }
525
526 fn touch(path: &Path) {
527 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
528 std::fs::write(path, "").unwrap();
529 }
530
531 fn resolved(
532 lang: crate::language::LangId,
533 raw: &str,
534 source_dir: &Path,
535 root: &Path,
536 ) -> Option<PathBuf> {
537 make_resolver(lang).resolve(raw, source_dir, root)
538 }
539
540 #[test]
541 fn python_level_two_relative_import_uses_the_parent_package() {
542 let root = scratch("py_level_two");
543 let pkg = root.join("pkg");
544 touch(&root.join("util.py"));
545 touch(&pkg.join("util.py"));
546
547 let out = resolved(crate::language::LangId::Python, "..util", &pkg, &root);
548 assert_eq!(out, Some(root.join("util.py")));
549 }
550
551 #[test]
552 fn python_third_party_import_is_not_a_project_path() {
553 let root = scratch("py_third_party");
554 let pkg = root.join("pkg");
555 touch(&pkg.join("__init__.py"));
556
557 let out = resolved(crate::language::LangId::Python, "requests", &pkg, &root);
558 assert_eq!(out, None);
559 }
560
561 #[test]
562 fn python_first_party_relative_import_still_resolves() {
563 let root = scratch("py_first_party");
564 let pkg = root.join("pkg");
565 touch(&pkg.join("util.py"));
566
567 let out = resolved(crate::language::LangId::Python, ".util", &pkg, &root);
568 assert_eq!(out, Some(pkg.join("util.py")));
569 }
570
571 #[test]
572 fn python_dotted_import_resolves_when_the_file_exists() {
573 let root = scratch("py_dotted");
574 touch(&root.join("svc/api.py"));
575
576 let out = resolved(crate::language::LangId::Python, "svc.api", &root, &root);
577 assert_eq!(out, Some(root.join("svc/api.py")));
578 }
579
580 #[test]
581 fn go_relative_import_is_rejected() {
582 let root = scratch("go_relative");
583 let out = resolved(crate::language::LangId::Go, "\"./util\"", &root, &root);
584 assert_eq!(out, None);
585 }
586
587 #[test]
588 fn go_module_prefix_requires_a_boundary() {
589 let root = scratch("go_prefix");
590 std::fs::write(root.join("go.mod"), "module myproject\n").unwrap();
591
592 let out = resolved(
593 crate::language::LangId::Go,
594 "\"myproject2/pkg\"",
595 &root,
596 &root,
597 );
598 assert_eq!(out, None);
599 }
600
601 #[test]
602 fn go_module_import_of_a_subpackage_resolves() {
603 let root = scratch("go_subpackage");
604 std::fs::write(root.join("go.mod"), "module myproject\n").unwrap();
605
606 let out = resolved(
607 crate::language::LangId::Go,
608 "\"myproject/internal/util\"",
609 &root,
610 &root,
611 );
612 assert_eq!(out, Some(root.join("internal/util.go")));
613 }
614
615 #[test]
616 fn c_system_include_is_not_a_project_path() {
617 let root = scratch("c_system_include");
618 let src = root.join("src");
619 std::fs::create_dir_all(&src).unwrap();
620
621 let out = resolved(crate::language::LangId::C, "<stdio.h>", &src, &root);
622 assert_eq!(out, None);
623 }
624
625 #[test]
626 fn c_missing_quoted_include_is_not_a_project_path() {
627 let root = scratch("c_missing_include");
628 let src = root.join("src");
629 std::fs::create_dir_all(&src).unwrap();
630
631 let out = resolved(crate::language::LangId::C, "\"missing.h\"", &src, &root);
632 assert_eq!(out, None);
633 }
634
635 #[test]
636 fn c_existing_quoted_include_resolves() {
637 let root = scratch("c_existing_include");
638 let src = root.join("src");
639 touch(&src.join("local.h"));
640
641 let out = resolved(crate::language::LangId::C, "\"local.h\"", &src, &root);
642 assert_eq!(out, Some(src.join("local.h")));
643 }
644
645 #[test]
646 fn rust_crate_path_walks_all_segments() {
647 let root = scratch("rust_crate_path");
648 let src = root.join("src");
649 touch(&src.join("a/b.rs"));
650
651 let out = resolved(
652 crate::language::LangId::Rust,
653 "\"crate::a::b\"",
654 &src,
655 &root,
656 );
657 assert_eq!(out, Some(src.join("a/b.rs")));
658 }
659
660 #[test]
661 fn rust_self_path_resolves_within_the_module() {
662 let root = scratch("rust_self_path");
663 let src = root.join("src");
664 touch(&src.join("here.rs"));
665
666 let out = resolved(crate::language::LangId::Rust, "\"self::here\"", &src, &root);
667 assert_eq!(out, Some(src.join("here.rs")));
668 }
669
670 #[test]
671 fn go_mod_resolver_resolves_import() {
672 fn dummy_go_resolver(raw: &str, _source: &Path, root: &Path) -> Option<PathBuf> {
673 Some(root.join(format!("{raw}.go")))
674 }
675 let resolver = GoModResolver::new(dummy_go_resolver);
676 let result = resolver.resolve("test", Path::new("/src"), Path::new("/proj"));
677 assert_eq!(result, Some(PathBuf::from("/proj/test.go")));
678 }
679
680 #[test]
681 fn go_mod_resolver_memoizes_go_mod_file() {
682 let temp_dir = std::env::temp_dir().join("go_mod_resolver_memoizes_go_mod_file");
683 if temp_dir.exists() {
684 let _ = std::fs::remove_dir_all(&temp_dir);
685 }
686 std::fs::create_dir_all(&temp_dir).unwrap();
687 let go_mod_path = temp_dir.join("go.mod");
688 std::fs::write(&go_mod_path, "module myproject\n").unwrap();
689
690 let resolver = make_resolver(crate::language::LangId::Go);
691
692 let res1 = resolver.resolve("myproject/sub", &temp_dir, &temp_dir);
694 assert_eq!(res1, Some(temp_dir.join("sub.go")));
695
696 std::fs::remove_file(&go_mod_path).unwrap();
698
699 let res2 = resolver.resolve("myproject/other", &temp_dir, &temp_dir);
701 assert_eq!(res2, Some(temp_dir.join("other.go")));
702
703 let _ = std::fs::remove_dir_all(&temp_dir);
704 }
705
706 #[test]
707 fn python_resolver_memoizes_exists_checks() {
708 let temp_dir = std::env::temp_dir().join("python_resolver_memoizes_exists_checks");
709 if temp_dir.exists() {
710 let _ = std::fs::remove_dir_all(&temp_dir);
711 }
712 std::fs::create_dir_all(&temp_dir).unwrap();
713 let pkg_dir = temp_dir.join("my_package");
714 std::fs::create_dir_all(&pkg_dir).unwrap();
715 let init_py = pkg_dir.join("__init__.py");
716 std::fs::write(&init_py, "").unwrap();
717
718 let resolver = make_resolver(crate::language::LangId::Python);
719
720 let res1 = resolver.resolve("my_package", &temp_dir, &temp_dir);
722 assert_eq!(res1, Some(init_py.clone()));
723
724 std::fs::remove_file(&init_py).unwrap();
726
727 let res2 = resolver.resolve("my_package", &temp_dir, &temp_dir);
729 assert_eq!(res2, Some(init_py));
730
731 let _ = std::fs::remove_dir_all(&temp_dir);
732 }
733
734 #[test]
735 fn tsconfig_resolver_memoizes_is_file_checks() {
736 let temp_dir = std::env::temp_dir().join("tsconfig_resolver_memoizes_is_file_checks");
737 if temp_dir.exists() {
738 let _ = std::fs::remove_dir_all(&temp_dir);
739 }
740 std::fs::create_dir_all(&temp_dir).unwrap();
741 let ts_file = temp_dir.join("my_file.ts");
742 std::fs::write(&ts_file, "").unwrap();
743
744 let resolver = make_resolver(crate::language::LangId::TypeScript);
745
746 let res1 = resolver.resolve("./my_file", &temp_dir, &temp_dir);
748 assert_eq!(res1, Some(ts_file.clone()));
749
750 std::fs::remove_file(&ts_file).unwrap();
752
753 let res2 = resolver.resolve("./my_file", &temp_dir, &temp_dir);
755 assert_eq!(res2, Some(ts_file));
756
757 let _ = std::fs::remove_dir_all(&temp_dir);
758 }
759
760 #[test]
761 fn resolver_clear_cache_invalidates_memoized_state() {
762 let temp_dir = std::env::temp_dir().join("resolver_clear_cache_invalidates_memoized_state");
763 if temp_dir.exists() {
764 let _ = std::fs::remove_dir_all(&temp_dir);
765 }
766 std::fs::create_dir_all(&temp_dir).unwrap();
767
768 let pkg_dir = temp_dir.join("py_pkg");
770 std::fs::create_dir_all(&pkg_dir).unwrap();
771 let init_py = pkg_dir.join("__init__.py");
772 std::fs::write(&init_py, "").unwrap();
773 let py_resolver = make_resolver(crate::language::LangId::Python);
774 assert_eq!(
775 py_resolver.resolve("py_pkg", &temp_dir, &temp_dir),
776 Some(init_py.clone())
777 );
778 std::fs::remove_file(&init_py).unwrap();
779 assert_eq!(
781 py_resolver.resolve("py_pkg", &temp_dir, &temp_dir),
782 Some(init_py.clone())
783 );
784 py_resolver.clear_cache();
786 assert_eq!(py_resolver.resolve("py_pkg", &temp_dir, &temp_dir), None);
787
788 let go_mod_path = temp_dir.join("go.mod");
790 std::fs::write(&go_mod_path, "module oldmod\n").unwrap();
791 let go_resolver = make_resolver(crate::language::LangId::Go);
792 assert_eq!(
793 go_resolver.resolve("oldmod/sub", &temp_dir, &temp_dir),
794 Some(temp_dir.join("sub.go"))
795 );
796 std::fs::write(&go_mod_path, "module newmod\n").unwrap();
797 assert_eq!(
799 go_resolver.resolve("oldmod/sub", &temp_dir, &temp_dir),
800 Some(temp_dir.join("sub.go"))
801 );
802 go_resolver.clear_cache();
804 assert_eq!(
805 go_resolver.resolve("oldmod/sub", &temp_dir, &temp_dir),
806 None
807 );
808 assert_eq!(
809 go_resolver.resolve("newmod/sub", &temp_dir, &temp_dir),
810 Some(temp_dir.join("sub.go"))
811 );
812
813 let ts_file = temp_dir.join("ts_file.ts");
815 std::fs::write(&ts_file, "").unwrap();
816 let ts_resolver = make_resolver(crate::language::LangId::TypeScript);
817 assert_eq!(
818 ts_resolver.resolve("./ts_file", &temp_dir, &temp_dir),
819 Some(ts_file.clone())
820 );
821 std::fs::remove_file(&ts_file).unwrap();
822 assert_eq!(
824 ts_resolver.resolve("./ts_file", &temp_dir, &temp_dir),
825 Some(ts_file)
826 );
827 ts_resolver.clear_cache();
829 assert_eq!(
830 ts_resolver.resolve("./ts_file", &temp_dir, &temp_dir),
831 Some(temp_dir.join("./ts_file"))
832 );
833
834 let _ = std::fs::remove_dir_all(&temp_dir);
835 }
836 #[test]
837 fn python_resolver_finds_a_module_created_after_a_miss() {
838 fn no_fallback(_raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
839 None
840 }
841
842 let root = std::env::temp_dir().join("resolver_python_created_after_miss");
843 let _ = std::fs::remove_dir_all(&root);
844 std::fs::create_dir_all(&root).unwrap();
845
846 let resolver = PythonResolver::new(no_fallback);
847 assert!(
848 resolver.resolve("pkg", &root, &root).is_none(),
849 "the module does not exist yet"
850 );
851
852 let (init, _module) = python_candidate_paths("pkg", &root, &root).unwrap();
853 std::fs::create_dir_all(init.parent().unwrap()).unwrap();
854 std::fs::write(&init, "").unwrap();
855
856 assert_eq!(
857 resolver.resolve("pkg", &root, &root),
858 Some(init),
859 "a module created after the first miss must resolve"
860 );
861
862 let _ = std::fs::remove_dir_all(&root);
863 }
864
865 #[test]
866 fn go_resolver_sees_a_module_file_created_after_a_miss() {
867 fn no_fallback(_raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
868 None
869 }
870
871 let root = std::env::temp_dir().join("resolver_go_created_after_miss");
872 let _ = std::fs::remove_dir_all(&root);
873 std::fs::create_dir_all(&root).unwrap();
874
875 let resolver = GoModResolver::new(no_fallback);
876 assert!(
877 resolver
878 .resolve("example.com/mod/util", &root, &root)
879 .is_none(),
880 "there is no go.mod yet"
881 );
882
883 std::fs::write(root.join("go.mod"), "module example.com/mod\n\ngo 1.22\n").unwrap();
884
885 assert_eq!(
886 resolver.resolve("example.com/mod/util", &root, &root),
887 Some(root.join("util").with_extension("go")),
888 "a go.mod written after the first miss must change the answer"
889 );
890
891 let _ = std::fs::remove_dir_all(&root);
892 }
893
894 #[test]
895 fn node_resolver_finds_a_file_created_after_a_miss() {
896 fn no_fallback(_raw: &str, _source: &Path, _root: &Path) -> Option<PathBuf> {
897 None
898 }
899
900 let root = std::env::temp_dir().join("resolver_node_created_after_miss");
901 let _ = std::fs::remove_dir_all(&root);
902 std::fs::create_dir_all(&root).unwrap();
903
904 let resolver = NodeResolver::new(no_fallback, JS_EXTS);
905 assert!(
906 resolver.resolve("./util", &root, &root).is_none(),
907 "the file does not exist yet"
908 );
909
910 let created = root.join("util.js");
911 std::fs::write(&created, "module.exports = {};\n").unwrap();
912
913 assert_eq!(
914 resolver.resolve("./util", &root, &root),
915 Some(created),
916 "a file created after the first miss must resolve"
917 );
918
919 let _ = std::fs::remove_dir_all(&root);
920 }
921}