Skip to main content

foundry_compilers_artifacts_solc/remappings/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    fmt,
4    path::{Path, PathBuf},
5    str::FromStr,
6};
7
8#[cfg(feature = "walkdir")]
9mod find;
10
11/// The solidity compiler can only reference files that exist locally on your computer.
12/// So importing directly from GitHub (as an example) is not possible.
13///
14/// Let's imagine you want to use OpenZeppelin's amazing library of smart contracts,
15/// `@openzeppelin/contracts-ethereum-package`:
16///
17/// ```ignore
18/// pragma solidity 0.5.11;
19///
20/// import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
21///
22/// contract MyContract {
23///     using SafeMath for uint256;
24///     ...
25/// }
26/// ```
27///
28/// When using `solc`, you have to specify the following:
29///
30/// - A `prefix`: the path that's used in your smart contract, i.e.
31///   `@openzeppelin/contracts-ethereum-package`
32/// - A `target`: the absolute path of the downloaded contracts on your computer
33///
34/// The format looks like this: `solc prefix=target ./MyContract.sol`
35///
36/// For example:
37///
38/// ```text
39/// solc --bin \
40///     @openzeppelin/contracts-ethereum-package=/Your/Absolute/Path/To/@openzeppelin/contracts-ethereum-package \
41///     ./MyContract.sol
42/// ```
43///
44/// You can also specify a `context` which limits the scope of the remapping to a subset of your
45/// project. This allows you to apply the remapping only to imports located in a specific library or
46/// a specific file. Without a context a remapping is applied to every matching import in all files.
47///
48/// The format is: `solc context:prefix=target ./MyContract.sol`
49///
50/// [Source](https://ethereum.stackexchange.com/questions/74448/what-are-remappings-and-how-do-they-work-in-solidity)
51#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
52pub struct Remapping {
53    pub context: Option<String>,
54    pub name: String,
55    pub path: String,
56}
57
58impl Remapping {
59    /// Convenience function for [`RelativeRemapping::new`]
60    pub fn into_relative(self, root: &Path) -> RelativeRemapping {
61        RelativeRemapping::new(self, root)
62    }
63
64    /// Removes the `base` path from the remapping
65    pub fn strip_prefix(&mut self, base: &Path) -> &mut Self {
66        if let Ok(stripped) = Path::new(&self.path).strip_prefix(base) {
67            self.path = stripped.display().to_string();
68        }
69        self
70    }
71}
72
73#[derive(Debug, PartialEq, Eq, PartialOrd, thiserror::Error)]
74pub enum RemappingError {
75    #[error("invalid remapping format, found `{0}`, expected `<key>=<value>`")]
76    InvalidRemapping(String),
77    #[error("remapping key can't be empty, found `{0}`, expected `<key>=<value>`")]
78    EmptyRemappingKey(String),
79    #[error("remapping value must be a path, found `{0}`, expected `<key>=<value>`")]
80    EmptyRemappingValue(String),
81}
82
83impl FromStr for Remapping {
84    type Err = RemappingError;
85
86    fn from_str(remapping: &str) -> Result<Self, Self::Err> {
87        let (name, path) = remapping
88            .split_once('=')
89            .ok_or_else(|| RemappingError::InvalidRemapping(remapping.to_string()))?;
90        let (context, name) = name
91            .split_once(':')
92            .map_or((None, name), |(context, name)| (Some(context.to_string()), name));
93        if name.trim().is_empty() {
94            return Err(RemappingError::EmptyRemappingKey(remapping.to_string()));
95        }
96        if path.trim().is_empty() {
97            return Err(RemappingError::EmptyRemappingValue(remapping.to_string()));
98        }
99        // if the remapping just starts with : (no context name), treat it as global
100        let context = context.filter(|c| !c.trim().is_empty());
101        Ok(Self { context, name: name.to_string(), path: path.to_string() })
102    }
103}
104
105impl Serialize for Remapping {
106    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
107    where
108        S: serde::ser::Serializer,
109    {
110        serializer.serialize_str(&self.to_string())
111    }
112}
113
114impl<'de> Deserialize<'de> for Remapping {
115    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
116    where
117        D: serde::de::Deserializer<'de>,
118    {
119        let remapping = String::deserialize(deserializer)?;
120        Self::from_str(&remapping).map_err(serde::de::Error::custom)
121    }
122}
123
124// Remappings are printed as `prefix=target`
125impl fmt::Display for Remapping {
126    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127        let mut s = String::new();
128        if let Some(context) = self.context.as_ref() {
129            #[cfg(target_os = "windows")]
130            {
131                // ensure we have `/` slashes on windows
132                use path_slash::PathExt;
133                s.push_str(&std::path::Path::new(context).to_slash_lossy());
134            }
135            #[cfg(not(target_os = "windows"))]
136            {
137                s.push_str(context);
138            }
139            s.push(':');
140        }
141        let name = if needs_trailing_slash(&self.name) {
142            format!("{}/", self.name)
143        } else {
144            self.name.clone()
145        };
146        s.push_str(&{
147            #[cfg(target_os = "windows")]
148            {
149                // ensure we have `/` slashes on windows
150                use path_slash::PathExt;
151                format!("{}={}", name, std::path::Path::new(&self.path).to_slash_lossy())
152            }
153            #[cfg(not(target_os = "windows"))]
154            {
155                format!("{}={}", name, self.path)
156            }
157        });
158
159        if needs_trailing_slash(&s) {
160            s.push('/');
161        }
162        f.write_str(&s)
163    }
164}
165
166impl Remapping {
167    /// Converts any `\\` separators in the `path` to `/`.
168    #[allow(clippy::missing_const_for_fn)]
169    pub fn slash_path(&mut self) {
170        #[cfg(windows)]
171        {
172            use path_slash::PathExt;
173            self.path = Path::new(&self.path).to_slash_lossy().to_string();
174            if let Some(context) = self.context.as_mut() {
175                *context = Path::new(&context).to_slash_lossy().to_string();
176            }
177        }
178    }
179}
180
181/// A relative [`Remapping`] that's aware of the current location
182///
183/// See [`RelativeRemappingPathBuf`]
184#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
185pub struct RelativeRemapping {
186    pub context: Option<String>,
187    pub name: String,
188    pub path: RelativeRemappingPathBuf,
189}
190
191impl RelativeRemapping {
192    /// Creates a new `RelativeRemapping` starting prefixed with `root`
193    pub fn new(remapping: Remapping, root: &Path) -> Self {
194        Self {
195            context: remapping.context.map(|c| {
196                RelativeRemappingPathBuf::with_root(root, c).path.to_string_lossy().to_string()
197            }),
198            name: remapping.name,
199            path: RelativeRemappingPathBuf::with_root(root, remapping.path),
200        }
201    }
202
203    /// Converts this relative remapping into an absolute remapping
204    ///
205    /// This sets to root of the remapping to the given `root` path
206    pub fn to_remapping(mut self, root: PathBuf) -> Remapping {
207        self.path.parent = Some(root);
208        self.into()
209    }
210
211    /// Converts this relative remapping into [`Remapping`] without the root path
212    pub fn to_relative_remapping(mut self) -> Remapping {
213        self.path.parent.take();
214        self.into()
215    }
216}
217
218// Remappings are printed as `prefix=target`
219impl fmt::Display for RelativeRemapping {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        let mut s = String::new();
222        if let Some(context) = self.context.as_ref() {
223            #[cfg(target_os = "windows")]
224            {
225                // ensure we have `/` slashes on windows
226                use path_slash::PathExt;
227                s.push_str(&std::path::Path::new(context).to_slash_lossy());
228            }
229            #[cfg(not(target_os = "windows"))]
230            {
231                s.push_str(context);
232            }
233            s.push(':');
234        }
235        s.push_str(&{
236            #[cfg(target_os = "windows")]
237            {
238                // ensure we have `/` slashes on windows
239                use path_slash::PathExt;
240                format!("{}={}", self.name, self.path.original().to_slash_lossy())
241            }
242            #[cfg(not(target_os = "windows"))]
243            {
244                format!("{}={}", self.name, self.path.original().display())
245            }
246        });
247
248        if needs_trailing_slash(&s) {
249            s.push('/');
250        }
251        f.write_str(&s)
252    }
253}
254
255impl From<RelativeRemapping> for Remapping {
256    fn from(r: RelativeRemapping) -> Self {
257        let RelativeRemapping { context, mut name, path } = r;
258        let mut path = path.relative().display().to_string();
259        if needs_trailing_slash(&path) {
260            path.push('/');
261        }
262        if needs_trailing_slash(&name) {
263            name.push('/');
264        }
265        Self { context, name, path }
266    }
267}
268
269impl From<Remapping> for RelativeRemapping {
270    fn from(r: Remapping) -> Self {
271        Self { context: r.context, name: r.name, path: r.path.into() }
272    }
273}
274
275/// The path part of the [`Remapping`] that knows the path of the file it was configured in, if any.
276///
277/// A [`Remapping`] is intended to be absolute, but paths in configuration files are often desired
278/// to be relative to the configuration file itself. For example, a path of
279/// `weird-erc20/=lib/weird-erc20/src/` configured in a file `/var/foundry.toml` might be desired to
280/// resolve as a `weird-erc20/=/var/lib/weird-erc20/src/` remapping.
281#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
282pub struct RelativeRemappingPathBuf {
283    pub parent: Option<PathBuf>,
284    pub path: PathBuf,
285}
286
287impl RelativeRemappingPathBuf {
288    /// Creates a new `RelativeRemappingPathBuf` that checks if the `path` is a child path of
289    /// `parent`.
290    pub fn with_root(
291        parent: impl AsRef<Path> + Into<PathBuf>,
292        path: impl AsRef<Path> + Into<PathBuf>,
293    ) -> Self {
294        if let Ok(path) = path.as_ref().strip_prefix(parent.as_ref()) {
295            Self { parent: Some(parent.into()), path: path.to_path_buf() }
296        } else if path.as_ref().has_root() {
297            Self { parent: None, path: path.into() }
298        } else {
299            Self { parent: Some(parent.into()), path: path.into() }
300        }
301    }
302
303    /// Returns the path as it was declared, without modification.
304    pub fn original(&self) -> &Path {
305        &self.path
306    }
307
308    /// Returns this path relative to the file it was declared in, if any.
309    /// Returns the original if this path was not declared in a file or if the
310    /// path has a root.
311    pub fn relative(&self) -> PathBuf {
312        if self.original().has_root() {
313            return self.original().into();
314        }
315        self.parent
316            .as_ref()
317            .map(|p| p.join(self.original()))
318            .unwrap_or_else(|| self.original().into())
319    }
320}
321
322impl<P: Into<PathBuf>> From<P> for RelativeRemappingPathBuf {
323    fn from(path: P) -> Self {
324        Self { parent: None, path: path.into() }
325    }
326}
327
328impl Serialize for RelativeRemapping {
329    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
330    where
331        S: serde::ser::Serializer,
332    {
333        serializer.serialize_str(&self.to_string())
334    }
335}
336
337impl<'de> Deserialize<'de> for RelativeRemapping {
338    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
339    where
340        D: serde::de::Deserializer<'de>,
341    {
342        let remapping = String::deserialize(deserializer)?;
343        let remapping = Remapping::from_str(&remapping).map_err(serde::de::Error::custom)?;
344        Ok(Self { context: remapping.context, name: remapping.name, path: remapping.path.into() })
345    }
346}
347
348/// Helper to determine if name or path of a remapping needs trailing slash.
349/// Returns false if it already ends with a slash or if remapping is a solidity file.
350/// Used to preserve name and path of single file remapping, see
351/// <https://github.com/foundry-rs/foundry/issues/6706>
352/// <https://github.com/foundry-rs/foundry/issues/8499>
353fn needs_trailing_slash(name_or_path: &str) -> bool {
354    !name_or_path.ends_with('/') && !name_or_path.ends_with(".sol")
355}
356
357#[cfg(test)]
358mod tests {
359    pub use super::*;
360
361    #[test]
362    fn relative_remapping() {
363        let remapping = "oz=a/b/c/d";
364        let remapping = Remapping::from_str(remapping).unwrap();
365
366        let relative = RelativeRemapping::new(remapping.clone(), Path::new("a/b/c"));
367        assert_eq!(relative.path.relative(), Path::new(&remapping.path));
368        assert_eq!(relative.path.original(), Path::new("d"));
369
370        let relative = RelativeRemapping::new(remapping.clone(), Path::new("x/y"));
371        assert_eq!(relative.path.relative(), Path::new("x/y/a/b/c/d"));
372        assert_eq!(relative.path.original(), Path::new(&remapping.path));
373
374        let remapping = "oz=/a/b/c/d";
375        let remapping = Remapping::from_str(remapping).unwrap();
376        let relative = RelativeRemapping::new(remapping.clone(), Path::new("a/b"));
377        assert_eq!(relative.path.relative(), Path::new(&remapping.path));
378        assert_eq!(relative.path.original(), Path::new(&remapping.path));
379        assert!(relative.path.parent.is_none());
380
381        let relative = RelativeRemapping::new(remapping, Path::new("/a/b"));
382        assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap());
383    }
384
385    #[test]
386    fn remapping_errors() {
387        let remapping = "oz=../b/c/d";
388        let remapping = Remapping::from_str(remapping).unwrap();
389        assert_eq!(remapping.name, "oz".to_string());
390        assert_eq!(remapping.path, "../b/c/d".to_string());
391
392        let err = Remapping::from_str("").unwrap_err();
393        matches!(err, RemappingError::InvalidRemapping(_));
394
395        let err = Remapping::from_str("oz=").unwrap_err();
396        matches!(err, RemappingError::EmptyRemappingValue(_));
397    }
398
399    #[test]
400    fn can_resolve_contexts() {
401        let remapping = "context:oz=a/b/c/d";
402        let remapping = Remapping::from_str(remapping).unwrap();
403
404        assert_eq!(
405            remapping,
406            Remapping {
407                context: Some("context".to_string()),
408                name: "oz".to_string(),
409                path: "a/b/c/d".to_string(),
410            }
411        );
412        assert_eq!(remapping.to_string(), "context:oz/=a/b/c/d/".to_string());
413
414        let remapping = "context:foo=C:/bar/src/";
415        let remapping = Remapping::from_str(remapping).unwrap();
416
417        assert_eq!(
418            remapping,
419            Remapping {
420                context: Some("context".to_string()),
421                name: "foo".to_string(),
422                path: "C:/bar/src/".to_string()
423            }
424        );
425    }
426
427    #[test]
428    fn can_resolve_global_contexts() {
429        let remapping = ":oz=a/b/c/d/";
430        let remapping = Remapping::from_str(remapping).unwrap();
431
432        assert_eq!(
433            remapping,
434            Remapping { context: None, name: "oz".to_string(), path: "a/b/c/d/".to_string() }
435        );
436        assert_eq!(remapping.to_string(), "oz/=a/b/c/d/".to_string());
437    }
438
439    // <https://github.com/foundry-rs/foundry/issues/6706#issuecomment-3141270852>
440    #[test]
441    fn can_preserve_single_sol_file_remapping() {
442        let remapping = "@my-lib/B.sol=lib/my-lib/B.sol";
443        let remapping = Remapping::from_str(remapping).unwrap();
444
445        assert_eq!(
446            remapping,
447            Remapping {
448                context: None,
449                name: "@my-lib/B.sol".to_string(),
450                path: "lib/my-lib/B.sol".to_string()
451            }
452        );
453        assert_eq!(remapping.to_string(), "@my-lib/B.sol=lib/my-lib/B.sol".to_string());
454    }
455}