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 solar_config::ImportRemapping;
10use std::{
11    borrow::Cow,
12    io,
13    path::{Path, PathBuf},
14    sync::Arc,
15};
16
17/// An error that occurred while resolving a path.
18#[derive(Debug, thiserror::Error)]
19pub enum ResolveError {
20    #[error("couldn't read stdin: {0}")]
21    ReadStdin(#[source] io::Error),
22    #[error("couldn't read {0}: {1}")]
23    ReadFile(PathBuf, #[source] io::Error),
24    #[error("file {0} not found")]
25    NotFound(PathBuf),
26    #[error("multiple files match {}: {}", .0.display(), .1.iter().map(|f| f.name.display()).format(", "))]
27    MultipleMatches(PathBuf, Vec<Arc<SourceFile>>),
28}
29
30/// Performs file resolution by applying import paths and mappings.
31#[derive(derive_more::Debug)]
32pub struct FileResolver<'a> {
33    #[debug(skip)]
34    source_map: &'a SourceMap,
35
36    /// Include paths.
37    include_paths: Vec<PathBuf>,
38    /// Import remappings.
39    remappings: Vec<ImportRemapping>,
40
41    /// [`std::env::current_dir`] cache. Unused if the current directory is set manually.
42    env_current_dir: Option<PathBuf>,
43    /// Custom current directory.
44    custom_current_dir: Option<PathBuf>,
45}
46
47impl<'a> FileResolver<'a> {
48    /// Creates a new file resolver.
49    pub fn new(source_map: &'a SourceMap) -> Self {
50        Self {
51            source_map,
52            include_paths: Vec::new(),
53            remappings: Vec::new(),
54            env_current_dir: std::env::current_dir()
55                .inspect_err(|e| debug!("failed to get current_dir: {e}"))
56                .ok(),
57            custom_current_dir: None,
58        }
59    }
60
61    /// Sets the current directory.
62    ///
63    /// # Panics
64    ///
65    /// Panics if `current_dir` is not an absolute path.
66    #[track_caller]
67    pub fn set_current_dir(&mut self, current_dir: &Path) {
68        if !current_dir.is_absolute() {
69            panic!("current_dir must be an absolute path");
70        }
71        self.custom_current_dir = Some(current_dir.to_path_buf());
72    }
73
74    /// Adds include paths.
75    pub fn add_include_paths(&mut self, paths: impl IntoIterator<Item = PathBuf>) {
76        self.include_paths.extend(paths);
77    }
78
79    /// Adds an include path.
80    pub fn add_include_path(&mut self, path: PathBuf) {
81        self.include_paths.push(path)
82    }
83
84    /// Adds import remappings.
85    pub fn add_import_remappings(&mut self, remappings: impl IntoIterator<Item = ImportRemapping>) {
86        self.remappings.extend(remappings);
87    }
88
89    /// Adds an import remapping.
90    pub fn add_import_remapping(&mut self, remapping: ImportRemapping) {
91        self.remappings.push(remapping);
92    }
93
94    /// Returns the source map.
95    pub fn source_map(&self) -> &'a SourceMap {
96        self.source_map
97    }
98
99    /// Returns the current directory, or `.` if it could not be resolved.
100    pub fn current_dir(&self) -> &Path {
101        self.try_current_dir().unwrap_or(Path::new("."))
102    }
103
104    /// Returns the current directory, if resolved successfully.
105    pub fn try_current_dir(&self) -> Option<&Path> {
106        self.custom_current_dir.as_deref().or(self.env_current_dir.as_deref())
107    }
108
109    /// Canonicalizes a path using [`Self::current_dir`].
110    pub fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
111        self.canonicalize_absolute(&self.make_absolute(path))
112    }
113
114    fn canonicalize_absolute(&self, path: &Path) -> io::Result<PathBuf> {
115        debug_assert!(path.is_absolute());
116        crate::canonicalize(path)
117    }
118
119    /// Normalizes a path removing unnecessary components.
120    ///
121    /// Does not perform I/O.
122    pub fn normalize<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
123        Cow::Owned(path.normalize())
124    }
125
126    /// Makes the path absolute by joining it with the current directory.
127    ///
128    /// Does not perform I/O.
129    pub fn make_absolute<'b>(&self, path: &'b Path) -> Cow<'b, Path> {
130        if path.is_absolute() {
131            Cow::Borrowed(path)
132        } else if let Some(current_dir) = self.try_current_dir() {
133            Cow::Owned(current_dir.join(path))
134        } else {
135            Cow::Borrowed(path)
136        }
137    }
138
139    /// Resolves an import path.
140    ///
141    /// `parent` is the path of the file that contains the import, if any.
142    #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
143    pub fn resolve_file(
144        &self,
145        path: &Path,
146        parent: Option<&Path>,
147    ) -> Result<Arc<SourceFile>, ResolveError> {
148        // https://docs.soliditylang.org/en/latest/path-resolution.html
149        // Only when the path starts with ./ or ../ are relative paths considered; this means
150        // that `import "b.sol";` will check the import paths for b.sol, while `import "./b.sol";`
151        // will only check the path relative to the current file.
152        //
153        // `parent.is_none()` only happens when resolving imports from a custom/stdin file, or when
154        // manually resolving a file, like from CLI arguments. In these cases, the file is
155        // considered to be in the current directory.
156        // Technically, this behavior allows the latter, the manual case, to also be resolved using
157        // remappings, which is not the case in solc, but this simplifies the implementation.
158        let is_relative = path.starts_with("./") || path.starts_with("../");
159        if (is_relative && parent.is_some()) || parent.is_none() {
160            let try_path = if let Some(base) = parent.filter(|_| is_relative).and_then(Path::parent)
161            {
162                &base.join(path)
163            } else {
164                path
165            };
166            if let Some(file) = self.try_file(try_path)? {
167                return Ok(file);
168            }
169            // See above.
170            if is_relative {
171                return Err(ResolveError::NotFound(path.into()));
172            }
173        }
174
175        let original_path = path;
176        let path = &*self.remap_path(path, parent);
177        let mut result = Vec::with_capacity(1);
178        // Quick deduplication when include paths are duplicated.
179        let mut push_file = |file: Arc<SourceFile>| {
180            if !result.iter().any(|f| Arc::ptr_eq(f, &file)) {
181                result.push(file);
182            }
183        };
184
185        // If there are no include paths, then try the file directly. See
186        // https://docs.soliditylang.org/en/latest/path-resolution.html#base-path-and-include-paths
187        // "By default the base path is empty, which leaves the source unit name unchanged."
188        if self.include_paths.is_empty() || path.is_absolute() {
189            if let Some(file) = self.try_file(path)? {
190                result.push(file);
191            }
192        } else {
193            // Try all the import paths.
194            for include_path in &self.include_paths {
195                let path = include_path.join(path);
196                if let Some(file) = self.try_file(&path)? {
197                    push_file(file);
198                }
199            }
200        }
201
202        match result.len() {
203            0 => Err(ResolveError::NotFound(original_path.into())),
204            1 => Ok(result.pop().unwrap()),
205            _ => Err(ResolveError::MultipleMatches(original_path.into(), result)),
206        }
207    }
208
209    /// Applies the import path mappings to `path`.
210    // Reference: <https://github.com/ethereum/solidity/blob/e202d30db8e7e4211ee973237ecbe485048aae97/libsolidity/interface/ImportRemapper.cpp#L32>
211    pub fn remap_path<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
212        let remapped = self.remap_path_(path, parent);
213        if remapped != path {
214            trace!(remapped=%remapped.display());
215        }
216        remapped
217    }
218
219    fn remap_path_<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
220        let _context = &*parent.map(|p| p.to_string_lossy()).unwrap_or_default();
221
222        let mut longest_prefix = 0;
223        let mut longest_context = 0;
224        let mut best_match_target = None;
225        let mut unprefixed_path = path;
226        for ImportRemapping { context, prefix, path: target } in &self.remappings {
227            let context = &*sanitize_path(context);
228            let prefix = &*sanitize_path(prefix);
229
230            if context.len() < longest_context {
231                continue;
232            }
233            if !_context.starts_with(context) {
234                continue;
235            }
236            if prefix.len() < longest_prefix {
237                continue;
238            }
239            let Ok(up) = path.strip_prefix(prefix) else {
240                continue;
241            };
242            longest_context = context.len();
243            longest_prefix = prefix.len();
244            best_match_target = Some(sanitize_path(target));
245            unprefixed_path = up;
246        }
247        if let Some(best_match_target) = best_match_target {
248            let mut out = PathBuf::from(&*best_match_target);
249            out.push(unprefixed_path);
250            Cow::Owned(out)
251        } else {
252            Cow::Borrowed(unprefixed_path)
253        }
254    }
255
256    /// Loads stdin into the source map.
257    pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
258        self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
259    }
260
261    /// Loads `path` into the source map. Returns `None` if the file doesn't exist.
262    #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
263    pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
264        // Normalize unnecessary components.
265        let rpath = &*self.normalize(path);
266        if let Some(file) = self.source_map().get_file(rpath) {
267            trace!("loaded from cache 1");
268            return Ok(Some(file));
269        }
270
271        // Make the path absolute with the current directory.
272        // Normally this is only needed when paths are inserted manually outside of the resolver, as
273        // we always try to strip the prefix of the current directory (see below).
274        let apath = &*self.make_absolute(rpath);
275        if apath != rpath
276            && let Some(file) = self.source_map().get_file(apath)
277        {
278            trace!("loaded from cache 2");
279            return Ok(Some(file));
280        }
281
282        // Canonicalize, checking symlinks and if it exists.
283        if let Ok(path) = self.canonicalize_absolute(apath) {
284            // Save the file name relative to the current directory.
285            let mut relpath = path.as_path();
286            if let Ok(p) = relpath.strip_prefix(self.current_dir()) {
287                relpath = p;
288            }
289            trace!("canonicalized to {}", relpath.display());
290            return self
291                .source_map()
292                // Can't use `load_file` with `rel_path` as a custom `current_dir` may be set.
293                .load_file_with_name(relpath.to_path_buf().into(), &path)
294                .map(Some)
295                .map_err(|e| ResolveError::ReadFile(relpath.into(), e));
296        }
297
298        trace!("not found");
299        Ok(None)
300    }
301}
302
303fn sanitize_path(s: &str) -> impl std::ops::Deref<Target = str> + '_ {
304    // TODO: Equivalent of: `boost::filesystem::path(_path).generic_string()`
305    s
306}