foundry_compilers_artifacts_solc/remappings/
find.rs

1use super::Remapping;
2use foundry_compilers_core::utils;
3use rayon::prelude::*;
4use std::{
5    collections::{btree_map::Entry, BTreeMap, HashSet},
6    fs::FileType,
7    path::{Path, PathBuf},
8    sync::Mutex,
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
16impl Remapping {
17    /// Attempts to autodetect all remappings given a certain root path.
18    ///
19    /// See [`Self::find_many`] for more information.
20    pub fn find_many_str(path: &Path) -> Vec<String> {
21        Self::find_many(path).into_iter().map(|r| r.to_string()).collect()
22    }
23
24    /// Attempts to autodetect all remappings given a certain root path.
25    ///
26    /// This will recursively scan all subdirectories of the root path, if a subdirectory contains a
27    /// solidity file then this a candidate for a remapping. The name of the remapping will be the
28    /// folder name.
29    ///
30    /// However, there are additional rules/assumptions when it comes to determining if a candidate
31    /// should in fact be a remapping:
32    ///
33    /// All names and paths end with a trailing "/"
34    ///
35    /// The name of the remapping will be the parent folder of a solidity file, unless the folder is
36    /// named `src`, `lib` or `contracts` in which case the name of the remapping will be the parent
37    /// folder's name of `src`, `lib`, `contracts`: The remapping of `repo1/src/contract.sol` is
38    /// `name: "repo1/", path: "repo1/src/"`
39    ///
40    /// Nested remappings need to be separated by `src`, `lib` or `contracts`, The remapping of
41    /// `repo1/lib/ds-math/src/contract.sol` is `name: "ds-math/", "repo1/lib/ds-math/src/"`
42    ///
43    /// Remapping detection is primarily designed for dapptool's rules for lib folders, however, we
44    /// attempt to detect and optimize various folder structures commonly used in `node_modules`
45    /// dependencies. For those the same rules apply. In addition, we try to unify all
46    /// remappings discovered according to the rules mentioned above, so that layouts like:
47    /// ```text
48    ///   @aave/
49    ///   ├─ governance/
50    ///   │  ├─ contracts/
51    ///   ├─ protocol-v2/
52    ///   │  ├─ contracts/
53    /// ```
54    ///
55    /// which would be multiple rededications according to our rules ("governance", "protocol-v2"),
56    /// are unified into `@aave` by looking at their common ancestor, the root of this subdirectory
57    /// (`@aave`)
58    #[instrument(level = "trace", name = "Remapping::find_many")]
59    pub fn find_many(dir: &Path) -> Vec<Self> {
60        /// prioritize
61        ///   - ("a", "1/2") over ("a", "1/2/3")
62        ///   - if a path ends with `src`
63        fn insert_prioritized(
64            mappings: &mut BTreeMap<String, PathBuf>,
65            key: String,
66            path: PathBuf,
67        ) {
68            match mappings.entry(key) {
69                Entry::Occupied(mut e) => {
70                    if e.get().components().count() > path.components().count()
71                        || (path.ends_with(DAPPTOOLS_CONTRACTS_DIR)
72                            && !e.get().ends_with(DAPPTOOLS_CONTRACTS_DIR))
73                    {
74                        e.insert(path);
75                    }
76                }
77                Entry::Vacant(e) => {
78                    e.insert(path);
79                }
80            }
81        }
82
83        let is_inside_node_modules = dir.ends_with("node_modules");
84        let visited_symlink_dirs = Mutex::new(HashSet::new());
85
86        // iterate over all dirs that are children of the root
87        let candidates = read_dir(dir)
88            .filter(|(_, file_type, _)| file_type.is_dir())
89            .collect::<Vec<_>>()
90            .par_iter()
91            .flat_map_iter(|(dir, _, _)| {
92                find_remapping_candidates(
93                    dir,
94                    dir,
95                    0,
96                    is_inside_node_modules,
97                    &visited_symlink_dirs,
98                )
99            })
100            .collect::<Vec<_>>();
101
102        // all combined remappings from all subdirs
103        let mut all_remappings = BTreeMap::new();
104        for candidate in candidates {
105            if let Some(name) = candidate.window_start.file_name().and_then(|s| s.to_str()) {
106                insert_prioritized(&mut all_remappings, format!("{name}/"), candidate.source_dir);
107            }
108        }
109
110        all_remappings
111            .into_iter()
112            .map(|(name, path)| Self { context: None, name, path: format!("{}/", path.display()) })
113            .collect()
114    }
115}
116
117#[derive(Clone, Debug)]
118struct Candidate {
119    /// dir that opened the window
120    window_start: PathBuf,
121    /// dir that contains the solidity file
122    source_dir: PathBuf,
123    /// number of the current nested dependency
124    window_level: usize,
125}
126
127impl Candidate {
128    /// There are several cases where multiple candidates are detected for the same level
129    ///
130    /// # Example - Dapptools style
131    ///
132    /// Another directory next to a `src` dir:
133    ///  ```text
134    ///  ds-test/
135    ///  ├── aux/demo.sol
136    ///  └── src/test.sol
137    ///  ```
138    ///  which effectively ignores the `aux` dir by prioritizing source dirs and keeps
139    ///  `ds-test/=ds-test/src/`
140    ///
141    ///
142    /// # Example - node_modules / commonly onpenzeppelin related
143    ///
144    /// The `@openzeppelin` domain can contain several nested dirs in `node_modules/@openzeppelin`.
145    /// Such as
146    ///    - `node_modules/@openzeppelin/contracts`
147    ///    - `node_modules/@openzeppelin/contracts-upgradeable`
148    ///
149    /// Which should be resolved to the top level dir `@openzeppelin`
150    ///
151    /// We also treat candidates with a `node_modules` parent directory differently and consider
152    /// them to be `hardhat` style. In which case the trailing library barrier `contracts` will be
153    /// stripped from the remapping path. This differs from dapptools style which does not include
154    /// the library barrier path `src` in the solidity import statements. For example, for
155    /// dapptools you could have
156    ///
157    /// ```text
158    /// <root>/lib/<library>
159    /// ├── src
160    ///     ├── A.sol
161    ///     ├── B.sol
162    /// ```
163    ///
164    /// with remapping `library/=library/src/`
165    ///
166    /// whereas with hardhat's import resolver the import statement
167    ///
168    /// ```text
169    /// <root>/node_modules/<library>
170    /// ├── contracts
171    ///     ├── A.sol
172    ///     ├── B.sol
173    /// ```
174    /// with the simple remapping `library/=library/` because hardhat's lib resolver essentially
175    /// joins the import path inside a solidity file with the `nodes_modules` folder when it tries
176    /// to find an imported solidity file. For example
177    ///
178    /// ```solidity
179    /// import "hardhat/console.sol";
180    /// ```
181    /// expects the file to be at: `<root>/node_modules/hardhat/console.sol`.
182    ///
183    /// In order to support these cases, we treat the Dapptools case as the outlier, in which case
184    /// we only keep the candidate that ends with `src`
185    ///
186    ///   - `candidates`: list of viable remapping candidates
187    ///   - `current_dir`: the directory that's currently processed, like `@openzeppelin/contracts`
188    ///   - `current_level`: the number of nested library dirs encountered
189    ///   - `window_start`: This contains the root directory of the current window. In other words
190    ///     this will be the parent directory of the most recent library barrier, which will be
191    ///     `@openzeppelin` if the `current_dir` is `@openzeppelin/contracts` See also
192    ///     [`next_nested_window()`]
193    ///   - `is_inside_node_modules` whether we're inside a `node_modules` lib
194    fn merge_on_same_level(
195        candidates: &mut Vec<Self>,
196        current_dir: &Path,
197        current_level: usize,
198        window_start: PathBuf,
199        is_inside_node_modules: bool,
200    ) {
201        // if there's only a single source dir candidate then we use this
202        if let Some(pos) = candidates
203            .iter()
204            .enumerate()
205            .fold((0, None), |(mut contracts_dir_count, mut pos), (idx, c)| {
206                if c.source_dir.ends_with(DAPPTOOLS_CONTRACTS_DIR) {
207                    contracts_dir_count += 1;
208                    if contracts_dir_count == 1 {
209                        pos = Some(idx)
210                    } else {
211                        pos = None;
212                    }
213                }
214
215                (contracts_dir_count, pos)
216            })
217            .1
218        {
219            let c = candidates.remove(pos);
220            *candidates = vec![c];
221        } else {
222            // merge all candidates on the current level if the current dir is itself a candidate or
223            // there are multiple nested candidates on the current level like `current/{auth,
224            // tokens}/contracts/c.sol`
225            candidates.retain(|c| c.window_level != current_level);
226
227            let source_dir = if is_inside_node_modules {
228                window_start.clone()
229            } else {
230                current_dir.to_path_buf()
231            };
232
233            // if the window start and the source dir are the same directory we can end early if
234            // we wrongfully detect something like: `<dep>/src/lib/`
235            if current_level > 0
236                && source_dir == window_start
237                && (is_source_dir(&source_dir) || is_lib_dir(&source_dir))
238            {
239                return;
240            }
241            candidates.push(Self { window_start, source_dir, window_level: current_level });
242        }
243    }
244
245    /// Returns `true` if the `source_dir` ends with `contracts` or `contracts/src`
246    ///
247    /// This is used to detect an edge case in `"@chainlink/contracts"` which layout is
248    ///
249    /// ```text
250    /// contracts/src
251    /// ├── v0.4
252    ///     ├── Pointer.sol
253    ///     ├── interfaces
254    ///         ├── AggregatorInterface.sol
255    ///     ├── tests
256    ///         ├── BasicConsumer.sol
257    /// ├── v0.5
258    ///     ├── Chainlink.sol
259    /// ├── v0.6
260    ///     ├── AccessControlledAggregator.sol
261    /// ```
262    ///
263    /// And import commonly used is
264    ///
265    /// ```solidity
266    /// import '@chainlink/contracts/src/v0.6/interfaces/AggregatorV3Interface.sol';
267    /// ```
268    fn source_dir_ends_with_js_source(&self) -> bool {
269        self.source_dir.ends_with(JS_CONTRACTS_DIR) || self.source_dir.ends_with("contracts/src/")
270    }
271}
272
273fn is_source_dir(dir: &Path) -> bool {
274    dir.file_name()
275        .and_then(|p| p.to_str())
276        .map(|name| [DAPPTOOLS_CONTRACTS_DIR, JS_CONTRACTS_DIR].contains(&name))
277        .unwrap_or_default()
278}
279
280fn is_lib_dir(dir: &Path) -> bool {
281    dir.file_name()
282        .and_then(|p| p.to_str())
283        .map(|name| [DAPPTOOLS_LIB_DIR, JS_LIB_DIR].contains(&name))
284        .unwrap_or_default()
285}
286
287/// Returns true if the file is _hidden_
288fn is_hidden(path: &Path) -> bool {
289    path.file_name().and_then(|p| p.to_str()).map(|s| s.starts_with('.')).unwrap_or(false)
290}
291
292/// Finds all remappings in the directory recursively
293///
294/// Note: this supports symlinks and will short-circuit if a symlink dir has already been visited,
295/// this can occur in pnpm setups: <https://github.com/foundry-rs/foundry/issues/7820>
296fn find_remapping_candidates(
297    current_dir: &Path,
298    open: &Path,
299    current_level: usize,
300    is_inside_node_modules: bool,
301    visited_symlink_dirs: &Mutex<HashSet<PathBuf>>,
302) -> Vec<Candidate> {
303    trace!("find_remapping_candidates({})", current_dir.display());
304
305    // this is a marker if the current root is a candidate for a remapping
306    let mut is_candidate = false;
307
308    // scan all entries in the current dir
309    let mut search = Vec::new();
310    for (subdir, file_type, path_is_symlink) in read_dir(current_dir) {
311        // found a solidity file directly the current dir
312        if !is_candidate && file_type.is_file() && subdir.extension() == Some("sol".as_ref()) {
313            is_candidate = true;
314        } else if file_type.is_dir() {
315            // if the dir is a symlink to a parent dir we short circuit here
316            // `walkdir` will catch symlink loops, but this check prevents that we end up scanning a
317            // workspace like
318            // ```text
319            // my-package/node_modules
320            // ├── dep/node_modules
321            //     ├── symlink to `my-package`
322            // ```
323            if path_is_symlink {
324                if let Ok(target) = utils::canonicalize(&subdir) {
325                    if !visited_symlink_dirs.lock().unwrap().insert(target.clone()) {
326                        // short-circuiting if we've already visited the symlink
327                        return Vec::new();
328                    }
329                    // the symlink points to a parent dir of the current window
330                    if open.components().count() > target.components().count()
331                        && utils::common_ancestor(open, &target).is_some()
332                    {
333                        // short-circuiting
334                        return Vec::new();
335                    }
336                }
337            }
338
339            // we skip commonly used subdirs that should not be searched for recursively
340            if !no_recurse(&subdir) {
341                search.push(subdir);
342            }
343        }
344    }
345
346    // all found candidates
347    let mut candidates = search
348        .par_iter()
349        .flat_map_iter(|subdir| {
350            // scan the subdirectory for remappings, but we need a way to identify nested
351            // dependencies like `ds-token/lib/ds-stop/lib/ds-note/src/contract.sol`, or
352            // `oz/{tokens,auth}/{contracts, interfaces}/contract.sol` to assign
353            // the remappings to their root, we use a window that lies between two barriers. If
354            // we find a solidity file within a window, it belongs to the dir that opened the
355            // window.
356
357            // check if the subdir is a lib barrier, in which case we open a new window
358            if is_lib_dir(subdir) {
359                find_remapping_candidates(
360                    subdir,
361                    subdir,
362                    current_level + 1,
363                    is_inside_node_modules,
364                    visited_symlink_dirs,
365                )
366            } else {
367                // continue scanning with the current window
368                find_remapping_candidates(
369                    subdir,
370                    open,
371                    current_level,
372                    is_inside_node_modules,
373                    visited_symlink_dirs,
374                )
375            }
376        })
377        .collect::<Vec<_>>();
378
379    // need to find the actual next window in the event `open` is a lib dir
380    let window_start = next_nested_window(open, current_dir);
381    // finally, we need to merge, adjust candidates from the same level and open window
382    if is_candidate
383        || candidates
384            .iter()
385            .filter(|c| c.window_level == current_level && c.window_start == window_start)
386            .count()
387            > 1
388    {
389        Candidate::merge_on_same_level(
390            &mut candidates,
391            current_dir,
392            current_level,
393            window_start,
394            is_inside_node_modules,
395        );
396    } else {
397        // this handles the case if there is a single nested candidate
398        if let Some(candidate) = candidates.iter_mut().find(|c| c.window_level == current_level) {
399            // we need to determine the distance from the starting point of the window to the
400            // contracts dir for cases like `current/nested/contracts/c.sol` which should point to
401            // `current`
402            let distance = dir_distance(&candidate.window_start, &candidate.source_dir);
403            if distance > 1 && candidate.source_dir_ends_with_js_source() {
404                candidate.source_dir = window_start;
405            } else if !is_source_dir(&candidate.source_dir)
406                && candidate.source_dir != candidate.window_start
407            {
408                candidate.source_dir = last_nested_source_dir(open, &candidate.source_dir);
409            }
410        }
411    }
412    candidates
413}
414
415/// Returns an iterator over the entries in the directory:
416/// `(path, real_file_type, path_is_symlink)`
417///
418/// File type is the file type of the link if it is a symlink. This mimics the behavior of
419/// `walkdir` with `follow_links` set to `true`.
420fn read_dir(dir: &Path) -> impl Iterator<Item = (PathBuf, FileType, bool)> {
421    std::fs::read_dir(dir)
422        .into_iter()
423        .flatten()
424        .filter_map(Result::ok)
425        .filter_map(|e| {
426            let path = e.path();
427            let mut ft = e.file_type().ok()?;
428            let path_is_symlink = ft.is_symlink();
429            if path_is_symlink {
430                ft = std::fs::metadata(&path).ok()?.file_type();
431            }
432            Some((path, ft, path_is_symlink))
433        })
434        .filter(|(p, _, _)| !is_hidden(p))
435}
436
437fn no_recurse(dir: &Path) -> bool {
438    dir.ends_with("tests") || dir.ends_with("test") || dir.ends_with("demo")
439}
440
441/// Counts the number of components between `root` and `current`
442/// `dir_distance("root/a", "root/a/b/c") == 2`
443fn dir_distance(root: &Path, current: &Path) -> usize {
444    if root == current {
445        return 0;
446    }
447    if let Ok(rem) = current.strip_prefix(root) {
448        rem.components().count()
449    } else {
450        0
451    }
452}
453
454/// This finds the next window between `root` and `current`
455/// If `root` ends with a `lib` component then start joining components from `current` until no
456/// valid window opener is found
457fn next_nested_window(root: &Path, current: &Path) -> PathBuf {
458    if !is_lib_dir(root) || root == current {
459        return root.to_path_buf();
460    }
461    if let Ok(rem) = current.strip_prefix(root) {
462        let mut p = root.to_path_buf();
463        for c in rem.components() {
464            let next = p.join(c);
465            if !is_lib_dir(&next) || !next.ends_with(JS_CONTRACTS_DIR) {
466                return next;
467            }
468            p = next
469        }
470    }
471    root.to_path_buf()
472}
473
474/// Finds the last valid source directory in the window (root -> dir)
475fn last_nested_source_dir(root: &Path, dir: &Path) -> PathBuf {
476    if is_source_dir(dir) {
477        return dir.to_path_buf();
478    }
479    let mut p = dir;
480    while let Some(parent) = p.parent() {
481        if parent == root {
482            return root.to_path_buf();
483        }
484        if is_source_dir(parent) {
485            return parent.to_path_buf();
486        }
487        p = parent;
488    }
489    root.to_path_buf()
490}
491
492#[cfg(test)]
493mod tests {
494    use super::{super::tests::*, *};
495    use foundry_compilers_core::utils::{mkdir_or_touch, tempdir, touch};
496    use similar_asserts::assert_eq;
497
498    /// Helper function for converting PathBufs to remapping strings.
499    fn to_str(p: std::path::PathBuf) -> String {
500        format!("{}/", p.display())
501    }
502
503    #[test]
504    fn can_determine_nested_window() {
505        let a = Path::new(
506            "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib",
507        );
508        let b = Path::new(
509            "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib/ds-test/src"
510        );
511        assert_eq!(next_nested_window(a, b),Path::new(
512            "/var/folders/l5/lprhf87s6xv8djgd017f0b2h0000gn/T/lib.Z6ODLZJQeJQa/repo1/lib/ds-test"
513        ));
514    }
515
516    #[test]
517    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
518    fn find_remapping_dapptools() {
519        let tmp_dir = tempdir("lib").unwrap();
520        let tmp_dir_path = tmp_dir.path();
521        let paths = ["repo1/src/", "repo1/src/contract.sol"];
522        mkdir_or_touch(tmp_dir_path, &paths[..]);
523
524        let path = tmp_dir_path.join("repo1").display().to_string();
525        let remappings = Remapping::find_many(tmp_dir_path);
526        // repo1/=lib/repo1/src
527        assert_eq!(remappings.len(), 1);
528
529        assert_eq!(remappings[0].name, "repo1/");
530        assert_eq!(remappings[0].path, format!("{path}/src/"));
531    }
532
533    #[test]
534    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
535    fn can_resolve_contract_dir_combinations() {
536        let tmp_dir = tempdir("demo").unwrap();
537        let paths =
538            ["lib/timeless/src/lib/A.sol", "lib/timeless/src/B.sol", "lib/timeless/src/test/C.sol"];
539        mkdir_or_touch(tmp_dir.path(), &paths[..]);
540
541        let tmp_dir_path = tmp_dir.path().join("lib");
542        let remappings = Remapping::find_many(&tmp_dir_path);
543        let expected = vec![Remapping {
544            context: None,
545            name: "timeless/".to_string(),
546            path: to_str(tmp_dir_path.join("timeless/src")),
547        }];
548        assert_eq!(remappings, expected);
549    }
550
551    #[test]
552    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
553    fn can_resolve_geb_remappings() {
554        let tmp_dir = tempdir("geb").unwrap();
555        let paths = [
556            "lib/ds-token/src/test/Contract.sol",
557            "lib/ds-token/lib/ds-test/src/Contract.sol",
558            "lib/ds-token/lib/ds-test/aux/Contract.sol",
559            "lib/ds-token/lib/ds-stop/lib/ds-test/src/Contract.sol",
560            "lib/ds-token/lib/ds-stop/lib/ds-note/src/Contract.sol",
561            "lib/ds-token/lib/ds-math/lib/ds-test/aux/Contract.sol",
562            "lib/ds-token/lib/ds-math/src/Contract.sol",
563            "lib/ds-token/lib/ds-stop/lib/ds-test/aux/Contract.sol",
564            "lib/ds-token/lib/ds-stop/lib/ds-note/lib/ds-test/src/Contract.sol",
565            "lib/ds-token/lib/ds-math/lib/ds-test/src/Contract.sol",
566            "lib/ds-token/lib/ds-stop/lib/ds-auth/lib/ds-test/src/Contract.sol",
567            "lib/ds-token/lib/ds-stop/src/Contract.sol",
568            "lib/ds-token/src/Contract.sol",
569            "lib/ds-token/lib/erc20/src/Contract.sol",
570            "lib/ds-token/lib/ds-stop/lib/ds-auth/lib/ds-test/aux/Contract.sol",
571            "lib/ds-token/lib/ds-stop/lib/ds-auth/src/Contract.sol",
572            "lib/ds-token/lib/ds-stop/lib/ds-note/lib/ds-test/aux/Contract.sol",
573        ];
574        mkdir_or_touch(tmp_dir.path(), &paths[..]);
575
576        let tmp_dir_path = tmp_dir.path().join("lib");
577        let mut remappings = Remapping::find_many(&tmp_dir_path);
578        remappings.sort_unstable();
579        let mut expected = vec![
580            Remapping {
581                context: None,
582                name: "ds-auth/".to_string(),
583                path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/lib/ds-auth/src")),
584            },
585            Remapping {
586                context: None,
587                name: "ds-math/".to_string(),
588                path: to_str(tmp_dir_path.join("ds-token/lib/ds-math/src")),
589            },
590            Remapping {
591                context: None,
592                name: "ds-note/".to_string(),
593                path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/lib/ds-note/src")),
594            },
595            Remapping {
596                context: None,
597                name: "ds-stop/".to_string(),
598                path: to_str(tmp_dir_path.join("ds-token/lib/ds-stop/src")),
599            },
600            Remapping {
601                context: None,
602                name: "ds-test/".to_string(),
603                path: to_str(tmp_dir_path.join("ds-token/lib/ds-test/src")),
604            },
605            Remapping {
606                context: None,
607                name: "ds-token/".to_string(),
608                path: to_str(tmp_dir_path.join("ds-token/src")),
609            },
610            Remapping {
611                context: None,
612                name: "erc20/".to_string(),
613                path: to_str(tmp_dir_path.join("ds-token/lib/erc20/src")),
614            },
615        ];
616        expected.sort_unstable();
617        assert_eq!(remappings, expected);
618    }
619
620    #[test]
621    fn can_resolve_nested_chainlink_remappings() {
622        let tmp_dir = tempdir("root").unwrap();
623        let paths = [
624            "@chainlink/contracts/src/v0.6/vendor/Contract.sol",
625            "@chainlink/contracts/src/v0.8/tests/Contract.sol",
626            "@chainlink/contracts/src/v0.7/Contract.sol",
627            "@chainlink/contracts/src/v0.6/Contract.sol",
628            "@chainlink/contracts/src/v0.5/Contract.sol",
629            "@chainlink/contracts/src/v0.7/tests/Contract.sol",
630            "@chainlink/contracts/src/v0.7/interfaces/Contract.sol",
631            "@chainlink/contracts/src/v0.4/tests/Contract.sol",
632            "@chainlink/contracts/src/v0.6/tests/Contract.sol",
633            "@chainlink/contracts/src/v0.5/tests/Contract.sol",
634            "@chainlink/contracts/src/v0.8/vendor/Contract.sol",
635            "@chainlink/contracts/src/v0.5/dev/Contract.sol",
636            "@chainlink/contracts/src/v0.6/examples/Contract.sol",
637            "@chainlink/contracts/src/v0.5/interfaces/Contract.sol",
638            "@chainlink/contracts/src/v0.4/interfaces/Contract.sol",
639            "@chainlink/contracts/src/v0.4/vendor/Contract.sol",
640            "@chainlink/contracts/src/v0.6/interfaces/Contract.sol",
641            "@chainlink/contracts/src/v0.7/dev/Contract.sol",
642            "@chainlink/contracts/src/v0.8/dev/Contract.sol",
643            "@chainlink/contracts/src/v0.5/vendor/Contract.sol",
644            "@chainlink/contracts/src/v0.7/vendor/Contract.sol",
645            "@chainlink/contracts/src/v0.4/Contract.sol",
646            "@chainlink/contracts/src/v0.8/interfaces/Contract.sol",
647            "@chainlink/contracts/src/v0.6/dev/Contract.sol",
648        ];
649        mkdir_or_touch(tmp_dir.path(), &paths[..]);
650        let remappings = Remapping::find_many(tmp_dir.path());
651
652        let expected = vec![Remapping {
653            context: None,
654            name: "@chainlink/".to_string(),
655            path: to_str(tmp_dir.path().join("@chainlink")),
656        }];
657        assert_eq!(remappings, expected);
658    }
659
660    #[test]
661    fn can_resolve_oz_upgradeable_remappings() {
662        let tmp_dir = tempdir("root").unwrap();
663        let paths = [
664            "@openzeppelin/contracts-upgradeable/proxy/ERC1967/Contract.sol",
665            "@openzeppelin/contracts-upgradeable/token/ERC1155/Contract.sol",
666            "@openzeppelin/contracts/token/ERC777/Contract.sol",
667            "@openzeppelin/contracts/token/ERC721/presets/Contract.sol",
668            "@openzeppelin/contracts/interfaces/Contract.sol",
669            "@openzeppelin/contracts-upgradeable/token/ERC777/presets/Contract.sol",
670            "@openzeppelin/contracts/token/ERC1155/extensions/Contract.sol",
671            "@openzeppelin/contracts/proxy/Contract.sol",
672            "@openzeppelin/contracts/proxy/utils/Contract.sol",
673            "@openzeppelin/contracts-upgradeable/security/Contract.sol",
674            "@openzeppelin/contracts-upgradeable/utils/Contract.sol",
675            "@openzeppelin/contracts/token/ERC20/Contract.sol",
676            "@openzeppelin/contracts-upgradeable/utils/introspection/Contract.sol",
677            "@openzeppelin/contracts/metatx/Contract.sol",
678            "@openzeppelin/contracts/utils/cryptography/Contract.sol",
679            "@openzeppelin/contracts/token/ERC20/utils/Contract.sol",
680            "@openzeppelin/contracts-upgradeable/token/ERC20/utils/Contract.sol",
681            "@openzeppelin/contracts-upgradeable/proxy/Contract.sol",
682            "@openzeppelin/contracts-upgradeable/token/ERC20/presets/Contract.sol",
683            "@openzeppelin/contracts-upgradeable/utils/math/Contract.sol",
684            "@openzeppelin/contracts-upgradeable/utils/escrow/Contract.sol",
685            "@openzeppelin/contracts/governance/extensions/Contract.sol",
686            "@openzeppelin/contracts-upgradeable/interfaces/Contract.sol",
687            "@openzeppelin/contracts/proxy/transparent/Contract.sol",
688            "@openzeppelin/contracts/utils/structs/Contract.sol",
689            "@openzeppelin/contracts-upgradeable/access/Contract.sol",
690            "@openzeppelin/contracts/governance/compatibility/Contract.sol",
691            "@openzeppelin/contracts/governance/Contract.sol",
692            "@openzeppelin/contracts-upgradeable/governance/extensions/Contract.sol",
693            "@openzeppelin/contracts/security/Contract.sol",
694            "@openzeppelin/contracts-upgradeable/metatx/Contract.sol",
695            "@openzeppelin/contracts-upgradeable/token/ERC721/utils/Contract.sol",
696            "@openzeppelin/contracts/token/ERC721/utils/Contract.sol",
697            "@openzeppelin/contracts-upgradeable/governance/compatibility/Contract.sol",
698            "@openzeppelin/contracts/token/common/Contract.sol",
699            "@openzeppelin/contracts/proxy/beacon/Contract.sol",
700            "@openzeppelin/contracts-upgradeable/token/ERC721/Contract.sol",
701            "@openzeppelin/contracts-upgradeable/proxy/beacon/Contract.sol",
702            "@openzeppelin/contracts/token/ERC1155/utils/Contract.sol",
703            "@openzeppelin/contracts/token/ERC777/presets/Contract.sol",
704            "@openzeppelin/contracts-upgradeable/token/ERC20/Contract.sol",
705            "@openzeppelin/contracts-upgradeable/utils/structs/Contract.sol",
706            "@openzeppelin/contracts/utils/escrow/Contract.sol",
707            "@openzeppelin/contracts/utils/Contract.sol",
708            "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/Contract.sol",
709            "@openzeppelin/contracts/token/ERC721/extensions/Contract.sol",
710            "@openzeppelin/contracts-upgradeable/token/ERC777/Contract.sol",
711            "@openzeppelin/contracts/token/ERC1155/presets/Contract.sol",
712            "@openzeppelin/contracts/token/ERC721/Contract.sol",
713            "@openzeppelin/contracts/token/ERC1155/Contract.sol",
714            "@openzeppelin/contracts-upgradeable/governance/Contract.sol",
715            "@openzeppelin/contracts/token/ERC20/extensions/Contract.sol",
716            "@openzeppelin/contracts-upgradeable/utils/cryptography/Contract.sol",
717            "@openzeppelin/contracts-upgradeable/token/ERC1155/presets/Contract.sol",
718            "@openzeppelin/contracts/access/Contract.sol",
719            "@openzeppelin/contracts/governance/utils/Contract.sol",
720            "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/Contract.sol",
721            "@openzeppelin/contracts-upgradeable/token/common/Contract.sol",
722            "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/Contract.sol",
723            "@openzeppelin/contracts/proxy/ERC1967/Contract.sol",
724            "@openzeppelin/contracts/finance/Contract.sol",
725            "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/Contract.sol",
726            "@openzeppelin/contracts-upgradeable/governance/utils/Contract.sol",
727            "@openzeppelin/contracts-upgradeable/proxy/utils/Contract.sol",
728            "@openzeppelin/contracts/token/ERC20/presets/Contract.sol",
729            "@openzeppelin/contracts/utils/math/Contract.sol",
730            "@openzeppelin/contracts-upgradeable/token/ERC721/presets/Contract.sol",
731            "@openzeppelin/contracts-upgradeable/finance/Contract.sol",
732            "@openzeppelin/contracts/utils/introspection/Contract.sol",
733        ];
734        mkdir_or_touch(tmp_dir.path(), &paths[..]);
735        let remappings = Remapping::find_many(tmp_dir.path());
736
737        let expected = vec![Remapping {
738            context: None,
739            name: "@openzeppelin/".to_string(),
740            path: to_str(tmp_dir.path().join("@openzeppelin")),
741        }];
742        assert_eq!(remappings, expected);
743    }
744
745    #[test]
746    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
747    fn recursive_remappings() {
748        let tmp_dir = tempdir("lib").unwrap();
749        let tmp_dir_path = tmp_dir.path();
750        let paths = [
751            "repo1/src/contract.sol",
752            "repo1/lib/ds-test/src/test.sol",
753            "repo1/lib/ds-math/src/contract.sol",
754            "repo1/lib/ds-math/lib/ds-test/src/test.sol",
755            "repo1/lib/guni-lev/src/contract.sol",
756            "repo1/lib/solmate/src/auth/contract.sol",
757            "repo1/lib/solmate/src/tokens/contract.sol",
758            "repo1/lib/solmate/lib/ds-test/src/test.sol",
759            "repo1/lib/solmate/lib/ds-test/demo/demo.sol",
760            "repo1/lib/openzeppelin-contracts/contracts/access/AccessControl.sol",
761            "repo1/lib/ds-token/lib/ds-stop/src/contract.sol",
762            "repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src/contract.sol",
763        ];
764        mkdir_or_touch(tmp_dir_path, &paths[..]);
765
766        let mut remappings = Remapping::find_many(tmp_dir_path);
767        remappings.sort_unstable();
768
769        let mut expected = vec![
770            Remapping {
771                context: None,
772                name: "repo1/".to_string(),
773                path: to_str(tmp_dir_path.join("repo1").join("src")),
774            },
775            Remapping {
776                context: None,
777                name: "ds-math/".to_string(),
778                path: to_str(tmp_dir_path.join("repo1").join("lib").join("ds-math").join("src")),
779            },
780            Remapping {
781                context: None,
782                name: "ds-test/".to_string(),
783                path: to_str(tmp_dir_path.join("repo1").join("lib").join("ds-test").join("src")),
784            },
785            Remapping {
786                context: None,
787                name: "guni-lev/".to_string(),
788                path: to_str(tmp_dir_path.join("repo1/lib/guni-lev").join("src")),
789            },
790            Remapping {
791                context: None,
792                name: "solmate/".to_string(),
793                path: to_str(tmp_dir_path.join("repo1/lib/solmate").join("src")),
794            },
795            Remapping {
796                context: None,
797                name: "openzeppelin-contracts/".to_string(),
798                path: to_str(tmp_dir_path.join("repo1/lib/openzeppelin-contracts/contracts")),
799            },
800            Remapping {
801                context: None,
802                name: "ds-stop/".to_string(),
803                path: to_str(tmp_dir_path.join("repo1/lib/ds-token/lib/ds-stop/src")),
804            },
805            Remapping {
806                context: None,
807                name: "ds-note/".to_string(),
808                path: to_str(tmp_dir_path.join("repo1/lib/ds-token/lib/ds-stop/lib/ds-note/src")),
809            },
810        ];
811        expected.sort_unstable();
812        assert_eq!(remappings, expected);
813    }
814
815    #[test]
816    fn remappings() {
817        let tmp_dir = tempdir("tmp").unwrap();
818        let tmp_dir_path = tmp_dir.path().join("lib");
819        let repo1 = tmp_dir_path.join("src_repo");
820        let repo2 = tmp_dir_path.join("contracts_repo");
821
822        let dir1 = repo1.join("src");
823        std::fs::create_dir_all(&dir1).unwrap();
824
825        let dir2 = repo2.join("contracts");
826        std::fs::create_dir_all(&dir2).unwrap();
827
828        let contract1 = dir1.join("contract.sol");
829        touch(&contract1).unwrap();
830
831        let contract2 = dir2.join("contract.sol");
832        touch(&contract2).unwrap();
833
834        let mut remappings = Remapping::find_many(&tmp_dir_path);
835        remappings.sort_unstable();
836        let mut expected = vec![
837            Remapping {
838                context: None,
839                name: "src_repo/".to_string(),
840                path: format!("{}/", dir1.into_os_string().into_string().unwrap()),
841            },
842            Remapping {
843                context: None,
844                name: "contracts_repo/".to_string(),
845                path: format!(
846                    "{}/",
847                    repo2.join("contracts").into_os_string().into_string().unwrap()
848                ),
849            },
850        ];
851        expected.sort_unstable();
852        assert_eq!(remappings, expected);
853    }
854
855    #[test]
856    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
857    fn simple_dapptools_remappings() {
858        let tmp_dir = tempdir("lib").unwrap();
859        let tmp_dir_path = tmp_dir.path();
860        let paths = [
861            "ds-test/src",
862            "ds-test/demo",
863            "ds-test/demo/demo.sol",
864            "ds-test/src/test.sol",
865            "openzeppelin/src/interfaces/c.sol",
866            "openzeppelin/src/token/ERC/c.sol",
867            "standards/src/interfaces/iweth.sol",
868            "uniswapv2/src",
869        ];
870        mkdir_or_touch(tmp_dir_path, &paths[..]);
871
872        let mut remappings = Remapping::find_many(tmp_dir_path);
873        remappings.sort_unstable();
874
875        let mut expected = vec![
876            Remapping {
877                context: None,
878                name: "ds-test/".to_string(),
879                path: to_str(tmp_dir_path.join("ds-test/src")),
880            },
881            Remapping {
882                context: None,
883                name: "openzeppelin/".to_string(),
884                path: to_str(tmp_dir_path.join("openzeppelin/src")),
885            },
886            Remapping {
887                context: None,
888                name: "standards/".to_string(),
889                path: to_str(tmp_dir_path.join("standards/src")),
890            },
891        ];
892        expected.sort_unstable();
893        assert_eq!(remappings, expected);
894    }
895
896    #[test]
897    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
898    fn hardhat_remappings() {
899        let tmp_dir = tempdir("node_modules").unwrap();
900        let tmp_dir_node_modules = tmp_dir.path().join("node_modules");
901        let paths = [
902            "node_modules/@aave/aave-token/contracts/token/AaveToken.sol",
903            "node_modules/@aave/governance-v2/contracts/governance/Executor.sol",
904            "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/",
905            "node_modules/@aave/protocol-v2/contracts/protocol/lendingpool/LendingPool.sol",
906            "node_modules/@ensdomains/ens/contracts/contract.sol",
907            "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/",
908            "node_modules/prettier-plugin-solidity/tests/format/ModifierDefinitions/
909            ModifierDefinitions.sol",
910            "node_modules/@openzeppelin/contracts/tokens/contract.sol",
911            "node_modules/@openzeppelin/contracts/access/contract.sol",
912            "node_modules/eth-gas-reporter/mock/contracts/ConvertLib.sol",
913            "node_modules/eth-gas-reporter/mock/test/TestMetacoin.sol",
914        ];
915        mkdir_or_touch(tmp_dir.path(), &paths[..]);
916        let mut remappings = Remapping::find_many(&tmp_dir_node_modules);
917        remappings.sort_unstable();
918        let mut expected = vec![
919            Remapping {
920                context: None,
921                name: "@aave/".to_string(),
922                path: to_str(tmp_dir_node_modules.join("@aave")),
923            },
924            Remapping {
925                context: None,
926                name: "@ensdomains/".to_string(),
927                path: to_str(tmp_dir_node_modules.join("@ensdomains")),
928            },
929            Remapping {
930                context: None,
931                name: "@openzeppelin/".to_string(),
932                path: to_str(tmp_dir_node_modules.join("@openzeppelin")),
933            },
934            Remapping {
935                context: None,
936                name: "eth-gas-reporter/".to_string(),
937                path: to_str(tmp_dir_node_modules.join("eth-gas-reporter")),
938            },
939        ];
940        expected.sort_unstable();
941        assert_eq!(remappings, expected);
942    }
943
944    #[test]
945    #[cfg_attr(windows, ignore = "Windows remappings #2347")]
946    fn find_openzeppelin_remapping() {
947        let tmp_dir = tempdir("lib").unwrap();
948        let tmp_dir_path = tmp_dir.path();
949        let paths = [
950            "lib/ds-test/src/test.sol",
951            "lib/forge-std/src/test.sol",
952            "openzeppelin/contracts/interfaces/c.sol",
953        ];
954        mkdir_or_touch(tmp_dir_path, &paths[..]);
955
956        let path = tmp_dir_path.display().to_string();
957        let mut remappings = Remapping::find_many(path.as_ref());
958        remappings.sort_unstable();
959
960        let mut expected = vec![
961            Remapping {
962                context: None,
963                name: "ds-test/".to_string(),
964                path: to_str(tmp_dir_path.join("lib/ds-test/src")),
965            },
966            Remapping {
967                context: None,
968                name: "openzeppelin/".to_string(),
969                path: to_str(tmp_dir_path.join("openzeppelin/contracts")),
970            },
971            Remapping {
972                context: None,
973                name: "forge-std/".to_string(),
974                path: to_str(tmp_dir_path.join("lib/forge-std/src")),
975            },
976        ];
977        expected.sort_unstable();
978        assert_eq!(remappings, expected);
979    }
980}