ethers_solc/
remappings.rs

1use crate::utils;
2
3use serde::{Deserialize, Serialize};
4use std::{
5    collections::{hash_map::Entry, HashMap},
6    fmt,
7    path::{Path, PathBuf},
8    str::FromStr,
9};
10
11const DAPPTOOLS_CONTRACTS_DIR: &str = "src";
12const DAPPTOOLS_LIB_DIR: &str = "lib";
13const JS_CONTRACTS_DIR: &str = "contracts";
14const JS_LIB_DIR: &str = "node_modules";
15
16/// The solidity compiler can only reference files that exist locally on your computer.
17/// So importing directly from GitHub (as an example) is not possible.
18///
19/// Let's imagine you want to use OpenZeppelin's amazing library of smart contracts,
20/// `@openzeppelin/contracts-ethereum-package`:
21///
22/// ```ignore
23/// pragma solidity 0.5.11;
24///
25/// import "@openzeppelin/contracts-ethereum-package/contracts/math/SafeMath.sol";
26///
27/// contract MyContract {
28///     using SafeMath for uint256;
29///     ...
30/// }
31/// ```
32///
33/// When using `solc`, you have to specify the following:
34///
35/// - A `prefix`: the path that's used in your smart contract, i.e.
36///   `@openzeppelin/contracts-ethereum-package`
37/// - A `target`: the absolute path of the downloaded contracts on your computer
38///
39/// The format looks like this: `solc prefix=target ./MyContract.sol`
40///
41/// For example:
42///
43/// ```text
44/// solc --bin \
45///     @openzeppelin/contracts-ethereum-package=/Your/Absolute/Path/To/@openzeppelin/contracts-ethereum-package \
46///     ./MyContract.sol
47/// ```
48///
49/// You can also specify a `context` which limits the scope of the remapping to a subset of your
50/// project. This allows you to apply the remapping only to imports located in a specific library or
51/// a specific file. Without a context a remapping is applied to every matching import in all files.
52///
53/// The format is: `solc context:prefix=target ./MyContract.sol`
54///
55/// [Source](https://ethereum.stackexchange.com/questions/74448/what-are-remappings-and-how-do-they-work-in-solidity)
56#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
57pub struct Remapping {
58    pub context: Option<String>,
59    pub name: String,
60    pub path: String,
61}
62
63impl Remapping {
64    /// Convenience function for [`RelativeRemapping::new`]
65    pub fn into_relative(self, root: impl AsRef<Path>) -> RelativeRemapping {
66        RelativeRemapping::new(self, root)
67    }
68
69    /// Removes the `base` path from the remapping
70    pub fn strip_prefix(&mut self, base: impl AsRef<Path>) -> &mut Self {
71        if let Ok(stripped) = Path::new(&self.path).strip_prefix(base.as_ref()) {
72            self.path = format!("{}", stripped.display());
73        }
74        self
75    }
76}
77
78#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd)]
79pub enum RemappingError {
80    #[error("invalid remapping format, found `{0}`, expected `<key>=<value>`")]
81    InvalidRemapping(String),
82    #[error("remapping key can't be empty, found `{0}`, expected `<key>=<value>`")]
83    EmptyRemappingKey(String),
84    #[error("remapping value must be a path, found `{0}`, expected `<key>=<value>`")]
85    EmptyRemappingValue(String),
86}
87
88impl FromStr for Remapping {
89    type Err = RemappingError;
90
91    fn from_str(remapping: &str) -> Result<Self, Self::Err> {
92        let (name, path) = remapping
93            .split_once('=')
94            .ok_or_else(|| RemappingError::InvalidRemapping(remapping.to_string()))?;
95        let (context, name) = name
96            .split_once(':')
97            .map_or((None, name), |(context, name)| (Some(context.to_string()), name));
98        if name.trim().is_empty() {
99            return Err(RemappingError::EmptyRemappingKey(remapping.to_string()))
100        }
101        if path.trim().is_empty() {
102            return Err(RemappingError::EmptyRemappingValue(remapping.to_string()))
103        }
104        // if the remapping just starts with : (no context name), treat it as global
105        let context =
106            context.and_then(|c| if c.trim().is_empty() { None } else { Some(c.to_string()) });
107        Ok(Remapping { context, name: name.to_string(), path: path.to_string() })
108    }
109}
110
111impl Serialize for Remapping {
112    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
113    where
114        S: serde::ser::Serializer,
115    {
116        serializer.serialize_str(&self.to_string())
117    }
118}
119
120impl<'de> Deserialize<'de> for Remapping {
121    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
122    where
123        D: serde::de::Deserializer<'de>,
124    {
125        let remapping = String::deserialize(deserializer)?;
126        Remapping::from_str(&remapping).map_err(serde::de::Error::custom)
127    }
128}
129
130// Remappings are printed as `prefix=target`
131impl fmt::Display for Remapping {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        let mut s = String::new();
134        if let Some(context) = self.context.as_ref() {
135            #[cfg(target_os = "windows")]
136            {
137                // ensure we have `/` slashes on windows
138                use path_slash::PathExt;
139                s.push_str(&std::path::Path::new(context).to_slash_lossy());
140            }
141            #[cfg(not(target_os = "windows"))]
142            {
143                s.push_str(context);
144            }
145            s.push(':');
146        }
147        s.push_str(&{
148            #[cfg(target_os = "windows")]
149            {
150                // ensure we have `/` slashes on windows
151                use path_slash::PathExt;
152                format!("{}={}", self.name, std::path::Path::new(&self.path).to_slash_lossy())
153            }
154            #[cfg(not(target_os = "windows"))]
155            {
156                format!("{}={}", self.name, self.path)
157            }
158        });
159
160        if !s.ends_with('/') {
161            s.push('/');
162        }
163        f.write_str(&s)
164    }
165}
166
167impl Remapping {
168    /// Returns all formatted remappings
169    pub fn find_many_str(path: &str) -> Vec<String> {
170        Self::find_many(path).into_iter().map(|r| r.to_string()).collect()
171    }
172
173    /// Attempts to autodetect all remappings given a certain root path.
174    ///
175    /// This will recursively scan all subdirectories of the root path, if a subdirectory contains a
176    /// solidity file then this a candidate for a remapping. The name of the remapping will be the
177    /// folder name.
178    ///
179    /// However, there are additional rules/assumptions when it comes to determining if a candidate
180    /// should in fact be a remapping:
181    ///
182    /// All names and paths end with a trailing "/"
183    ///
184    /// The name of the remapping will be the parent folder of a solidity file, unless the folder is
185    /// named `src`, `lib` or `contracts` in which case the name of the remapping will be the parent
186    /// folder's name of `src`, `lib`, `contracts`: The remapping of `repo1/src/contract.sol` is
187    /// `name: "repo1/", path: "repo1/src/"`
188    ///
189    /// Nested remappings need to be separated by `src`, `lib` or `contracts`, The remapping of
190    /// `repo1/lib/ds-math/src/contract.sol` is `name: "ds-math/", "repo1/lib/ds-math/src/"`
191    ///
192    /// Remapping detection is primarily designed for dapptool's rules for lib folders, however, we
193    /// attempt to detect and optimize various folder structures commonly used in `node_modules`
194    /// dependencies. For those the same rules apply. In addition, we try to unify all
195    /// remappings discovered according to the rules mentioned above, so that layouts like,
196    //   @aave/
197    //   ├─ governance/
198    //   │  ├─ contracts/
199    //   ├─ protocol-v2/
200    //   │  ├─ contracts/
201    ///
202    /// which would be multiple rededications according to our rules ("governance", "protocol-v2"),
203    /// are unified into `@aave` by looking at their common ancestor, the root of this subdirectory
204    /// (`@aave`)
205    pub fn find_many(dir: impl AsRef<Path>) -> Vec<Remapping> {
206        /// prioritize
207        ///   - ("a", "1/2") over ("a", "1/2/3")
208        ///   - if a path ends with `src`
209        fn insert_prioritized(mappings: &mut HashMap<String, PathBuf>, key: String, path: PathBuf) {
210            match mappings.entry(key) {
211                Entry::Occupied(mut e) => {
212                    if e.get().components().count() > path.components().count() ||
213                        (path.ends_with(DAPPTOOLS_CONTRACTS_DIR) &&
214                            !e.get().ends_with(DAPPTOOLS_CONTRACTS_DIR))
215                    {
216                        e.insert(path);
217                    }
218                }
219                Entry::Vacant(e) => {
220                    e.insert(path);
221                }
222            }
223        }
224
225        // all combined remappings from all subdirs
226        let mut all_remappings = HashMap::new();
227
228        let dir = dir.as_ref();
229        let is_inside_node_modules = dir.ends_with("node_modules");
230
231        // iterate over all dirs that are children of the root
232        for dir in walkdir::WalkDir::new(dir)
233            .follow_links(true)
234            .min_depth(1)
235            .max_depth(1)
236            .into_iter()
237            .filter_entry(|e| !is_hidden(e))
238            .filter_map(Result::ok)
239            .filter(|e| e.file_type().is_dir())
240        {
241            let depth1_dir = dir.path();
242            // check all remappings in this depth 1 folder
243            let candidates =
244                find_remapping_candidates(depth1_dir, depth1_dir, 0, is_inside_node_modules);
245
246            for candidate in candidates {
247                if let Some(name) = candidate.window_start.file_name().and_then(|s| s.to_str()) {
248                    insert_prioritized(
249                        &mut all_remappings,
250                        format!("{name}/"),
251                        candidate.source_dir,
252                    );
253                }
254            }
255        }
256
257        all_remappings
258            .into_iter()
259            .map(|(name, path)| Remapping {
260                context: None,
261                name,
262                path: format!("{}/", path.display()),
263            })
264            .collect()
265    }
266
267    /// Converts any `\\` separators in the `path` to `/`
268    pub fn slash_path(&mut self) {
269        #[cfg(windows)]
270        {
271            use path_slash::PathExt;
272            self.path = Path::new(&self.path).to_slash_lossy().to_string();
273            if let Some(context) = self.context.as_mut() {
274                *context = Path::new(&context).to_slash_lossy().to_string();
275            }
276        }
277    }
278}
279
280/// A relative [`Remapping`] that's aware of the current location
281///
282/// See [`RelativeRemappingPathBuf`]
283#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
284pub struct RelativeRemapping {
285    pub context: Option<String>,
286    pub name: String,
287    pub path: RelativeRemappingPathBuf,
288}
289
290impl RelativeRemapping {
291    /// Creates a new `RelativeRemapping` starting prefixed with `root`
292    pub fn new(remapping: Remapping, root: impl AsRef<Path>) -> Self {
293        Self {
294            context: remapping.context.map(|c| {
295                RelativeRemappingPathBuf::with_root(root.as_ref(), c)
296                    .path
297                    .to_string_lossy()
298                    .to_string()
299            }),
300            name: remapping.name,
301            path: RelativeRemappingPathBuf::with_root(root, remapping.path),
302        }
303    }
304
305    /// Converts this relative remapping into an absolute remapping
306    ///
307    /// This sets to root of the remapping to the given `root` path
308    pub fn to_remapping(mut self, root: PathBuf) -> Remapping {
309        self.path.parent = Some(root);
310        self.into()
311    }
312
313    /// Converts this relative remapping into [`Remapping`] without the root path
314    pub fn to_relative_remapping(mut self) -> Remapping {
315        self.path.parent.take();
316        self.into()
317    }
318}
319
320// Remappings are printed as `prefix=target`
321impl fmt::Display for RelativeRemapping {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        let mut s = String::new();
324        if let Some(context) = self.context.as_ref() {
325            #[cfg(target_os = "windows")]
326            {
327                // ensure we have `/` slashes on windows
328                use path_slash::PathExt;
329                s.push_str(&std::path::Path::new(context).to_slash_lossy());
330            }
331            #[cfg(not(target_os = "windows"))]
332            {
333                s.push_str(context);
334            }
335            s.push(':');
336        }
337        s.push_str(&{
338            #[cfg(target_os = "windows")]
339            {
340                // ensure we have `/` slashes on windows
341                use path_slash::PathExt;
342                format!("{}={}", self.name, self.path.original().to_slash_lossy())
343            }
344            #[cfg(not(target_os = "windows"))]
345            {
346                format!("{}={}", self.name, self.path.original().display())
347            }
348        });
349
350        if !s.ends_with('/') {
351            s.push('/');
352        }
353        f.write_str(&s)
354    }
355}
356
357impl From<RelativeRemapping> for Remapping {
358    fn from(r: RelativeRemapping) -> Self {
359        let RelativeRemapping { context, mut name, path } = r;
360        let mut path = format!("{}", path.relative().display());
361        if !path.ends_with('/') {
362            path.push('/');
363        }
364        if !name.ends_with('/') {
365            name.push('/');
366        }
367        Remapping { context, name, path }
368    }
369}
370
371impl From<Remapping> for RelativeRemapping {
372    fn from(r: Remapping) -> Self {
373        Self { context: r.context, name: r.name, path: r.path.into() }
374    }
375}
376
377/// The path part of the [`Remapping`] that knows the path of the file it was configured in, if any.
378///
379/// A [`Remapping`] is intended to be absolute, but paths in configuration files are often desired
380/// to be relative to the configuration file itself. For example, a path of
381/// `weird-erc20/=lib/weird-erc20/src/` configured in a file `/var/foundry.toml` might be desired to
382/// resolve as a `weird-erc20/=/var/lib/weird-erc20/src/` remapping.
383#[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord)]
384pub struct RelativeRemappingPathBuf {
385    pub parent: Option<PathBuf>,
386    pub path: PathBuf,
387}
388
389impl RelativeRemappingPathBuf {
390    /// Creates a new `RelativeRemappingPathBuf` that checks if the `path` is a child path of
391    /// `parent`.
392    pub fn with_root(parent: impl AsRef<Path>, path: impl AsRef<Path>) -> Self {
393        let parent = parent.as_ref();
394        let path = path.as_ref();
395        if let Ok(path) = path.strip_prefix(parent) {
396            Self { parent: Some(parent.to_path_buf()), path: path.to_path_buf() }
397        } else if path.has_root() {
398            Self { parent: None, path: path.to_path_buf() }
399        } else {
400            Self { parent: Some(parent.to_path_buf()), path: path.to_path_buf() }
401        }
402    }
403
404    /// Returns the path as it was declared, without modification.
405    pub fn original(&self) -> &Path {
406        &self.path
407    }
408
409    /// Returns this path relative to the file it was declared in, if any.
410    /// Returns the original if this path was not declared in a file or if the
411    /// path has a root.
412    pub fn relative(&self) -> PathBuf {
413        if self.original().has_root() {
414            return self.original().into()
415        }
416        self.parent
417            .as_ref()
418            .map(|p| p.join(self.original()))
419            .unwrap_or_else(|| self.original().into())
420    }
421}
422
423impl<P: AsRef<Path>> From<P> for RelativeRemappingPathBuf {
424    fn from(path: P) -> RelativeRemappingPathBuf {
425        Self { parent: None, path: path.as_ref().to_path_buf() }
426    }
427}
428
429impl Serialize for RelativeRemapping {
430    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
431    where
432        S: serde::ser::Serializer,
433    {
434        serializer.serialize_str(&self.to_string())
435    }
436}
437
438impl<'de> Deserialize<'de> for RelativeRemapping {
439    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
440    where
441        D: serde::de::Deserializer<'de>,
442    {
443        let remapping = String::deserialize(deserializer)?;
444        let remapping = Remapping::from_str(&remapping).map_err(serde::de::Error::custom)?;
445        Ok(RelativeRemapping {
446            context: remapping.context,
447            name: remapping.name,
448            path: remapping.path.into(),
449        })
450    }
451}
452
453#[derive(Debug, Clone)]
454struct Candidate {
455    /// dir that opened the window
456    window_start: PathBuf,
457    /// dir that contains the solidity file
458    source_dir: PathBuf,
459    /// number of the current nested dependency
460    window_level: usize,
461}
462
463impl Candidate {
464    /// There are several cases where multiple candidates are detected for the same level
465    ///
466    /// # Example - Dapptools style
467    ///
468    /// Another directory next to a `src` dir:
469    ///  ```text
470    ///  ds-test/
471    ///  ├── aux/demo.sol
472    ///  └── src/test.sol
473    ///  ```
474    ///  which effectively ignores the `aux` dir by prioritizing source dirs and keeps
475    ///  `ds-test/=ds-test/src/`
476    ///
477    ///
478    /// # Example - node_modules / commonly onpenzeppelin related
479    ///
480    /// The `@openzeppelin` domain can contain several nested dirs in `node_modules/@openzeppelin`.
481    /// Such as
482    ///    - `node_modules/@openzeppelin/contracts`
483    ///    - `node_modules/@openzeppelin/contracts-upgradeable`
484    ///
485    /// Which should be resolved to the top level dir `@openzeppelin`
486    ///
487    /// We also treat candidates with a `node_modules` parent directory differently and consider
488    /// them to be `hardhat` style. In which case the trailing library barrier `contracts` will be
489    /// stripped from the remapping path. This differs from dapptools style which does not include
490    /// the library barrier path `src` in the solidity import statements. For example, for
491    /// dapptools you could have
492    ///
493    /// ```text
494    /// <root>/lib/<library>
495    /// ├── src
496    ///     ├── A.sol
497    ///     ├── B.sol
498    /// ```
499    ///
500    /// with remapping `library/=library/src/`
501    ///
502    /// whereas with hardhat's import resolver the import statement
503    ///
504    /// ```text
505    /// <root>/node_modules/<library>
506    /// ├── contracts
507    ///     ├── A.sol
508    ///     ├── B.sol
509    /// ```
510    /// with the simple remapping `library/=library/` because hardhat's lib resolver essentially
511    /// joins the import path inside a solidity file with the `nodes_modules` folder when it tries
512    /// to find an imported solidity file. For example
513    ///
514    /// ```solidity
515    /// import "hardhat/console.sol";
516    /// ```
517    /// expects the file to be at: `<root>/node_modules/hardhat/console.sol`.
518    ///
519    /// In order to support these cases, we treat the Dapptools case as the outlier, in which case
520    /// we only keep the candidate that ends with `src`
521    ///
522    ///   - `candidates`: list of viable remapping candidates
523    ///   - `current_dir`: the directory that's currently processed, like `@openzeppelin/contracts`
524    ///   - `current_level`: the number of nested library dirs encountered
525    ///   - `window_start`: This contains the root directory of the current window. In other words
526    ///     this will be the parent directory of the most recent library barrier, which will be
527    ///     `@openzeppelin` if the `current_dir` is `@openzeppelin/contracts` See also
528    ///     [`next_nested_window()`]
529    ///   - `is_inside_node_modules` whether we're inside a `node_modules` lib
530    fn merge_on_same_level(
531        candidates: &mut Vec<Candidate>,
532        current_dir: &Path,
533        current_level: usize,
534        window_start: PathBuf,
535        is_inside_node_modules: bool,
536    ) {
537        // if there's only a single source dir candidate then we use this
538        if let Some(pos) = candidates
539            .iter()
540            .enumerate()
541            .fold((0, None), |(mut contracts_dir_count, mut pos), (idx, c)| {
542                if c.source_dir.ends_with(DAPPTOOLS_CONTRACTS_DIR) {
543                    contracts_dir_count += 1;
544                    if contracts_dir_count == 1 {
545                        pos = Some(idx)
546                    } else {
547                        pos = None;
548                    }
549                }
550
551                (contracts_dir_count, pos)
552            })
553            .1
554        {
555            let c = candidates.remove(pos);
556            *candidates = vec![c];
557        } else {
558            // merge all candidates on the current level if the current dir is itself a candidate or
559            // there are multiple nested candidates on the current level like `current/{auth,
560            // tokens}/contracts/c.sol`
561            candidates.retain(|c| c.window_level != current_level);
562
563            let source_dir = if is_inside_node_modules {
564                window_start.clone()
565            } else {
566                current_dir.to_path_buf()
567            };
568
569            // if the window start and the source dir are the same directory we can end early if
570            // we wrongfully detect something like: `<dep>/src/lib/`
571            if current_level > 0 &&
572                source_dir == window_start &&
573                (is_source_dir(&source_dir) || is_lib_dir(&source_dir))
574            {
575                return
576            }
577            candidates.push(Candidate { window_start, source_dir, window_level: current_level });
578        }
579    }
580
581    /// Returns `true` if the `source_dir` ends with `contracts` or `contracts/src`
582    ///
583    /// This is used to detect an edge case in `"@chainlink/contracts"` which layout is
584    ///
585    /// ```text
586    /// contracts/src
587    /// ├── v0.4
588    ///     ├── Pointer.sol
589    ///     ├── interfaces
590    ///         ├── AggregatorInterface.sol
591    ///     ├── tests
592    ///         ├── BasicConsumer.sol
593    /// ├── v0.5
594    ///     ├── Chainlink.sol
595    /// ├── v0.6
596    ///     ├── AccessControlledAggregator.sol
597    /// ```
598    ///
599    /// And import commonly used is
600    ///
601    /// ```solidity
602    /// import '@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol';
603    /// ```
604    fn source_dir_ends_with_js_source(&self) -> bool {
605        self.source_dir.ends_with(JS_CONTRACTS_DIR) || self.source_dir.ends_with("contracts/src/")
606    }
607}
608
609fn is_source_dir(dir: &Path) -> bool {
610    dir.file_name()
611        .and_then(|p| p.to_str())
612        .map(|name| [DAPPTOOLS_CONTRACTS_DIR, JS_CONTRACTS_DIR].contains(&name))
613        .unwrap_or_default()
614}
615
616fn is_lib_dir(dir: &Path) -> bool {
617    dir.file_name()
618        .and_then(|p| p.to_str())
619        .map(|name| [DAPPTOOLS_LIB_DIR, JS_LIB_DIR].contains(&name))
620        .unwrap_or_default()
621}
622
623/// Returns true if the file is _hidden_
624fn is_hidden(entry: &walkdir::DirEntry) -> bool {
625    entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
626}
627
628/// Finds all remappings in the directory recursively
629fn find_remapping_candidates(
630    current_dir: &Path,
631    open: &Path,
632    current_level: usize,
633    is_inside_node_modules: bool,
634) -> Vec<Candidate> {
635    // this is a marker if the current root is a candidate for a remapping
636    let mut is_candidate = false;
637
638    // all found candidates
639    let mut candidates = Vec::new();
640
641    // scan all entries in the current dir
642    for entry in walkdir::WalkDir::new(current_dir)
643        .follow_links(true)
644        .min_depth(1)
645        .max_depth(1)
646        .into_iter()
647        .filter_entry(|e| !is_hidden(e))
648        .filter_map(Result::ok)
649    {
650        let entry: walkdir::DirEntry = entry;
651
652        // found a solidity file directly the current dir
653        if !is_candidate &&
654            entry.file_type().is_file() &&
655            entry.path().extension() == Some("sol".as_ref())
656        {
657            is_candidate = true;
658        } else if entry.file_type().is_dir() {
659            // if the dir is a symlink to a parent dir we short circuit here
660            // `walkdir` will catch symlink loops, but this check prevents that we end up scanning a
661            // workspace like
662            // ```text
663            // my-package/node_modules
664            // ├── dep/node_modules
665            //     ├── symlink to `my-package`
666            // ```
667            if entry.path_is_symlink() {
668                if let Ok(target) = utils::canonicalize(entry.path()) {
669                    // the symlink points to a parent dir of the current window
670                    if open.components().count() > target.components().count() &&
671                        utils::common_ancestor(open, &target).is_some()
672                    {
673                        // short-circuiting
674                        return Vec::new()
675                    }
676                }
677            }
678
679            let subdir = entry.path();
680            // we skip commonly used subdirs that should not be searched for recursively
681            if !(subdir.ends_with("tests") || subdir.ends_with("test") || subdir.ends_with("demo"))
682            {
683                // scan the subdirectory for remappings, but we need a way to identify nested
684                // dependencies like `ds-token/lib/ds-stop/lib/ds-note/src/contract.sol`, or
685                // `oz/{tokens,auth}/{contracts, interfaces}/contract.sol` to assign
686                // the remappings to their root, we use a window that lies between two barriers. If
687                // we find a solidity file within a window, it belongs to the dir that opened the
688                // window.
689
690                // check if the subdir is a lib barrier, in which case we open a new window
691                if is_lib_dir(subdir) {
692                    candidates.extend(find_remapping_candidates(
693                        subdir,
694                        subdir,
695                        current_level + 1,
696                        is_inside_node_modules,
697                    ));
698                } else {
699                    // continue scanning with the current window
700                    candidates.extend(find_remapping_candidates(
701                        subdir,
702                        open,
703                        current_level,
704                        is_inside_node_modules,
705                    ));
706                }
707            }
708        }
709    }
710
711    // need to find the actual next window in the event `open` is a lib dir
712    let window_start = next_nested_window(open, current_dir);
713    // finally, we need to merge, adjust candidates from the same level and opening window
714    if is_candidate ||
715        candidates
716            .iter()
717            .filter(|c| c.window_level == current_level && c.window_start == window_start)
718            .count() >
719            1
720    {
721        Candidate::merge_on_same_level(
722            &mut candidates,
723            current_dir,
724            current_level,
725            window_start,
726            is_inside_node_modules,
727        );
728    } else {
729        // this handles the case if there is a single nested candidate
730        if let Some(candidate) = candidates.iter_mut().find(|c| c.window_level == current_level) {
731            // we need to determine the distance from the starting point of the window to the
732            // contracts dir for cases like `current/nested/contracts/c.sol` which should point to
733            // `current`
734            let distance = dir_distance(&candidate.window_start, &candidate.source_dir);
735            if distance > 1 && candidate.source_dir_ends_with_js_source() {
736                candidate.source_dir = window_start;
737            } else if !is_source_dir(&candidate.source_dir) &&
738                candidate.source_dir != candidate.window_start
739            {
740                candidate.source_dir = last_nested_source_dir(open, &candidate.source_dir);
741            }
742        }
743    }
744    candidates
745}
746
747/// Counts the number of components between `root` and `current`
748/// `dir_distance("root/a", "root/a/b/c") == 2`
749fn dir_distance(root: &Path, current: &Path) -> usize {
750    if root == current {
751        return 0
752    }
753    if let Ok(rem) = current.strip_prefix(root) {
754        rem.components().count()
755    } else {
756        0
757    }
758}
759
760/// This finds the next window between `root` and `current`
761/// If `root` ends with a `lib` component then start join components from `current` until no valid
762/// window opener is found
763fn next_nested_window(root: &Path, current: &Path) -> PathBuf {
764    if !is_lib_dir(root) || root == current {
765        return root.to_path_buf()
766    }
767    if let Ok(rem) = current.strip_prefix(root) {
768        let mut p = root.to_path_buf();
769        for c in rem.components() {
770            let next = p.join(c);
771            if !is_lib_dir(&next) || !next.ends_with(JS_CONTRACTS_DIR) {
772                return next
773            }
774            p = next
775        }
776    }
777    root.to_path_buf()
778}
779
780/// Finds the last valid source directory in the window (root -> dir)
781fn last_nested_source_dir(root: &Path, dir: &Path) -> PathBuf {
782    if is_source_dir(dir) {
783        return dir.to_path_buf()
784    }
785    let mut p = dir;
786    while let Some(parent) = p.parent() {
787        if parent == root {
788            return root.to_path_buf()
789        }
790        if is_source_dir(parent) {
791            return parent.to_path_buf()
792        }
793        p = parent;
794    }
795    root.to_path_buf()
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::{utils::tempdir, ProjectPathsConfig};
802
803    #[test]
804    fn relative_remapping() {
805        let remapping = "oz=a/b/c/d";
806        let remapping = Remapping::from_str(remapping).unwrap();
807
808        let relative = RelativeRemapping::new(remapping.clone(), "a/b/c");
809        assert_eq!(relative.path.relative(), Path::new(&remapping.path));
810        assert_eq!(relative.path.original(), Path::new("d"));
811
812        let relative = RelativeRemapping::new(remapping.clone(), "x/y");
813        assert_eq!(relative.path.relative(), Path::new("x/y/a/b/c/d"));
814        assert_eq!(relative.path.original(), Path::new(&remapping.path));
815
816        let remapping = "oz=/a/b/c/d";
817        let remapping = Remapping::from_str(remapping).unwrap();
818        let relative = RelativeRemapping::new(remapping.clone(), "a/b");
819        assert_eq!(relative.path.relative(), Path::new(&remapping.path));
820        assert_eq!(relative.path.original(), Path::new(&remapping.path));
821        assert!(relative.path.parent.is_none());
822
823        let relative = RelativeRemapping::new(remapping, "/a/b");
824        assert_eq!(relative.to_relative_remapping(), Remapping::from_str("oz/=c/d/").unwrap());
825    }
826
827    #[test]
828    fn remapping_errors() {
829        let remapping = "oz=../b/c/d";
830        let remapping = Remapping::from_str(remapping).unwrap();
831        assert_eq!(remapping.name, "oz".to_string());
832        assert_eq!(remapping.path, "../b/c/d".to_string());
833
834        let err = Remapping::from_str("").unwrap_err();
835        matches!(err, RemappingError::InvalidRemapping(_));
836
837        let err = Remapping::from_str("oz=").unwrap_err();
838        matches!(err, RemappingError::EmptyRemappingValue(_));
839    }
840
841    // <https://doc.rust-lang.org/rust-by-example/std_misc/fs.html>
842    fn touch(path: &std::path::Path) -> std::io::Result<()> {
843        match std::fs::OpenOptions::new().create(true).append(true).open(path) {
844            Ok(_) => Ok(()),
845            Err(e) => Err(e),
846        }
847    }
848
849    fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
850        for path in paths {
851            if let Some(parent) = Path::new(path).parent() {
852                std::fs::create_dir_all(tmp.join(parent)).unwrap();
853            }
854            if path.ends_with(".sol") {
855                let path = tmp.join(path);
856                touch(&path).unwrap();
857            } else {
858                let path = tmp.join(path);
859                std::fs::create_dir_all(path).unwrap();
860            }
861        }
862    }
863
864    // helper function for converting path bufs to remapping strings
865    fn to_str(p: std::path::PathBuf) -> String {
866        format!("{}/", p.display())
867    }
868
869    #[test]
870    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
871    fn find_remapping_dapptools() {
872        let tmp_dir = tempdir("lib").unwrap();
873        let tmp_dir_path = tmp_dir.path();
874        let paths = ["repo1/src/", "repo1/src/contract.sol"];
875        mkdir_or_touch(tmp_dir_path, &paths[..]);
876
877        let path = tmp_dir_path.join("repo1").display().to_string();
878        let remappings = Remapping::find_many(tmp_dir_path);
879        // repo1/=lib/repo1/src
880        assert_eq!(remappings.len(), 1);
881
882        assert_eq!(remappings[0].name, "repo1/");
883        assert_eq!(remappings[0].path, format!("{path}/src/"));
884    }
885
886    #[test]
887    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
888    fn can_resolve_contract_dir_combinations() {
889        let tmp_dir = tempdir("demo").unwrap();
890        let paths =
891            ["lib/timeless/src/lib/A.sol", "lib/timeless/src/B.sol", "lib/timeless/src/test/C.sol"];
892        mkdir_or_touch(tmp_dir.path(), &paths[..]);
893
894        let tmp_dir_path = tmp_dir.path().join("lib");
895        let remappings = Remapping::find_many(&tmp_dir_path);
896        let expected = vec![Remapping {
897            context: None,
898            name: "timeless/".to_string(),
899            path: to_str(tmp_dir_path.join("timeless/src")),
900        }];
901        pretty_assertions::assert_eq!(remappings, expected);
902    }
903
904    #[test]
905    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
906    fn can_resolve_geb_remappings() {
907        let tmp_dir = tempdir("geb").unwrap();
908        let paths = [
909            "lib/ds-token/src/test/Contract.sol",
910            "lib/ds-token/lib/ds-test/src/Contract.sol",
911            "lib/ds-token/lib/ds-test/aux/Contract.sol",
912            "lib/ds-token/lib/ds-stop/lib/ds-test/src/Contract.sol",
913            "lib/ds-token/lib/ds-stop/lib/ds-note/src/Contract.sol",
914            "lib/ds-token/lib/ds-math/lib/ds-test/aux/Contract.sol",
915            "lib/ds-token/lib/ds-math/src/Contract.sol",
916            "lib/ds-token/lib/ds-stop/lib/ds-test/aux/Contract.sol",
917            "lib/ds-token/lib/ds-stop/lib/ds-note/lib/ds-test/src/Contract.sol",
918            "lib/ds-token/lib/ds-math/lib/ds-test/src/Contract.sol",
919            "lib/ds-token/lib/ds-stop/lib/ds-auth/lib/ds-test/src/Contract.sol",
920            "lib/ds-token/lib/ds-stop/src/Contract.sol",
921            "lib/ds-token/src/Contract.sol",
922            "lib/ds-token/lib/erc20/src/Contract.sol",
923            "lib/ds-token/lib/ds-stop/lib/ds-auth/lib/ds-test/aux/Contract.sol",
924            "lib/ds-token/lib/ds-stop/lib/ds-auth/src/Contract.sol",
925            "lib/ds-token/lib/ds-stop/lib/ds-note/lib/ds-test/aux/Contract.sol",
926        ];
927        mkdir_or_touch(tmp_dir.path(), &paths[..]);
928
929        let tmp_dir_path = tmp_dir.path().join("lib");
930        let mut remappings = Remapping::find_many(&tmp_dir_path);
931        remappings.sort_unstable();
932        let mut expected = vec![
933            Remapping {
934                context: None,
935                name: "ds-auth/".to_string(),
936                path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/lib/ds-auth/src")),
937            },
938            Remapping {
939                context: None,
940                name: "ds-math/".to_string(),
941                path: to_str(tmp_dir_path.join("ds-token/lib/ds-math/src")),
942            },
943            Remapping {
944                context: None,
945                name: "ds-note/".to_string(),
946                path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/lib/ds-note/src")),
947            },
948            Remapping {
949                context: None,
950                name: "ds-stop/".to_string(),
951                path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/src")),
952            },
953            Remapping {
954                context: None,
955                name: "ds-test/".to_string(),
956                path: to_str(tmp_dir_path.join("ds-token/lib/ds-test/src")),
957            },
958            Remapping {
959                context: None,
960                name: "ds-token/".to_string(),
961                path: to_str(tmp_dir_path.join("ds-token/src")),
962            },
963            Remapping {
964                context: None,
965                name: "erc20/".to_string(),
966                path: to_str(tmp_dir_path.join("ds-token/lib/erc20/src")),
967            },
968        ];
969        expected.sort_unstable();
970        pretty_assertions::assert_eq!(remappings, expected);
971    }
972
973    #[test]
974    fn can_resolve_nested_chainlink_remappings() {
975        let tmp_dir = tempdir("root").unwrap();
976        let paths = [
977            "@chainlink/contracts/src/v0.6/vendor/Contract.sol",
978            "@chainlink/contracts/src/v0.8/tests/Contract.sol",
979            "@chainlink/contracts/src/v0.7/Contract.sol",
980            "@chainlink/contracts/src/v0.6/Contract.sol",
981            "@chainlink/contracts/src/v0.5/Contract.sol",
982            "@chainlink/contracts/src/v0.7/tests/Contract.sol",
983            "@chainlink/contracts/src/v0.7/interfaces/Contract.sol",
984            "@chainlink/contracts/src/v0.4/tests/Contract.sol",
985            "@chainlink/contracts/src/v0.6/tests/Contract.sol",
986            "@chainlink/contracts/src/v0.5/tests/Contract.sol",
987            "@chainlink/contracts/src/v0.8/vendor/Contract.sol",
988            "@chainlink/contracts/src/v0.5/dev/Contract.sol",
989            "@chainlink/contracts/src/v0.6/examples/Contract.sol",
990            "@chainlink/contracts/src/v0.5/interfaces/Contract.sol",
991            "@chainlink/contracts/src/v0.4/interfaces/Contract.sol",
992            "@chainlink/contracts/src/v0.4/vendor/Contract.sol",
993            "@chainlink/contracts/src/v0.6/interfaces/Contract.sol",
994            "@chainlink/contracts/src/v0.7/dev/Contract.sol",
995            "@chainlink/contracts/src/v0.8/dev/Contract.sol",
996            "@chainlink/contracts/src/v0.5/vendor/Contract.sol",
997            "@chainlink/contracts/src/v0.7/vendor/Contract.sol",
998            "@chainlink/contracts/src/v0.4/Contract.sol",
999            "@chainlink/contracts/src/v0.8/interfaces/Contract.sol",
1000            "@chainlink/contracts/src/v0.6/dev/Contract.sol",
1001        ];
1002        mkdir_or_touch(tmp_dir.path(), &paths[..]);
1003        let remappings = Remapping::find_many(tmp_dir.path());
1004
1005        let expected = vec![Remapping {
1006            context: None,
1007            name: "@chainlink/".to_string(),
1008            path: to_str(tmp_dir.path().join("@chainlink")),
1009        }];
1010        pretty_assertions::assert_eq!(remappings, expected);
1011    }
1012
1013    #[test]
1014    fn can_resolve_oz_upgradeable_remappings() {
1015        let tmp_dir = tempdir("root").unwrap();
1016        let paths = [
1017            "@openzeppelin/contracts-upgradeable/proxy/ERC1967/Contract.sol",
1018            "@openzeppelin/contracts-upgradeable/token/ERC1155/Contract.sol",
1019            "@openzeppelin/contracts/token/ERC777/Contract.sol",
1020            "@openzeppelin/contracts/token/ERC721/presets/Contract.sol",
1021            "@openzeppelin/contracts/interfaces/Contract.sol",
1022            "@openzeppelin/contracts-upgradeable/token/ERC777/presets/Contract.sol",
1023            "@openzeppelin/contracts/token/ERC1155/extensions/Contract.sol",
1024            "@openzeppelin/contracts/proxy/Contract.sol",
1025            "@openzeppelin/contracts/proxy/utils/Contract.sol",
1026            "@openzeppelin/contracts-upgradeable/security/Contract.sol",
1027            "@openzeppelin/contracts-upgradeable/utils/Contract.sol",
1028            "@openzeppelin/contracts/token/ERC20/Contract.sol",
1029            "@openzeppelin/contracts-upgradeable/utils/introspection/Contract.sol",
1030            "@openzeppelin/contracts/metatx/Contract.sol",
1031            "@openzeppelin/contracts/utils/cryptography/Contract.sol",
1032            "@openzeppelin/contracts/token/ERC20/utils/Contract.sol",
1033            "@openzeppelin/contracts-upgradeable/token/ERC20/utils/Contract.sol",
1034            "@openzeppelin/contracts-upgradeable/proxy/Contract.sol",
1035            "@openzeppelin/contracts-upgradeable/token/ERC20/presets/Contract.sol",
1036            "@openzeppelin/contracts-upgradeable/utils/math/Contract.sol",
1037            "@openzeppelin/contracts-upgradeable/utils/escrow/Contract.sol",
1038            "@openzeppelin/contracts/governance/extensions/Contract.sol",
1039            "@openzeppelin/contracts-upgradeable/interfaces/Contract.sol",
1040            "@openzeppelin/contracts/proxy/transparent/Contract.sol",
1041            "@openzeppelin/contracts/utils/structs/Contract.sol",
1042            "@openzeppelin/contracts-upgradeable/access/Contract.sol",
1043            "@openzeppelin/contracts/governance/compatibility/Contract.sol",
1044            "@openzeppelin/contracts/governance/Contract.sol",
1045            "@openzeppelin/contracts-upgradeable/governance/extensions/Contract.sol",
1046            "@openzeppelin/contracts/security/Contract.sol",
1047            "@openzeppelin/contracts-upgradeable/metatx/Contract.sol",
1048            "@openzeppelin/contracts-upgradeable/token/ERC721/utils/Contract.sol",
1049            "@openzeppelin/contracts/token/ERC721/utils/Contract.sol",
1050            "@openzeppelin/contracts-upgradeable/governance/compatibility/Contract.sol",
1051            "@openzeppelin/contracts/token/common/Contract.sol",
1052            "@openzeppelin/contracts/proxy/beacon/Contract.sol",
1053            "@openzeppelin/contracts-upgradeable/token/ERC721/Contract.sol",
1054            "@openzeppelin/contracts-upgradeable/proxy/beacon/Contract.sol",
1055            "@openzeppelin/contracts/token/ERC1155/utils/Contract.sol",
1056            "@openzeppelin/contracts/token/ERC777/presets/Contract.sol",
1057            "@openzeppelin/contracts-upgradeable/token/ERC20/Contract.sol",
1058            "@openzeppelin/contracts-upgradeable/utils/structs/Contract.sol",
1059            "@openzeppelin/contracts/utils/escrow/Contract.sol",
1060            "@openzeppelin/contracts/utils/Contract.sol",
1061            "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/Contract.sol",
1062            "@openzeppelin/contracts/token/ERC721/extensions/Contract.sol",
1063            "@openzeppelin/contracts-upgradeable/token/ERC777/Contract.sol",
1064            "@openzeppelin/contracts/token/ERC1155/presets/Contract.sol",
1065            "@openzeppelin/contracts/token/ERC721/Contract.sol",
1066            "@openzeppelin/contracts/token/ERC1155/Contract.sol",
1067            "@openzeppelin/contracts-upgradeable/governance/Contract.sol",
1068            "@openzeppelin/contracts/token/ERC20/extensions/Contract.sol",
1069            "@openzeppelin/contracts-upgradeable/utils/cryptography/Contract.sol",
1070            "@openzeppelin/contracts-upgradeable/token/ERC1155/presets/Contract.sol",
1071            "@openzeppelin/contracts/access/Contract.sol",
1072            "@openzeppelin/contracts/governance/utils/Contract.sol",
1073            "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/Contract.sol",
1074            "@openzeppelin/contracts-upgradeable/token/common/Contract.sol",
1075            "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/Contract.sol",
1076            "@openzeppelin/contracts/proxy/ERC1967/Contract.sol",
1077            "@openzeppelin/contracts/finance/Contract.sol",
1078            "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/Contract.sol",
1079            "@openzeppelin/contracts-upgradeable/governance/utils/Contract.sol",
1080            "@openzeppelin/contracts-upgradeable/proxy/utils/Contract.sol",
1081            "@openzeppelin/contracts/token/ERC20/presets/Contract.sol",
1082            "@openzeppelin/contracts/utils/math/Contract.sol",
1083            "@openzeppelin/contracts-upgradeable/token/ERC721/presets/Contract.sol",
1084            "@openzeppelin/contracts-upgradeable/finance/Contract.sol",
1085            "@openzeppelin/contracts/utils/introspection/Contract.sol",
1086        ];
1087        mkdir_or_touch(tmp_dir.path(), &paths[..]);
1088        let remappings = Remapping::find_many(tmp_dir.path());
1089
1090        let expected = vec![Remapping {
1091            context: None,
1092            name: "@openzeppelin/".to_string(),
1093            path: to_str(tmp_dir.path().join("@openzeppelin")),
1094        }];
1095        pretty_assertions::assert_eq!(remappings, expected);
1096    }
1097
1098    #[test]
1099    fn can_resolve_oz_remappings() {
1100        let tmp_dir = tempdir("node_modules").unwrap();
1101        let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
1102        let paths = [
1103            "node_modules/@openzeppelin/contracts/interfaces/IERC1155.sol",
1104            "node_modules/@openzeppelin/contracts/finance/VestingWallet.sol",
1105            "node_modules/@openzeppelin/contracts/proxy/Proxy.sol",
1106            "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol",
1107        ];
1108        mkdir_or_touch(tmp_dir.path(), &paths[..]);
1109        let remappings = Remapping::find_many(tmp_dir_node_modules);
1110        let mut paths = ProjectPathsConfig::hardhat(tmp_dir.path()).unwrap();
1111        paths.remappings = remappings;
1112
1113        let resolved = paths
1114            .resolve_library_import(
1115                tmp_dir.path(),
1116                Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1117            )
1118            .unwrap();
1119        assert!(resolved.exists());
1120
1121        // adjust remappings
1122        paths.remappings[0].name = "@openzeppelin/".to_string();
1123
1124        let resolved = paths
1125            .resolve_library_import(
1126                tmp_dir.path(),
1127                Path::new("@openzeppelin/contracts/token/ERC20/IERC20.sol"),
1128            )
1129            .unwrap();
1130        assert!(resolved.exists());
1131    }
1132
1133    #[test]
1134    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1135    fn recursive_remappings() {
1136        let tmp_dir = tempdir("lib").unwrap();
1137        let tmp_dir_path = tmp_dir.path();
1138        let paths = [
1139            "repo1/src/contract.sol",
1140            "repo1/lib/ds-test/src/test.sol",
1141            "repo1/lib/ds-math/src/contract.sol",
1142            "repo1/lib/ds-math/lib/ds-test/src/test.sol",
1143            "repo1/lib/guni-lev/src/contract.sol",
1144            "repo1/lib/solmate/src/auth/contract.sol",
1145            "repo1/lib/solmate/src/tokens/contract.sol",
1146            "repo1/lib/solmate/lib/ds-test/src/test.sol",
1147            "repo1/lib/solmate/lib/ds-test/demo/demo.sol",
1148            "repo1/lib/openzeppelin-contracts/contracts/access/AccessControl.sol",
1149            "repo1/lib/ds-token/lib/ds-stop/src/contract.sol",
1150            "repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src/contract.sol",
1151        ];
1152        mkdir_or_touch(tmp_dir_path, &paths[..]);
1153
1154        let path = tmp_dir_path.display().to_string();
1155        let mut remappings = Remapping::find_many(path);
1156        remappings.sort_unstable();
1157
1158        let mut expected = vec![
1159            Remapping {
1160                context: None,
1161                name: "repo1/".to_string(),
1162                path: to_str(tmp_dir_path.join("repo1").join("src")),
1163            },
1164            Remapping {
1165                context: None,
1166                name: "ds-math/".to_string(),
1167                path: to_str(tmp_dir_path.join("repo1").join("lib").join("ds-math").join("src")),
1168            },
1169            Remapping {
1170                context: None,
1171                name: "ds-test/".to_string(),
1172                path: to_str(tmp_dir_path.join("repo1").join("lib").join("ds-test").join("src")),
1173            },
1174            Remapping {
1175                context: None,
1176                name: "guni-lev/".to_string(),
1177                path: to_str(tmp_dir_path.join("repo1/lib/guni-lev").join("src")),
1178            },
1179            Remapping {
1180                context: None,
1181                name: "solmate/".to_string(),
1182                path: to_str(tmp_dir_path.join("repo1/lib/solmate").join("src")),
1183            },
1184            Remapping {
1185                context: None,
1186                name: "openzeppelin-contracts/".to_string(),
1187                path: to_str(tmp_dir_path.join("repo1/lib/openzeppelin-contracts/contracts")),
1188            },
1189            Remapping {
1190                context: None,
1191                name: "ds-stop/".to_string(),
1192                path: to_str(tmp_dir_path.join("repo1/lib/ds-token/lib/ds-stop/src")),
1193            },
1194            Remapping {
1195                context: None,
1196                name: "ds-note/".to_string(),
1197                path: to_str(tmp_dir_path.join("repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src")),
1198            },
1199        ];
1200        expected.sort_unstable();
1201        pretty_assertions::assert_eq!(remappings, expected);
1202    }
1203
1204    #[test]
1205    fn can_resolve_contexts() {
1206        let remapping = "context:oz=a/b/c/d";
1207        let remapping = Remapping::from_str(remapping).unwrap();
1208
1209        assert_eq!(
1210            remapping,
1211            Remapping {
1212                context: Some("context".to_string()),
1213                name: "oz".to_string(),
1214                path: "a/b/c/d".to_string(),
1215            }
1216        );
1217        assert_eq!(remapping.to_string(), "context:oz=a/b/c/d/".to_string());
1218
1219        let remapping = "context:foo=C:/bar/src/";
1220        let remapping = Remapping::from_str(remapping).unwrap();
1221
1222        assert_eq!(
1223            remapping,
1224            Remapping {
1225                context: Some("context".to_string()),
1226                name: "foo".to_string(),
1227                path: "C:/bar/src/".to_string()
1228            }
1229        );
1230    }
1231
1232    #[test]
1233    fn can_resolve_global_contexts() {
1234        let remapping = ":oz=a/b/c/d/";
1235        let remapping = Remapping::from_str(remapping).unwrap();
1236
1237        assert_eq!(
1238            remapping,
1239            Remapping { context: None, name: "oz".to_string(), path: "a/b/c/d/".to_string() }
1240        );
1241        assert_eq!(remapping.to_string(), "oz=a/b/c/d/".to_string());
1242    }
1243
1244    #[test]
1245    fn remappings() {
1246        let tmp_dir = tempdir("tmp").unwrap();
1247        let tmp_dir_path = tmp_dir.path().join("lib");
1248        let repo1 = tmp_dir_path.join("src_repo");
1249        let repo2 = tmp_dir_path.join("contracts_repo");
1250
1251        let dir1 = repo1.join("src");
1252        std::fs::create_dir_all(&dir1).unwrap();
1253
1254        let dir2 = repo2.join("contracts");
1255        std::fs::create_dir_all(&dir2).unwrap();
1256
1257        let contract1 = dir1.join("contract.sol");
1258        touch(&contract1).unwrap();
1259
1260        let contract2 = dir2.join("contract.sol");
1261        touch(&contract2).unwrap();
1262
1263        let path = tmp_dir_path.display().to_string();
1264        let mut remappings = Remapping::find_many(path);
1265        remappings.sort_unstable();
1266        let mut expected = vec![
1267            Remapping {
1268                context: None,
1269                name: "src_repo/".to_string(),
1270                path: format!("{}/", dir1.into_os_string().into_string().unwrap()),
1271            },
1272            Remapping {
1273                context: None,
1274                name: "contracts_repo/".to_string(),
1275                path: format!(
1276                    "{}/",
1277                    repo2.join("contracts").into_os_string().into_string().unwrap()
1278                ),
1279            },
1280        ];
1281        expected.sort_unstable();
1282        pretty_assertions::assert_eq!(remappings, expected);
1283    }
1284
1285    #[test]
1286    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1287    fn simple_dapptools_remappings() {
1288        let tmp_dir = tempdir("lib").unwrap();
1289        let tmp_dir_path = tmp_dir.path();
1290        let paths = [
1291            "ds-test/src",
1292            "ds-test/demo",
1293            "ds-test/demo/demo.sol",
1294            "ds-test/src/test.sol",
1295            "openzeppelin/src/interfaces/c.sol",
1296            "openzeppelin/src/token/ERC/c.sol",
1297            "standards/src/interfaces/iweth.sol",
1298            "uniswapv2/src",
1299        ];
1300        mkdir_or_touch(tmp_dir_path, &paths[..]);
1301
1302        let path = tmp_dir_path.display().to_string();
1303        let mut remappings = Remapping::find_many(path);
1304        remappings.sort_unstable();
1305
1306        let mut expected = vec![
1307            Remapping {
1308                context: None,
1309                name: "ds-test/".to_string(),
1310                path: to_str(tmp_dir_path.join("ds-test/src")),
1311            },
1312            Remapping {
1313                context: None,
1314                name: "openzeppelin/".to_string(),
1315                path: to_str(tmp_dir_path.join("openzeppelin/src")),
1316            },
1317            Remapping {
1318                context: None,
1319                name: "standards/".to_string(),
1320                path: to_str(tmp_dir_path.join("standards/src")),
1321            },
1322        ];
1323        expected.sort_unstable();
1324        pretty_assertions::assert_eq!(remappings, expected);
1325    }
1326
1327    #[test]
1328    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1329    fn hardhat_remappings() {
1330        let tmp_dir = tempdir("node_modules").unwrap();
1331        let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
1332        let paths = [
1333            "node_modules/@aave/aave-token/contracts/token/AaveToken.sol",
1334            "node_modules/@aave/governance-v2/contracts/governance/Executor.sol",
1335            "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/",
1336            "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol",
1337            "node_modules/@ensdomains/ens/contracts/contract.sol",
1338            "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/",
1339            "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/
1340            ModifierDefinitions.sol",
1341            "node_modules/@openzeppelin/contracts/tokens/contract.sol",
1342            "node_modules/@openzeppelin/contracts/access/contract.sol",
1343            "node_modules/eth-gas-reporter/mock/contracts/ConvertLib.sol",
1344            "node_modules/eth-gas-reporter/mock/test/TestMetacoin.sol",
1345        ];
1346        mkdir_or_touch(tmp_dir.path(), &paths[..]);
1347        let mut remappings = Remapping::find_many(&tmp_dir_node_modules);
1348        remappings.sort_unstable();
1349        let mut expected = vec![
1350            Remapping {
1351                context: None,
1352                name: "@aave/".to_string(),
1353                path: to_str(tmp_dir_node_modules.join("@aave")),
1354            },
1355            Remapping {
1356                context: None,
1357                name: "@ensdomains/".to_string(),
1358                path: to_str(tmp_dir_node_modules.join("@ensdomains")),
1359            },
1360            Remapping {
1361                context: None,
1362                name: "@openzeppelin/".to_string(),
1363                path: to_str(tmp_dir_node_modules.join("@openzeppelin")),
1364            },
1365            Remapping {
1366                context: None,
1367                name: "eth-gas-reporter/".to_string(),
1368                path: to_str(tmp_dir_node_modules.join("eth-gas-reporter")),
1369            },
1370        ];
1371        expected.sort_unstable();
1372        pretty_assertions::assert_eq!(remappings, expected);
1373    }
1374
1375    #[test]
1376    fn can_determine_nested_window() {
1377        let a = Path::new(
1378            "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib",
1379        );
1380        let b = Path::new(
1381            "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib/ds-test/src"
1382        );
1383        assert_eq!(next_nested_window(a, b),Path::new(
1384            "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib/ds-test"
1385        ));
1386    }
1387
1388    #[test]
1389    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
1390    fn find_openzeppelin_remapping() {
1391        let tmp_dir = tempdir("lib").unwrap();
1392        let tmp_dir_path = tmp_dir.path();
1393        let paths = [
1394            "lib/ds-test/src/test.sol",
1395            "lib/forge-std/src/test.sol",
1396            "openzeppelin/contracts/interfaces/c.sol",
1397        ];
1398        mkdir_or_touch(tmp_dir_path, &paths[..]);
1399
1400        let path = tmp_dir_path.display().to_string();
1401        let mut remappings = Remapping::find_many(path);
1402        remappings.sort_unstable();
1403
1404        let mut expected = vec![
1405            Remapping {
1406                context: None,
1407                name: "ds-test/".to_string(),
1408                path: to_str(tmp_dir_path.join("lib/ds-test/src")),
1409            },
1410            Remapping {
1411                context: None,
1412                name: "openzeppelin/".to_string(),
1413                path: to_str(tmp_dir_path.join("openzeppelin/contracts")),
1414            },
1415            Remapping {
1416                context: None,
1417                name: "forge-std/".to_string(),
1418                path: to_str(tmp_dir_path.join("lib/forge-std/src")),
1419            },
1420        ];
1421        expected.sort_unstable();
1422        pretty_assertions::assert_eq!(remappings, expected);
1423    }
1424}