1use super::SourceFile;
6use crate::{Session, SourceMap};
7use itertools::Itertools;
8use normalize_path::NormalizePath;
9use solar_config::ImportRemapping;
10use solar_data_structures::smallvec::SmallVec;
11use std::{
12 borrow::Cow,
13 io,
14 path::{Path, PathBuf},
15 sync::{Arc, OnceLock},
16};
17
18#[derive(Debug, thiserror::Error)]
20pub enum ResolveError {
21 #[error("couldn't read stdin: {0}")]
22 ReadStdin(#[source] io::Error),
23 #[error("couldn't read {0}: {1}")]
24 ReadFile(PathBuf, #[source] io::Error),
25 #[error("file {0} not found")]
26 NotFound(PathBuf),
27 #[error("multiple files match {}: {}", .0.display(), .1.iter().map(|f| f.name.display()).format(", "))]
28 MultipleMatches(PathBuf, Vec<Arc<SourceFile>>),
29}
30
31#[derive(derive_more::Debug)]
33pub struct FileResolver<'a> {
34 #[debug(skip)]
35 source_map: &'a SourceMap,
36
37 include_paths: Vec<PathBuf>,
39 remappings: Vec<ImportRemapping>,
41 base_path: Option<PathBuf>,
43
44 custom_current_dir: Option<PathBuf>,
46 env_current_dir: OnceLock<Option<PathBuf>>,
48}
49
50impl<'a> FileResolver<'a> {
51 pub fn new(source_map: &'a SourceMap) -> Self {
53 Self {
54 source_map,
55 include_paths: Vec::new(),
56 remappings: Vec::new(),
57 base_path: source_map.base_path(),
58 custom_current_dir: source_map.base_path(),
59 env_current_dir: OnceLock::new(),
60 }
61 }
62
63 pub fn configure_from_sess(&mut self, sess: &Session) {
65 self.add_include_paths(sess.opts.include_paths.iter().cloned());
66 self.add_import_remappings(sess.opts.import_remappings.iter().cloned());
67 if let Ok(current_dir) = std::env::current_dir() {
68 self.set_current_dir(¤t_dir);
69 }
70 'b: {
71 if let Some(base_path) = &sess.opts.base_path {
72 let base_path = if base_path.is_absolute() {
73 base_path.as_path()
74 } else {
75 &if let Ok(path) = self.canonicalize_unchecked(base_path) {
76 path
77 } else {
78 break 'b;
79 }
80 };
81 self.set_base_path(base_path);
82 self.set_current_dir(base_path);
84 }
85 }
86 }
87
88 pub fn clear(&mut self) {
90 self.include_paths.clear();
91 self.remappings.clear();
92 self.base_path = None;
93 self.custom_current_dir = None;
94 self.env_current_dir.take();
95 }
96
97 #[track_caller]
103 #[doc(alias = "set_base_path")]
104 pub fn set_current_dir(&mut self, current_dir: &Path) {
105 if !current_dir.is_absolute() {
106 panic!("current_dir must be an absolute path");
107 }
108 self.custom_current_dir = Some(current_dir.to_path_buf());
109 }
110
111 #[track_caller]
117 pub fn set_base_path(&mut self, base_path: &Path) {
118 if !base_path.is_absolute() {
119 panic!("base_path must be an absolute path");
120 }
121 self.base_path = Some(base_path.to_path_buf());
122 }
123
124 pub fn add_include_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
126 self.include_paths.extend(paths);
127 }
128
129 pub fn add_include_path(&mut self, path: PathBuf) {
131 self.include_paths.push(path)
132 }
133
134 pub fn add_import_remappings(&mut self, remappings: impl IntoIterator<Item = ImportRemapping>) {
136 self.remappings.extend(remappings);
137 }
138
139 pub fn add_import_remapping(&mut self, remapping: ImportRemapping) {
141 self.remappings.push(remapping);
142 }
143
144 pub fn source_map(&self) -> &'a SourceMap {
146 self.source_map
147 }
148
149 #[doc(alias = "base_path")]
151 pub fn current_dir(&self) -> &Path {
152 self.try_current_dir().unwrap_or(Path::new("."))
153 }
154
155 #[doc(alias = "try_base_path")]
157 pub fn try_current_dir(&self) -> Option<&Path> {
158 self.custom_current_dir.as_deref().or_else(|| self.env_current_dir())
159 }
160
161 pub fn try_base_path(&self) -> Option<&Path> {
163 self.base_path.as_deref().or_else(|| self.try_current_dir())
164 }
165
166 fn env_current_dir(&self) -> Option<&Path> {
167 self.env_current_dir
168 .get_or_init(|| {
169 std::env::current_dir()
170 .inspect_err(|e| debug!("failed to get current_dir: {e}"))
171 .ok()
172 })
173 .as_deref()
174 }
175
176 pub fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
178 self.canonicalize_unchecked(&self.make_absolute(path))
179 }
180
181 fn canonicalize_unchecked(&self, path: &Path) -> io::Result<PathBuf> {
182 self.source_map.file_loader().canonicalize_path(path)
183 }
184
185 pub fn normalize<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
189 Cow::Owned(path.normalize())
192 }
193
194 pub fn make_absolute<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
198 if path.is_absolute() {
199 Cow::Borrowed(path)
200 } else if let Some(current_dir) = self.try_current_dir() {
201 Cow::Owned(current_dir.join(path))
202 } else {
203 Cow::Borrowed(path)
204 }
205 }
206
207 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
211 pub fn resolve_file(
212 &self,
213 path: &Path,
214 mut parent: Option<&Path>,
215 ) -> Result<Arc<SourceFile>, ResolveError> {
216 if let Some(parent) = &mut parent
219 && let Some(base_path) = self.try_base_path()
220 {
221 if let Ok(new_parent) = parent.strip_prefix(base_path) {
222 *parent = new_parent;
223 } else {
224 trace!(?parent, ?base_path, "parent is not a subpath of the base path");
225 }
226 }
227
228 let is_relative = path.starts_with("./") || path.starts_with("../");
239 if (is_relative && parent.is_some()) || parent.is_none() {
240 let try_path = if is_relative
241 && let Some(parent) = parent
242 && let Some(parent_dir) = parent.parent()
243 {
244 &parent_dir.join(path)
245 } else {
246 path
247 };
248 if is_relative
249 && let Some(file) = self.source_map().get_file(&*self.normalize(try_path))
250 {
251 return Ok(file);
252 }
253 if let Some(file) = self.try_file(try_path)? {
254 return Ok(file);
255 }
256 if is_relative {
258 return Err(ResolveError::NotFound(path.into()));
259 }
260 }
261
262 let original_path = path;
263 let path = &*self.remap_path(path, parent);
264
265 let mut candidates = SmallVec::<[_; 1]>::new();
266 let mut push_candidate = |file: Arc<SourceFile>| {
268 if !candidates.iter().any(|f| Arc::ptr_eq(f, &file)) {
269 candidates.push(file);
270 }
271 };
272
273 if path.is_absolute() {
274 if let Some(file) = self.try_file(path)? {
275 push_candidate(file);
276 }
277 } else if let Some(file) = self.get_source_unit_file(path) {
278 return Ok(file);
279 } else {
280 let base_path = self.try_base_path().into_iter();
282 let mut searched = false;
283 for include_path in base_path.chain(self.include_paths.iter().map(|p| p.as_path())) {
284 searched = true;
285 let path = include_path.join(path);
286 if let Some(file) = self.try_file(&path)? {
287 push_candidate(file);
288 }
289 }
290 if !searched && let Some(file) = self.try_file(path)? {
291 push_candidate(file);
292 }
293 }
294
295 match candidates.len() {
296 0 => Err(ResolveError::NotFound(original_path.into())),
297 1 => Ok(candidates.pop().unwrap()),
298 _ => Err(ResolveError::MultipleMatches(original_path.into(), candidates.into_vec())),
299 }
300 }
301
302 pub fn remap_path<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
305 let remapped = self.remap_path_(path, parent);
306 if remapped != path {
307 trace!(remapped=%remapped.display());
308 }
309 remapped
310 }
311
312 fn remap_path_<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
313 let _context = &*parent.map(|p| p.to_string_lossy()).unwrap_or_default();
314
315 let mut longest_prefix = 0;
316 let mut longest_context = 0;
317 let mut best_match_target = None;
318 let mut unprefixed_path = path;
319 for ImportRemapping { context, prefix, path: target } in &self.remappings {
320 let context = &*sanitize_path(context);
321 let prefix = &*sanitize_path(prefix);
322
323 if context.len() < longest_context {
325 continue;
326 }
327 if !_context.starts_with(context) {
329 continue;
330 }
331 if prefix.len() < longest_prefix && context.len() == longest_context {
333 continue;
334 }
335 let Ok(up) = path.strip_prefix(prefix) else {
337 continue;
338 };
339 longest_context = context.len();
340 longest_prefix = prefix.len();
341 best_match_target = Some(sanitize_path(target));
342 unprefixed_path = up;
343 }
344 if let Some(best_match_target) = best_match_target {
345 let mut out = PathBuf::from(&*best_match_target);
346 out.push(unprefixed_path);
347 Cow::Owned(out)
348 } else {
349 Cow::Borrowed(unprefixed_path)
350 }
351 }
352
353 pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
355 self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
356 }
357
358 pub fn get_file(&self, path: &Path) -> Option<Arc<SourceFile>> {
360 self.get_file_inner(path, false).ok().flatten()
361 }
362
363 fn get_source_unit_file(&self, path: &Path) -> Option<Arc<SourceFile>> {
364 if path.is_absolute() {
365 return None;
366 }
367 if let Some(file) = self.source_map().get_file(path) {
368 return Some(file);
369 }
370
371 let rpath = &*self.normalize(path);
372 if rpath != path { self.source_map().get_file(rpath) } else { None }
373 }
374
375 #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
377 pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
378 self.get_file_inner(path, true)
379 }
380
381 fn get_file_inner(
382 &self,
383 path: &Path,
384 load: bool,
385 ) -> Result<Option<Arc<SourceFile>>, ResolveError> {
386 if let Some(file) = self.source_map().get_file(path) {
387 trace!("loaded from cache 1");
388 return Ok(Some(file));
389 }
390
391 let apath = &*self.make_absolute(path);
394 let rpath = &*self.normalize(apath);
395 if rpath != path
396 && let Some(file) = self.source_map().get_file(rpath)
397 {
398 trace!("loaded from cache 2");
399 return Ok(Some(file));
400 }
401
402 if load && let Ok(path) = self.canonicalize_unchecked(rpath) {
404 return self
405 .source_map()
406 .load_file_with_name(rpath.to_path_buf().into(), &path)
409 .map(Some)
410 .map_err(|e| ResolveError::ReadFile(path, e));
411 }
412
413 trace!("not found");
414 Ok(None)
415 }
416}
417
418fn sanitize_path(s: &str) -> impl std::ops::Deref<Target = str> + '_ {
419 s
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 struct TestCase<'a> {
428 remappings: &'a [&'a str],
429 sources: &'a [Source<'a>],
430 }
431 struct Source<'a> {
432 path: &'a str,
433 imports: &'a [&'a str],
435 }
436
437 fn run(test_case: &TestCase<'_>) {
438 static ONCE: std::sync::Once = std::sync::Once::new();
439 ONCE.call_once(|| {
440 tracing_subscriber::fmt::fmt()
441 .with_test_writer()
442 .with_max_level(tracing::level_filters::LevelFilter::TRACE)
443 .init();
444 });
445
446 let tmp = tempfile::Builder::new().prefix("solar-file-resolver-test").tempdir().unwrap();
447 let base_path = tmp.path().to_path_buf();
448
449 let sm = SourceMap::empty();
450 sm.set_base_path(Some(base_path.clone()));
451 for source in test_case.sources {
452 let path = base_path.join(source.path);
453 if let Some(parent) = path.parent() {
454 let parent = parent.to_str().unwrap();
455 std::fs::create_dir_all(parent).expect(parent);
456 }
457 std::fs::write(&path, "").expect(source.path);
458 sm.load_file(&path).expect(source.path);
459 }
460
461 let mut file_resolver = FileResolver::new(&sm);
462 for &remapping in test_case.remappings {
463 file_resolver.add_import_remapping(remapping.parse().expect(remapping));
464 }
465 for &Source { path, imports } in test_case.sources {
466 for (i, &import) in imports.iter().enumerate() {
467 let res = (|| -> Result<(), Box<dyn std::error::Error>> {
468 let (import, expected) = import
469 .split_once(" => ")
470 .ok_or("import is not in the format <import string> => <resolved path>")?;
471 let parent = base_path.join(path);
472 let resolved = file_resolver.resolve_file(import.as_ref(), Some(&parent))?;
473 let actual_full = resolved.name.as_real().ok_or("resolved file has no path")?;
474 let actual = actual_full.strip_prefix(&base_path).ok().ok_or(
475 "resolved file path is not a subpath of the base path (not absolute?)",
476 )?;
477 let actual =
478 actual.to_str().ok_or("resolved file path is not a valid string")?;
479 if actual != expected {
480 return Err(format!(
481 "did not resolve to the expected path ({actual} != {expected})",
482 )
483 .into());
484 }
485 Ok(())
486 })();
487 match res {
488 Ok(()) => {}
489 Err(e) => panic!("{path}:{i}: [{import}] {e}"),
490 }
491 }
492 }
493 }
494
495 #[test]
497 fn remappings() {
498 run(&TestCase {
499 remappings: &["s=s_1.4.6", "t=Tee"],
500 sources: &[
501 Source { path: "a", imports: &["s/s.sol => s_1.4.6/s.sol"] },
502 Source { path: "b", imports: &["t/tee.sol => Tee/tee.sol"] },
503 Source { path: "s_1.4.6/s.sol", imports: &[] },
504 Source { path: "Tee/tee.sol", imports: &[] },
505 ],
506 })
507 }
508
509 #[test]
510 fn context_dependent_remappings() {
511 run(&TestCase {
512 remappings: &["a:s=s_1.4.6", "b:s=s_1.4.7"],
513 sources: &[
514 Source { path: "a/a.sol", imports: &["s/s.sol => s_1.4.6/s.sol"] },
515 Source { path: "b/b.sol", imports: &["s/s.sol => s_1.4.7/s.sol"] },
516 Source { path: "s_1.4.6/s.sol", imports: &[] },
517 Source { path: "s_1.4.7/s.sol", imports: &[] },
518 ],
519 })
520 }
521
522 #[test]
523 fn context_dependent_remappings_ensure_default_and_module_preserved() {
524 run(&TestCase {
525 remappings: &[
526 "foo=vendor/foo_2.0.0",
527 "vendor/bar:foo=vendor/foo_1.0.0",
528 "bar=vendor/bar",
529 ],
530 sources: &[
531 Source {
532 path: "main.sol",
533 imports: &[
534 "foo/foo.sol => vendor/foo_2.0.0/foo.sol",
535 "bar/bar.sol => vendor/bar/bar.sol",
536 ],
537 },
538 Source {
539 path: "vendor/bar/bar.sol",
540 imports: &["foo/foo.sol => vendor/foo_1.0.0/foo.sol"],
541 },
542 Source { path: "vendor/foo_1.0.0/foo.sol", imports: &[] },
543 Source { path: "vendor/foo_2.0.0/foo.sol", imports: &[] },
544 ],
545 })
546 }
547
548 #[test]
549 fn context_dependent_remappings_order_independent() {
550 let sources = &[
551 Source { path: "a/main.sol", imports: &["x/y/z/z.sol => d/z.sol"] },
552 Source { path: "a/b/main.sol", imports: &["x/y/z/z.sol => e/y/z/z.sol"] },
553 Source { path: "d/z.sol", imports: &[] },
554 Source { path: "e/y/z/z.sol", imports: &[] },
555 ];
556 run(&TestCase { remappings: &["a:x/y/z=d", "a/b:x=e"], sources });
557 run(&TestCase { remappings: &["a/b:x=e", "a:x/y/z=d"], sources });
558 }
559
560 #[test]
561 fn top_level_relative_path_uses_current_dir() {
562 let tmp = tempfile::Builder::new().prefix("solar-file-resolver-test").tempdir().unwrap();
563 let cwd = tmp.path().join("cwd");
564 let sibling = tmp.path().join("sibling");
565 let source = sibling.join("a.sol");
566 let import = sibling.join("b.sol");
567 std::fs::create_dir_all(&cwd).unwrap();
568 std::fs::create_dir_all(&sibling).unwrap();
569 std::fs::write(&source, "").unwrap();
570 std::fs::write(&import, "").unwrap();
571
572 let sm = SourceMap::empty();
573 sm.set_base_path(Some(cwd));
574 let file_resolver = FileResolver::new(&sm);
575 let resolved = file_resolver.resolve_file(Path::new("../sibling/a.sol"), None).unwrap();
576
577 assert_eq!(resolved.name.as_real(), Some(source.as_path()));
578
579 let parent = resolved.name.as_real().unwrap();
580 let relative_import =
581 file_resolver.resolve_file(Path::new("./b.sol"), Some(parent)).unwrap();
582 assert_eq!(relative_import.name.as_real(), Some(import.as_path()));
583
584 let direct_import = file_resolver.resolve_file(Path::new("b.sol"), Some(parent));
585 assert!(
586 matches!(direct_import, Err(ResolveError::NotFound(path)) if path == Path::new("b.sol"))
587 );
588 }
589
590 #[test]
591 fn relative_import_from_virtual_source_uses_source_unit_name() {
592 let sm = SourceMap::empty();
593 sm.set_base_path(Some(PathBuf::new()));
594 let mut file_resolver = FileResolver::new(&sm);
595 file_resolver.set_current_dir(&std::env::current_dir().unwrap());
596 let imported = sm.new_source_file(PathBuf::from("B.sol"), "").unwrap();
597
598 let resolved = file_resolver.resolve_file(Path::new("./B.sol"), Some(Path::new("A.sol")));
599
600 assert!(Arc::ptr_eq(&resolved.unwrap(), &imported));
601 }
602
603 #[test]
604 fn direct_import_without_current_dir_uses_source_unit_name() {
605 use crate::source_map::FileLoader;
606
607 struct Loader;
608
609 impl FileLoader for Loader {
610 fn canonicalize_path(&self, path: &Path) -> io::Result<PathBuf> {
611 Ok(path.to_path_buf())
612 }
613
614 fn load_stdin(&self) -> io::Result<String> {
615 unreachable!()
616 }
617
618 fn load_file(&self, path: &Path) -> io::Result<String> {
619 assert_eq!(path, Path::new("B.sol"));
620 Ok(String::new())
621 }
622
623 fn load_binary_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
624 unreachable!()
625 }
626 }
627
628 let sm = SourceMap::empty();
629 sm.set_file_loader(Loader);
630 let file_resolver = FileResolver::new(&sm);
631 file_resolver.env_current_dir.set(None).unwrap();
632
633 let resolved = file_resolver.resolve_file(Path::new("B.sol"), Some(Path::new("A.sol")));
634
635 assert_eq!(resolved.unwrap().name.as_real(), Some(Path::new("B.sol")));
636 }
637
638 #[test]
639 fn direct_import_reuses_preloaded_source_unit_name() {
640 let tmp = tempfile::Builder::new().prefix("solar-file-resolver-test").tempdir().unwrap();
641 let base_path = tmp.path().to_path_buf();
642 let source_path = base_path.join("src/B.sol");
643 std::fs::create_dir_all(source_path.parent().unwrap()).unwrap();
644 std::fs::write(&source_path, "contract B {}").unwrap();
645
646 let sm = SourceMap::empty();
647 sm.set_base_path(Some(base_path.clone()));
648 let imported = sm.new_source_file(PathBuf::from("src/B.sol"), "contract B {}").unwrap();
649 let mut file_resolver = FileResolver::new(&sm);
650 file_resolver.set_current_dir(&base_path);
651
652 let resolved =
653 file_resolver.resolve_file(Path::new("src/B.sol"), Some(Path::new("test/A.sol")));
654
655 assert!(Arc::ptr_eq(&resolved.unwrap(), &imported));
656 }
657}
658
659#[cfg(test)]
661mod solang_import_resolution {
662 use super::*;
663 use std::collections::{HashMap, HashSet};
664
665 struct FixtureFile {
666 path: &'static str,
667 imports: &'static [&'static str],
668 }
669
670 struct Scenario {
671 name: &'static str,
672 input: &'static str,
673 remappings: &'static [&'static str],
674 base_path: Option<&'static str>,
675 include_paths: &'static [&'static str],
676 should_resolve: bool,
677 }
678
679 struct Harness<'a> {
680 _tmp: tempfile::TempDir,
681 root: PathBuf,
682 cwd: PathBuf,
683 imports: HashMap<PathBuf, &'a [&'a str]>,
684 }
685
686 impl<'a> Harness<'a> {
687 fn new(cwd: &str, files: &'a [FixtureFile]) -> Self {
688 let tmp = tempfile::Builder::new()
689 .prefix("solar-solang-import-resolution-test")
690 .tempdir()
691 .unwrap();
692 let root = tmp.path().to_path_buf();
693 let cwd = root.join(cwd);
694 let mut imports = HashMap::default();
695 for FixtureFile { path, imports: file_imports } in files {
696 let path_on_disk = cwd.join(path);
697 if let Some(parent) = path_on_disk.parent() {
698 std::fs::create_dir_all(parent).unwrap();
699 }
700 std::fs::write(path_on_disk, "").unwrap();
701 let key = cwd.join(path).strip_prefix(&root).unwrap().to_path_buf();
702 imports.insert(key, *file_imports);
703 }
704 Self { _tmp: tmp, root, cwd, imports }
705 }
706
707 fn run(&self, scenario: &Scenario) -> Result<(), ResolveError> {
708 let sm = SourceMap::empty();
709 sm.set_base_path(Some(self.cwd.clone()));
710 let mut file_resolver = FileResolver::new(&sm);
711 file_resolver.set_current_dir(&self.cwd);
712 if let Some(base_path) = scenario.base_path {
713 file_resolver.set_base_path(&self.cwd.join(base_path));
714 }
715 for include_path in scenario.include_paths {
716 file_resolver.add_include_path(self.cwd.join(include_path));
717 }
718 for remapping in scenario.remappings {
719 file_resolver.add_import_remapping(remapping.parse().unwrap());
720 }
721
722 let root_file = file_resolver.resolve_file(Path::new(scenario.input), None)?;
723 let mut stack = vec![root_file];
724 let mut seen = HashSet::new();
725 while let Some(file) = stack.pop() {
726 let path = file.name.as_real().unwrap();
727 if !seen.insert(path.to_path_buf()) {
728 continue;
729 }
730
731 let key = path.strip_prefix(&self.root).unwrap();
732 for import in self.imports.get(key).copied().unwrap_or_default() {
733 stack.push(file_resolver.resolve_file(Path::new(import), Some(path))?);
734 }
735 }
736 Ok(())
737 }
738 }
739
740 fn check_solang_import_resolution_scenarios(
741 cwd: &str,
742 files: &[FixtureFile],
743 scenarios: &[Scenario],
744 ) {
745 let harness = Harness::new(cwd, files);
746 for scenario in scenarios {
747 let result = harness.run(scenario);
748 assert_eq!(
749 result.is_ok(),
750 scenario.should_resolve,
751 "{}: expected should_resolve={}, got {result:?}",
752 scenario.name,
753 scenario.should_resolve
754 );
755 }
756 }
757
758 #[test]
759 fn solang_import_resolution_corpus() {
760 check_solang_import_resolution_scenarios(
761 "01_solang_remap_target",
762 &[
763 FixtureFile { path: "contracts/Contract.sol", imports: &["lib/Lib.sol"] },
764 FixtureFile { path: "resources/node_modules/lib/Lib.sol", imports: &[] },
765 ],
766 &[
767 Scenario {
768 name: "01.1 no remapping",
769 input: "contracts/Contract.sol",
770 remappings: &[],
771 base_path: None,
772 include_paths: &[],
773 should_resolve: false,
774 },
775 Scenario {
776 name: "01.2 no base path or include path",
777 input: "contracts/Contract.sol",
778 remappings: &["lib=node_modules/lib"],
779 base_path: None,
780 include_paths: &[],
781 should_resolve: false,
782 },
783 Scenario {
784 name: "01.3 incomplete include paths",
785 input: "contracts/Contract.sol",
786 remappings: &["lib=node_modules/lib"],
787 base_path: Some("."),
788 include_paths: &[],
789 should_resolve: false,
790 },
791 Scenario {
792 name: "01.4 incorrect include paths",
793 input: "contracts/Contract.sol",
794 remappings: &["lib=node_modules/lib"],
795 base_path: Some("."),
796 include_paths: &["resources/node_modules"],
797 should_resolve: false,
798 },
799 Scenario {
800 name: "01.5 correct configuration",
801 input: "contracts/Contract.sol",
802 remappings: &["lib=node_modules/lib"],
803 base_path: Some("."),
804 include_paths: &["resources"],
805 should_resolve: true,
806 },
807 ],
808 );
809
810 check_solang_import_resolution_scenarios(
811 "02_solang_incorrect_direct_imports",
812 &[
813 FixtureFile { path: "Ambiguous.sol", imports: &[] },
814 FixtureFile {
815 path: "contracts/Ambiguous.sol",
816 imports: &["Error: contracts/Ambiguous.sol should not be imported"],
817 },
818 FixtureFile { path: "contracts/Contract.sol", imports: &["Ambiguous.sol"] },
819 FixtureFile {
820 path: "resources/node_modules/lib/Ambiguous.sol",
821 imports: &[
822 "Error: resources/node_modules/lib/Ambiguous.sol should not be imported",
823 ],
824 },
825 FixtureFile { path: "resources/node_modules/lib/Lib.sol", imports: &[] },
826 ],
827 &[
828 Scenario {
829 name: "02.1 direct import default base path",
830 input: "contracts/Contract.sol",
831 remappings: &[],
832 base_path: None,
833 include_paths: &[],
834 should_resolve: true,
835 },
836 Scenario {
837 name: "02.2 direct import explicit base path",
838 input: "contracts/Contract.sol",
839 remappings: &[],
840 base_path: Some("."),
841 include_paths: &[],
842 should_resolve: true,
843 },
844 ],
845 );
846
847 check_solang_import_resolution_scenarios(
848 "03_ambiguous_imports_should_fail",
849 &[
850 FixtureFile { path: "Ambiguous.sol", imports: &["This should not be imported"] },
851 FixtureFile { path: "contracts/Ambiguous.sol", imports: &[] },
852 FixtureFile {
853 path: "contracts/Contract.sol",
854 imports: &["lib/Lib.sol", "Ambiguous.sol"],
855 },
856 FixtureFile { path: "resources/node_modules/lib/Ambiguous.sol", imports: &[] },
857 FixtureFile { path: "resources/node_modules/lib/Lib.sol", imports: &[] },
858 ],
859 &[
860 Scenario {
861 name: "03.1 ambiguous imports should fail",
862 input: "contracts/Contract.sol",
863 remappings: &["lib=resources/node_modules/lib"],
864 base_path: Some("."),
865 include_paths: &["contracts"],
866 should_resolve: false,
867 },
868 Scenario {
869 name: "03.2 import order resources then root",
870 input: "contracts/Contract.sol",
871 remappings: &["lib=resources/node_modules/lib"],
872 base_path: Some("."),
873 include_paths: &["resources/node_modules/lib", "."],
874 should_resolve: false,
875 },
876 Scenario {
877 name: "03.3 import order root then resources",
878 input: "contracts/Contract.sol",
879 remappings: &["lib=resources/node_modules/lib"],
880 base_path: Some("."),
881 include_paths: &[".", "resources/node_modules/lib"],
882 should_resolve: false,
883 },
884 ],
885 );
886
887 check_solang_import_resolution_scenarios(
888 "04_multiple_map_path_segments",
889 &[
890 FixtureFile { path: "contracts/Contract.sol", imports: &["lib/nested/Lib.sol"] },
891 FixtureFile { path: "resources/node_modules/lib/nested/Lib.sol", imports: &[] },
892 ],
893 &[Scenario {
894 name: "04.1 multiple import mapping segments",
895 input: "contracts/Contract.sol",
896 remappings: &["lib/nested=resources/node_modules/lib/nested"],
897 base_path: Some("."),
898 include_paths: &[],
899 should_resolve: true,
900 }],
901 );
902
903 check_solang_import_resolution_scenarios(
904 "05_import_path_order_should_not_matter",
905 &[
906 FixtureFile { path: "contracts/Contract.sol", imports: &["A.sol"] },
907 FixtureFile { path: "contracts/nested1/A.sol", imports: &[] },
908 FixtureFile { path: "contracts/nested2/A.sol", imports: &[] },
909 ],
910 &[
911 Scenario {
912 name: "05.1 include order nested1 then nested2",
913 input: "contracts/Contract.sol",
914 remappings: &[],
915 base_path: None,
916 include_paths: &["contracts/nested1", "contracts/nested2"],
917 should_resolve: false,
918 },
919 Scenario {
920 name: "05.2 include order nested2 then nested1",
921 input: "contracts/Contract.sol",
922 remappings: &[],
923 base_path: None,
924 include_paths: &["contracts/nested2", "contracts/nested1"],
925 should_resolve: false,
926 },
927 ],
928 );
929
930 check_solang_import_resolution_scenarios(
931 "06_redundant_remaps",
932 &[
933 FixtureFile {
934 path: "contracts/Contract.sol",
935 imports: &["node_modules/lib/Lib.sol"],
936 },
937 FixtureFile { path: "resources/node_modules/lib/Lib.sol", imports: &[] },
938 ],
939 &[
940 Scenario {
941 name: "06.1 multiple remappings",
942 input: "contracts/Contract.sol",
943 remappings: &[
944 "node_modules=resources/node_modules",
945 "node_modules=node_modules",
946 ],
947 base_path: Some("resources"),
948 include_paths: &[],
949 should_resolve: true,
950 },
951 Scenario {
952 name: "06.2 multiple remappings reversed",
953 input: "contracts/Contract.sol",
954 remappings: &[
955 "node_modules=node_modules",
956 "node_modules=resources/node_modules",
957 ],
958 base_path: Some("resources"),
959 include_paths: &[],
960 should_resolve: false,
961 },
962 Scenario {
963 name: "06.3 multiple remappings last wins",
964 input: "contracts/Contract.sol",
965 remappings: &[
966 "node_modules=node_modules",
967 "node_modules=resources/node_modules",
968 "node_modules=node_modules",
969 ],
970 base_path: Some("resources"),
971 include_paths: &[],
972 should_resolve: true,
973 },
974 ],
975 );
976 }
977}