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::SourceMap;
7use itertools::Itertools;
8use normalize_path::NormalizePath;
9use std::{
10    borrow::Cow,
11    io,
12    path::{Path, PathBuf},
13    sync::Arc,
14};
15
16/// An error that occurred while resolving a path.
17#[derive(Debug, thiserror::Error)]
18pub enum ResolveError {
19    #[error("couldn't read stdin: {0}")]
20    ReadStdin(#[source] io::Error),
21    #[error("couldn't read {0}: {1}")]
22    ReadFile(PathBuf, #[source] io::Error),
23    #[error("file {0} not found")]
24    NotFound(PathBuf),
25    #[error("multiple files match {}: {}", .0.display(), .1.iter().map(|f| f.name.display()).format(", "))]
26    MultipleMatches(PathBuf, Vec<Arc<SourceFile>>),
27}
28
29pub struct FileResolver<'a> {
30    source_map: &'a SourceMap,
31    import_paths: Vec<(Option<PathBuf>, PathBuf)>,
32}
33
34impl<'a> FileResolver<'a> {
35    /// Creates a new file resolver.
36    pub fn new(source_map: &'a SourceMap) -> Self {
37        Self { source_map, import_paths: Vec::new() }
38    }
39
40    /// Returns the source map.
41    pub fn source_map(&self) -> &'a SourceMap {
42        self.source_map
43    }
44
45    /// Adds an import path. Returns `true` if the path is newly inserted.
46    pub fn add_import_path(&mut self, path: PathBuf) -> bool {
47        let entry = (None, path);
48        let new = !self.import_paths.contains(&entry);
49        if new {
50            self.import_paths.push(entry);
51        }
52        new
53    }
54
55    /// Adds an import map.
56    pub fn add_import_map(&mut self, map: PathBuf, path: PathBuf) {
57        let map = Some(map);
58        if let Some((_, e)) = self.import_paths.iter_mut().find(|(k, _)| *k == map) {
59            *e = path;
60        } else {
61            self.import_paths.push((map, path));
62        }
63    }
64
65    /// Get the import path and the optional mapping corresponding to `import_no`.
66    pub fn get_import_path(&self, import_no: usize) -> Option<&(Option<PathBuf>, PathBuf)> {
67        self.import_paths.get(import_no)
68    }
69
70    /// Get the import paths
71    pub fn get_import_paths(&self) -> &[(Option<PathBuf>, PathBuf)] {
72        self.import_paths.as_slice()
73    }
74
75    /// Get the import path corresponding to a map
76    pub fn get_import_map(&self, map: &Path) -> Option<&PathBuf> {
77        self.import_paths.iter().find(|(m, _)| m.as_deref() == Some(map)).map(|(_, pb)| pb)
78    }
79
80    /// Resolves an import path. `parent` is the path of the file that contains the import, if any.
81    #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
82    pub fn resolve_file(
83        &self,
84        path: &Path,
85        parent: Option<&Path>,
86    ) -> Result<Arc<SourceFile>, ResolveError> {
87        // https://docs.soliditylang.org/en/latest/path-resolution.html
88        // Only when the path starts with ./ or ../ are relative paths considered; this means
89        // that `import "b.sol";` will check the import paths for b.sol, while `import "./b.sol";`
90        // will only the path relative to the current file.
91        if path.starts_with("./") || path.starts_with("../") {
92            if let Some(parent) = parent {
93                let base = parent.parent().unwrap_or(Path::new("."));
94                let path = base.join(path);
95                if let Some(file) = self.try_file(&path)? {
96                    // No ambiguity possible, so just return
97                    return Ok(file);
98                }
99            }
100
101            return Err(ResolveError::NotFound(path.into()));
102        }
103
104        if parent.is_none() {
105            if let Some(file) = self.try_file(path)? {
106                return Ok(file);
107            }
108            if path.is_absolute() {
109                return Err(ResolveError::NotFound(path.into()));
110            }
111        }
112
113        let original_path = path;
114        let path = self.remap_path(path);
115        let mut result = Vec::with_capacity(1);
116
117        // Walk over the import paths until we find one that resolves.
118        for import in &self.import_paths {
119            if let (None, import_path) = import {
120                let path = import_path.join(&path);
121                if let Some(file) = self.try_file(&path)? {
122                    result.push(file);
123                }
124            }
125        }
126
127        // If there was no defined import path, then try the file directly. See
128        // https://docs.soliditylang.org/en/latest/path-resolution.html#base-path-and-include-paths
129        // "By default the base path is empty, which leaves the source unit name unchanged."
130        if !self.import_paths.iter().any(|(m, _)| m.is_none()) {
131            if let Some(file) = self.try_file(&path)? {
132                result.push(file);
133            }
134        }
135
136        match result.len() {
137            0 => Err(ResolveError::NotFound(original_path.into())),
138            1 => Ok(result.pop().unwrap()),
139            _ => Err(ResolveError::MultipleMatches(original_path.into(), result)),
140        }
141    }
142
143    /// Applies the import path mappings to `path`.
144    #[instrument(level = "trace", skip_all, ret)]
145    pub fn remap_path<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
146        let orig = path;
147        let mut remapped = Cow::Borrowed(path);
148        for import_path in &self.import_paths {
149            if let (Some(mapping), target) = import_path {
150                if let Ok(relpath) = orig.strip_prefix(mapping) {
151                    remapped = Cow::Owned(target.join(relpath));
152                }
153            }
154        }
155        remapped
156    }
157
158    /// Loads stdin into the source map.
159    pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
160        self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
161    }
162
163    /// Loads `path` into the source map. Returns `None` if the file doesn't exist.
164    #[instrument(level = "debug", skip_all)]
165    pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
166        let cache_path = path.normalize();
167        if let Ok(file) = self.source_map().load_file(&cache_path) {
168            trace!("loaded from cache");
169            return Ok(Some(file));
170        }
171
172        if let Ok(path) = crate::canonicalize(path) {
173            // TODO: avoids loading the same file twice by canonicalizing,
174            // and then not displaying the full path in the error message
175            let mut path = path.as_path();
176            if let Ok(curdir) = std::env::current_dir() {
177                if let Ok(p) = path.strip_prefix(curdir) {
178                    path = p;
179                }
180            }
181            trace!("canonicalized to {}", path.display());
182            return self
183                .source_map()
184                .load_file(path)
185                .map(Some)
186                .map_err(|e| ResolveError::ReadFile(path.into(), e));
187        }
188
189        trace!("not found");
190        Ok(None)
191    }
192}