Skip to main content

solar_interface/source_map/
file_resolver.rs

1//! File resolver.
2//!
3//! Modified from [`solang`](https://github.com/hyperledger/solang/blob/0f032dcec2c6e96797fd66fa0175a02be0aba71c/src/file_resolver.rs).
4
5use 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/// An error that occurred while resolving a path.
19#[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/// Performs file resolution by applying import paths and mappings.
32#[derive(derive_more::Debug)]
33pub struct FileResolver<'a> {
34    #[debug(skip)]
35    source_map: &'a SourceMap,
36
37    /// Include paths.
38    include_paths: Vec<PathBuf>,
39    /// Import remappings.
40    remappings: Vec<ImportRemapping>,
41    /// Base path for source unit names.
42    base_path: Option<PathBuf>,
43
44    /// Custom current directory.
45    custom_current_dir: Option<PathBuf>,
46    /// [`std::env::current_dir`] cache. Unused if the current directory is set manually.
47    env_current_dir: OnceLock<Option<PathBuf>>,
48}
49
50impl<'a> FileResolver<'a> {
51    /// Creates a new file resolver.
52    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    /// Configures the file resolver from a session.
64    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(&current_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                // Source unit names are relative to the base path after parent paths are stripped.
83                self.set_current_dir(base_path);
84            }
85        }
86    }
87
88    /// Clears the internal state.
89    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    /// Sets the current directory.
98    ///
99    /// # Panics
100    ///
101    /// Panics if `current_dir` is not an absolute path.
102    #[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    /// Sets the base path.
112    ///
113    /// # Panics
114    ///
115    /// Panics if `base_path` is not an absolute path.
116    #[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    /// Adds include paths.
125    pub fn add_include_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
126        self.include_paths.extend(paths);
127    }
128
129    /// Adds an include path.
130    pub fn add_include_path(&mut self, path: PathBuf) {
131        self.include_paths.push(path)
132    }
133
134    /// Adds import remappings.
135    pub fn add_import_remappings(&mut self, remappings: impl IntoIterator<Item = ImportRemapping>) {
136        self.remappings.extend(remappings);
137    }
138
139    /// Adds an import remapping.
140    pub fn add_import_remapping(&mut self, remapping: ImportRemapping) {
141        self.remappings.push(remapping);
142    }
143
144    /// Returns the source map.
145    pub fn source_map(&self) -> &'a SourceMap {
146        self.source_map
147    }
148
149    /// Returns the current directory, or `.` if it could not be resolved.
150    #[doc(alias = "base_path")]
151    pub fn current_dir(&self) -> &Path {
152        self.try_current_dir().unwrap_or(Path::new("."))
153    }
154
155    /// Returns the current directory, if resolved successfully.
156    #[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    /// Returns the base path for import resolution.
162    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    /// Canonicalizes a path using [`Self::current_dir`].
177    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    /// Normalizes a path removing unnecessary components.
186    ///
187    /// Does not perform I/O.
188    pub fn normalize<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
189        // NOTE: checking `is_normalized` will not produce the correct result since it won't
190        // consider `./` segments. See its documentation.
191        Cow::Owned(path.normalize())
192    }
193
194    /// Makes the path absolute by joining it with the current directory.
195    ///
196    /// Does not perform I/O.
197    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    /// Resolves an import path.
208    ///
209    /// `parent` is the path of the file that contains the import, if any.
210    #[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        // `parent` comes from `FileName::Real` so it should be an absolute path.
217        // Make it relative to the base path.
218        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        // https://docs.soliditylang.org/en/latest/path-resolution.html
229        // Only when the path starts with ./ or ../ are relative paths considered; this means
230        // that `import "b.sol";` will check the import paths for b.sol, while `import "./b.sol";`
231        // will only check the path relative to the current file.
232        //
233        // `parent.is_none()` only happens when resolving imports from a custom/stdin file, or when
234        // manually resolving a file, like from CLI arguments. In these cases, the file is
235        // considered to be in the current directory.
236        // Technically, this behavior allows the latter, the manual case, to also be resolved using
237        // remappings, which is not the case in solc, but this simplifies the implementation.
238        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            // See above.
257            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        // Quick deduplication when include paths are duplicated.
267        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            // Try the base path and all include paths.
281            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    /// Applies the import path mappings to `path`.
303    // Reference: <https://github.com/argotorg/solidity/blob/e202d30db8e7e4211ee973237ecbe485048aae97/libsolidity/interface/ImportRemapper.cpp#L32>
304    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            // Skip if current context is closer.
324            if context.len() < longest_context {
325                continue;
326            }
327            // Skip if current context is not a prefix of the context.
328            if !_context.starts_with(context) {
329                continue;
330            }
331            // Skip if we already have a closer prefix match.
332            if prefix.len() < longest_prefix && context.len() == longest_context {
333                continue;
334            }
335            // Skip if the prefix does not match.
336            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    /// Loads stdin into the source map.
354    pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
355        self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
356    }
357
358    /// Returns the source file with the given path, if it exists, without loading it.
359    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    /// Loads `path` into the source map. Returns `None` if the file doesn't exist.
376    #[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        // Make the path absolute before normalizing so leading `..` components are resolved
392        // against the current directory instead of being discarded from a relative path.
393        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        // Canonicalize, checking symlinks and if it exists.
403        if load && let Ok(path) = self.canonicalize_unchecked(rpath) {
404            return self
405                .source_map()
406                // Store the file with `rpath` as the name instead of `path`.
407                // In case of symlinks we want to reference the symlink path, not the target path.
408                .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    // TODO: Equivalent of: `boost::filesystem::path(_path).generic_string()`
420    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        // `<import string> => <resolved path>`
434        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    // Taken from: https://github.com/argotorg/solidity/blob/32c8f080c4cc939df5a3c7ca5ad6b6144ee9aa66/test/libsolidity/Imports.cpp
496    #[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// ported-from: https://github.com/BenTheKush/solc_remapping_behavior_test
660#[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}