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.
100    pub fn current_dir(&self) -> &Path {
101        self.custom_current_dir
102            .as_deref()
103            .or(self.env_current_dir.as_deref())
104            .unwrap_or(Path::new("."))
105    }
106
107    /// Canonicalizes a path using [`Self::current_dir`].
108    pub fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
109        let path = if path.is_absolute() {
110            path
111        } else if let Some(current_dir) = &self.custom_current_dir {
112            &current_dir.join(path)
113        } else {
114            path
115        };
116        crate::canonicalize(path)
117    }
118
119    /// Resolves an import path.
120    ///
121    /// `parent` is the path of the file that contains the import, if any.
122    #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
123    pub fn resolve_file(
124        &self,
125        path: &Path,
126        parent: Option<&Path>,
127    ) -> Result<Arc<SourceFile>, ResolveError> {
128        // https://docs.soliditylang.org/en/latest/path-resolution.html
129        // Only when the path starts with ./ or ../ are relative paths considered; this means
130        // that `import "b.sol";` will check the import paths for b.sol, while `import "./b.sol";`
131        // will only check the path relative to the current file.
132        //
133        // `parent.is_none()` only happens when resolving imports from a custom/stdin file, or when
134        // manually resolving a file, like from CLI arguments. In these cases, the file is
135        // considered to be in the current directory.
136        // Technically, this behavior allows the latter, the manual case, to also be resolved using
137        // remappings, which is not the case in solc, but this simplifies the implementation.
138        let is_relative = path.starts_with("./") || path.starts_with("../");
139        if (is_relative && parent.is_some()) || parent.is_none() {
140            let try_path = if let Some(base) = parent.filter(|_| is_relative).and_then(Path::parent)
141            {
142                &base.join(path)
143            } else {
144                path
145            };
146            if let Some(file) = self.try_file(try_path)? {
147                return Ok(file);
148            }
149            // See above.
150            if is_relative {
151                return Err(ResolveError::NotFound(path.into()));
152            }
153        }
154
155        let original_path = path;
156        let path = &*self.remap_path(path, parent);
157        let mut result = Vec::with_capacity(1);
158        // Quick deduplication when include paths are duplicated.
159        let mut push_file = |file: Arc<SourceFile>| {
160            if !result.iter().any(|f| Arc::ptr_eq(f, &file)) {
161                result.push(file);
162            }
163        };
164
165        // If there are no include paths, then try the file directly. See
166        // https://docs.soliditylang.org/en/latest/path-resolution.html#base-path-and-include-paths
167        // "By default the base path is empty, which leaves the source unit name unchanged."
168        if self.include_paths.is_empty() || path.is_absolute() {
169            if let Some(file) = self.try_file(path)? {
170                result.push(file);
171            }
172        } else {
173            // Try all the import paths.
174            for include_path in &self.include_paths {
175                let path = include_path.join(path);
176                if let Some(file) = self.try_file(&path)? {
177                    push_file(file);
178                }
179            }
180        }
181
182        match result.len() {
183            0 => Err(ResolveError::NotFound(original_path.into())),
184            1 => Ok(result.pop().unwrap()),
185            _ => Err(ResolveError::MultipleMatches(original_path.into(), result)),
186        }
187    }
188
189    /// Applies the import path mappings to `path`.
190    // Reference: <https://github.com/ethereum/solidity/blob/e202d30db8e7e4211ee973237ecbe485048aae97/libsolidity/interface/ImportRemapper.cpp#L32>
191    #[instrument(level = "trace", skip_all, ret)]
192    pub fn remap_path<'b>(&self, path: &'b Path, parent: Option<&Path>) -> Cow<'b, Path> {
193        let _context = &*parent.map(|p| p.to_string_lossy()).unwrap_or_default();
194
195        let mut longest_prefix = 0;
196        let mut longest_context = 0;
197        let mut best_match_target = None;
198        let mut unprefixed_path = path;
199        for ImportRemapping { context, prefix, path: target } in &self.remappings {
200            let context = &*sanitize_path(context);
201            let prefix = &*sanitize_path(prefix);
202
203            if context.len() < longest_context {
204                continue;
205            }
206            if !_context.starts_with(context) {
207                continue;
208            }
209            if prefix.len() < longest_prefix {
210                continue;
211            }
212            let Ok(up) = path.strip_prefix(prefix) else {
213                continue;
214            };
215            longest_context = context.len();
216            longest_prefix = prefix.len();
217            best_match_target = Some(sanitize_path(target));
218            unprefixed_path = up;
219        }
220        if let Some(best_match_target) = best_match_target {
221            let mut out = PathBuf::from(&*best_match_target);
222            out.push(unprefixed_path);
223            out.into()
224        } else {
225            Cow::Borrowed(unprefixed_path)
226        }
227    }
228
229    /// Loads stdin into the source map.
230    pub fn load_stdin(&self) -> Result<Arc<SourceFile>, ResolveError> {
231        self.source_map().load_stdin().map_err(ResolveError::ReadStdin)
232    }
233
234    /// Loads `path` into the source map. Returns `None` if the file doesn't exist.
235    #[instrument(level = "debug", skip_all, fields(path = %path.display()))]
236    pub fn try_file(&self, path: &Path) -> Result<Option<Arc<SourceFile>>, ResolveError> {
237        let path = &*path.normalize();
238        if let Some(file) = self.source_map().get_file(path) {
239            trace!("loaded from cache");
240            return Ok(Some(file));
241        }
242
243        if let Ok(path) = self.canonicalize(path) {
244            // Save the file name relative to the current directory.
245            let mut relpath = path.as_path();
246            if let Ok(p) = relpath.strip_prefix(self.current_dir()) {
247                relpath = p;
248            }
249            trace!("canonicalized to {}", relpath.display());
250            return self
251                .source_map()
252                // Can't use `load_file` with `rel_path` as a custom `current_dir` may be set.
253                .load_file_with_name(relpath.to_path_buf().into(), &path)
254                .map(Some)
255                .map_err(|e| ResolveError::ReadFile(relpath.into(), e));
256        }
257
258        trace!("not found");
259        Ok(None)
260    }
261}
262
263fn sanitize_path(s: &str) -> impl std::ops::Deref<Target = str> + '_ {
264    // TODO: Equivalent of: `boost::filesystem::path(_path).generic_string()`
265    s
266}