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    pub fn slash_path(&mut self) {
169        #[cfg(windows)]
170        {
171            use path_slash::PathExt;
172            self.path = Path::new(&self.path).to_slash_lossy().to_string();
173            if let Some(context) = self.context.as_mut() {
174                *context = Path::new(&context).to_slash_lossy().to_string();
175            }
176        }
177    }
178}
179
180/// A relative [`Remapping`] that's aware of the current location
181///
182/// See [`RelativeRemappingPathBuf`]
183#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
184pub struct RelativeRemapping {
185    pub context: Option<String>,
186    pub name: String,
187    pub path: RelativeRemappingPathBuf,
188}
189
190impl RelativeRemapping {
191    /// Creates a new `RelativeRemapping` starting prefixed with `root`
192    pub fn new(remapping: Remapping, root: &Path) -> Self {
193        Self {
194            context: remapping.context.map(|c| {
195                RelativeRemappingPathBuf::with_root(root, c).path.to_string_lossy().to_string()
196            }),
197            name: remapping.name,
198            path: RelativeRemappingPathBuf::with_root(root, remapping.path),
199        }
200    }
201
202    /// Converts this relative remapping into an absolute remapping
203    ///
204    /// This sets to root of the remapping to the given `root` path
205    pub fn to_remapping(mut self, root: PathBuf) -> Remapping {
206        self.path.parent = Some(root);
207        self.into()
208    }
209
210    /// Converts this relative remapping into [`Remapping`] without the root path
211    pub fn to_relative_remapping(mut self) -> Remapping {
212        self.path.parent.take();
213        self.into()
214    }
215}
216
217// Remappings are printed as `prefix=target`
218impl fmt::Display for RelativeRemapping {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        let mut s = String::new();
221        if let Some(context) = self.context.as_ref() {
222            #[cfg(target_os = "windows")]
223            {
224                // ensure we have `/` slashes on windows
225                use path_slash::PathExt;
226                s.push_str(&std::path::Path::new(context).to_slash_lossy());
227            }
228            #[cfg(not(target_os = "windows"))]
229            {
230                s.push_str(context);
231            }
232            s.push(':');
233        }
234        s.push_str(&{
235            #[cfg(target_os = "windows")]
236            {
237                // ensure we have `/` slashes on windows
238                use path_slash::PathExt;
239                format!("{}={}", self.name, self.path.original().to_slash_lossy())
240            }
241            #[cfg(not(target_os = "windows"))]
242            {
243                format!("{}={}", self.name, self.path.original().display())
244            }
245        });
246
247        if needs_trailing_slash(&s) {
248            s.push('/');
249        }
250        f.write_str(&s)
251    }
252}
253
254impl From<RelativeRemapping> for Remapping {
255    fn from(r: RelativeRemapping) -> Self {
256        let RelativeRemapping { context, mut name, path } = r;
257        let mut path = path.relative().display().to_string();
258        if needs_trailing_slash(&path) {
259            path.push('/');
260        }
261        if needs_trailing_slash(&name) {
262            name.push('/');
263        }
264        Self { context, name, path }
265    }
266}
267
268impl From<Remapping> for RelativeRemapping {
269    fn from(r: Remapping) -> Self {
270        Self { context: r.context, name: r.name, path: r.path.into() }
271    }
272}
273
274/// The path part of the [`Remapping`] that knows the path of the file it was configured in, if any.
275///
276/// A [`Remapping`] is intended to be absolute, but paths in configuration files are often desired
277/// to be relative to the configuration file itself. For example, a path of
278/// `weird-erc20/=lib/weird-erc20/src/` configured in a file `/var/foundry.toml` might be desired to
279/// resolve as a `weird-erc20/=/var/lib/weird-erc20/src/` remapping.
280#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
281pub struct RelativeRemappingPathBuf {
282    pub parent: Option<PathBuf>,
283    pub path: PathBuf,
284}
285
286impl RelativeRemappingPathBuf {
287    /// Creates a new `RelativeRemappingPathBuf` that checks if the `path` is a child path of
288    /// `parent`.
289    pub fn with_root(
290        parent: impl AsRef<Path> + Into<PathBuf>,
291        path: impl AsRef<Path> + Into<PathBuf>,
292    ) -> Self {
293        if let Ok(path) = path.as_ref().strip_prefix(parent.as_ref()) {
294            Self { parent: Some(parent.into()), path: path.to_path_buf() }
295        } else if path.as_ref().has_root() {
296            Self { parent: None, path: path.into() }
297        } else {
298            Self { parent: Some(parent.into()), path: path.into() }
299        }
300    }
301
302    /// Returns the path as it was declared, without modification.
303    pub fn original(&self) -> &Path {
304        &self.path
305    }
306
307    /// Returns this path relative to the file it was declared in, if any.
308    /// Returns the original if this path was not declared in a file or if the
309    /// path has a root.
310    pub fn relative(&self) -> PathBuf {
311        if self.original().has_root() {
312            return self.original().into();
313        }
314        self.parent
315            .as_ref()
316            .map(|p| p.join(self.original()))
317            .unwrap_or_else(|| self.original().into())
318    }
319}
320
321impl<P: Into<PathBuf>> From<P> for RelativeRemappingPathBuf {
322    fn from(path: P) -> Self {
323        Self { parent: None, path: path.into() }
324    }
325}
326
327impl Serialize for RelativeRemapping {
328    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
329    where
330        S: serde::ser::Serializer,
331    {
332        serializer.serialize_str(&self.to_string())
333    }
334}
335
336impl<'de> Deserialize<'de> for RelativeRemapping {
337    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
338    where
339        D: serde::de::Deserializer<'de>,
340    {
341        let remapping = String::deserialize(deserializer)?;
342        let remapping = Remapping::from_str(&remapping).map_err(serde::de::Error::custom)?;
343        Ok(Self { context: remapping.context, name: remapping.name, path: remapping.path.into() })
344    }
345}
346
347/// Helper to determine if name or path of a remapping needs trailing slash.
348/// Returns false if it already ends with a slash or if remapping is a solidity file.
349/// Used to preserve name and path of single file remapping, see
350/// <https://github.com/foundry-rs/foundry/issues/6706>
351/// <https://github.com/foundry-rs/foundry/issues/8499>
352fn needs_trailing_slash(name_or_path: &str) -> bool {
353    !name_or_path.ends_with('/') && !name_or_path.ends_with(".sol")
354}
355
356#[cfg(test)]
357mod tests {
358    pub use super::*;
359    pub use similar_asserts::assert_eq;
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}