foundry_compilers_core/utils/
mod.rs

1//! Utility functions
2
3use crate::error::{SolcError, SolcIoError};
4use alloy_primitives::{hex, keccak256};
5use cfg_if::cfg_if;
6use semver::{Version, VersionReq};
7use serde::{de::DeserializeOwned, Serialize};
8use std::{
9    fs,
10    io::Write,
11    ops::Range,
12    path::{Component, Path, PathBuf},
13    sync::LazyLock as Lazy,
14};
15
16#[cfg(feature = "regex")]
17mod re;
18#[cfg(feature = "regex")]
19pub use re::*;
20
21#[cfg(feature = "walkdir")]
22mod wd;
23#[cfg(feature = "walkdir")]
24pub use wd::*;
25
26/// Extensions acceptable by solc compiler.
27pub const SOLC_EXTENSIONS: &[&str] = &["sol", "yul"];
28
29/// Support for configuring the EVM version
30/// <https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/>
31pub const BYZANTIUM_SOLC: Version = Version::new(0, 4, 21);
32
33/// Bug fix for configuring the EVM version with Constantinople
34/// <https://blog.soliditylang.org/2018/03/08/solidity-0.4.21-release-announcement/>
35pub const CONSTANTINOPLE_SOLC: Version = Version::new(0, 4, 22);
36
37/// Petersburg support
38/// <https://blog.soliditylang.org/2019/03/05/solidity-0.5.5-release-announcement/>
39pub const PETERSBURG_SOLC: Version = Version::new(0, 5, 5);
40
41/// Istanbul support
42/// <https://blog.soliditylang.org/2019/12/09/solidity-0.5.14-release-announcement/>
43pub const ISTANBUL_SOLC: Version = Version::new(0, 5, 14);
44
45/// Berlin support
46/// <https://blog.soliditylang.org/2021/06/10/solidity-0.8.5-release-announcement/>
47pub const BERLIN_SOLC: Version = Version::new(0, 8, 5);
48
49/// London support
50/// <https://blog.soliditylang.org/2021/08/11/solidity-0.8.7-release-announcement/>
51pub const LONDON_SOLC: Version = Version::new(0, 8, 7);
52
53/// Paris support
54/// <https://blog.soliditylang.org/2023/02/01/solidity-0.8.18-release-announcement/>
55pub const PARIS_SOLC: Version = Version::new(0, 8, 18);
56
57/// Shanghai support
58/// <https://blog.soliditylang.org/2023/05/10/solidity-0.8.20-release-announcement/>
59pub const SHANGHAI_SOLC: Version = Version::new(0, 8, 20);
60
61/// Cancun support
62/// <https://soliditylang.org/blog/2024/01/26/solidity-0.8.24-release-announcement/>
63pub const CANCUN_SOLC: Version = Version::new(0, 8, 24);
64
65/// Prague support
66/// <https://soliditylang.org/blog/2024/09/04/solidity-0.8.27-release-announcement>
67pub const PRAGUE_SOLC: Version = Version::new(0, 8, 27);
68
69/// Osaka support
70/// <https://soliditylang.org/blog/2025/03/12/solidity-0.8.29-release-announcement>
71pub const OSAKA_SOLC: Version = Version::new(0, 8, 29);
72
73// `--base-path` was introduced in 0.6.9 <https://github.com/ethereum/solidity/releases/tag/v0.6.9>
74pub static SUPPORTS_BASE_PATH: Lazy<VersionReq> =
75    Lazy::new(|| VersionReq::parse(">=0.6.9").unwrap());
76
77// `--include-path` was introduced in 0.8.8 <https://github.com/ethereum/solidity/releases/tag/v0.8.8>
78pub static SUPPORTS_INCLUDE_PATH: Lazy<VersionReq> =
79    Lazy::new(|| VersionReq::parse(">=0.8.8").unwrap());
80
81/// A non-cryptographic hash function for creating unique identifiers.
82///
83/// The exact algorithm being used shouldn't matter.
84// See Hardhat: https://github.com/NomicFoundation/hardhat/blob/e9ab5332a5505a6d1fe9bfbc687f5f46bdff6dd7/packages/hardhat-core/src/internal/util/hash.ts#L1-L16
85#[cfg(feature = "hasher")]
86pub fn unique_hash(input: impl AsRef<[u8]>) -> String {
87    encode_hash(xxhash_rust::xxh3::xxh3_64(input.as_ref()))
88}
89
90/// A non-cryptographic hash function for creating unique identifiers.
91///
92/// See [`unique_hash`] for more details.
93#[cfg(feature = "hasher")]
94pub fn unique_hash_many(inputs: impl IntoIterator<Item = impl AsRef<[u8]>>) -> String {
95    let mut hasher = xxhash_rust::xxh3::Xxh3Default::new();
96    for input in inputs {
97        hasher.update(input.as_ref());
98    }
99    encode_hash(hasher.digest())
100}
101
102#[cfg(feature = "hasher")]
103fn encode_hash(x: u64) -> String {
104    hex::encode(x.to_be_bytes())
105}
106
107/// Move a range by a specified offset
108pub fn range_by_offset(range: &Range<usize>, offset: isize) -> Range<usize> {
109    Range {
110        start: offset.saturating_add(range.start as isize) as usize,
111        end: offset.saturating_add(range.end as isize) as usize,
112    }
113}
114
115/// Returns the source name for the given source path, the ancestors of the root path.
116///
117/// `/Users/project/sources/contract.sol` -> `sources/contracts.sol`
118pub fn source_name<'a>(source: &'a Path, root: &Path) -> &'a Path {
119    strip_prefix(source, root)
120}
121
122/// Strips `root` from `source` and returns the relative path.
123pub fn strip_prefix<'a>(source: &'a Path, root: &Path) -> &'a Path {
124    source.strip_prefix(root).unwrap_or(source)
125}
126
127/// Strips `root` from `source` and returns the relative path.
128pub fn strip_prefix_owned(source: PathBuf, root: &Path) -> PathBuf {
129    source.strip_prefix(root).map(Path::to_path_buf).unwrap_or(source)
130}
131
132/// Attempts to determine if the given source is a local, relative import.
133pub fn is_local_source_name(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> bool {
134    resolve_library(libs, source.as_ref()).is_none()
135}
136
137/// Canonicalize the path, platform-agnostic.
138///
139/// On windows this will ensure the path only consists of `/` separators.
140pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, SolcIoError> {
141    let path = path.as_ref();
142    let res = dunce::canonicalize(path);
143    #[cfg(windows)]
144    let res = res.map(|p| {
145        use path_slash::PathBufExt;
146        PathBuf::from(p.to_slash_lossy().as_ref())
147    });
148    res.map_err(|err| SolcIoError::new(err, path))
149}
150
151/// Returns a normalized Solidity file path for the given import path based on the specified
152/// directory.
153///
154/// This function resolves `./` and `../`, but, unlike [`canonicalize`], it does not resolve
155/// symbolic links.
156///
157/// The function returns an error if the normalized path does not exist in the file system.
158///
159/// See also: <https://docs.soliditylang.org/en/v0.8.23/path-resolution.html>
160pub fn normalize_solidity_import_path(
161    directory: &Path,
162    import_path: &Path,
163) -> Result<PathBuf, SolcIoError> {
164    let original = directory.join(import_path);
165    let cleaned = clean_solidity_path(&original);
166
167    // this is to align the behavior with `canonicalize`
168    let normalized = dunce::simplified(&cleaned);
169    #[cfg(windows)]
170    let normalized = {
171        use path_slash::PathExt;
172        PathBuf::from(normalized.to_slash_lossy().as_ref())
173    };
174    #[cfg(not(windows))]
175    let normalized = PathBuf::from(normalized);
176
177    // checks if the path exists without reading its content and obtains an io error if it doesn't.
178    let _ = normalized.metadata().map_err(|err| SolcIoError::new(err, original))?;
179    Ok(normalized)
180}
181
182// This function lexically cleans the given path.
183//
184// It performs the following transformations for the path:
185//
186// * Resolves references (current directories (`.`) and parent (`..`) directories).
187// * Reduces repeated separators to a single separator (e.g., from `//` to `/`).
188//
189// This transformation is lexical, not involving the file system, which means it does not account
190// for symlinks. This approach has a caveat. For example, consider a filesystem-accessible path
191// `a/b/../c.sol` passed to this function. It returns `a/c.sol`. However, if `b` is a symlink,
192// `a/c.sol` might not be accessible in the filesystem in some environments. Despite this, it's
193// unlikely that this will pose a problem for our intended use.
194//
195// # How it works
196//
197// The function splits the given path into components, where each component roughly corresponds to a
198// string between separators. It then iterates over these components (starting from the leftmost
199// part of the path) to reconstruct the path. The following steps are applied to each component:
200//
201// * If the component is a current directory, it's removed.
202// * If the component is a parent directory, the following rules are applied:
203//     * If the preceding component is a normal, then both the preceding normal component and the
204//       parent directory component are removed. (Examples of normal components include `a` and `b`
205//       in `a/b`.)
206//     * Otherwise (if there is no preceding component, or if the preceding component is a parent,
207//       root, or prefix), it remains untouched.
208// * Otherwise, the component remains untouched.
209//
210// Finally, the processed components are reassembled into a path.
211fn clean_solidity_path(original_path: &Path) -> PathBuf {
212    let mut new_path = Vec::new();
213
214    for component in original_path.components() {
215        match component {
216            Component::Prefix(..) | Component::RootDir | Component::Normal(..) => {
217                new_path.push(component);
218            }
219            Component::CurDir => {}
220            Component::ParentDir => {
221                if let Some(Component::Normal(..)) = new_path.last() {
222                    new_path.pop();
223                } else {
224                    new_path.push(component);
225                }
226            }
227        }
228    }
229
230    new_path.iter().collect()
231}
232
233/// Returns the same path config but with canonicalized paths.
234///
235/// This will take care of potential symbolic linked directories.
236/// For example, the tempdir library is creating directories hosted under `/var/`, which in OS X
237/// is a symbolic link to `/private/var/`. So if when we try to resolve imports and a path is
238/// rooted in a symbolic directory we might end up with different paths for the same file, like
239/// `private/var/.../Dapp.sol` and `/var/.../Dapp.sol`
240///
241/// This canonicalizes all the paths but does not treat non existing dirs as an error
242pub fn canonicalized(path: impl Into<PathBuf>) -> PathBuf {
243    let path = path.into();
244    canonicalize(&path).unwrap_or(path)
245}
246
247/// Returns the path to the library if the source path is in fact determined to be a library path,
248/// and it exists.
249/// Note: this does not handle relative imports or remappings.
250pub fn resolve_library(libs: &[impl AsRef<Path>], source: impl AsRef<Path>) -> Option<PathBuf> {
251    let source = source.as_ref();
252    let comp = source.components().next()?;
253    match comp {
254        Component::Normal(first_dir) => {
255            // attempt to verify that the root component of this source exists under a library
256            // folder
257            for lib in libs {
258                let lib = lib.as_ref();
259                let contract = lib.join(source);
260                if contract.exists() {
261                    // contract exists in <lib>/<source>
262                    return Some(contract);
263                }
264                // check for <lib>/<first_dir>/src/name.sol
265                let contract = lib
266                    .join(first_dir)
267                    .join("src")
268                    .join(source.strip_prefix(first_dir).expect("is first component"));
269                if contract.exists() {
270                    return Some(contract);
271                }
272            }
273            None
274        }
275        Component::RootDir => Some(source.into()),
276        _ => None,
277    }
278}
279
280/// Tries to find an absolute import like `src/interfaces/IConfig.sol` in `cwd`, moving up the path
281/// until the `root` is reached.
282///
283/// If an existing file under `root` is found, this returns the path up to the `import` path and the
284/// normalized `import` path itself:
285///
286/// For example for following layout:
287///
288/// ```text
289/// <root>/mydependency/
290/// ├── src (`cwd`)
291/// │   ├── interfaces
292/// │   │   ├── IConfig.sol
293/// ```
294/// and `import` as `src/interfaces/IConfig.sol` and `cwd` as `src` this will return
295/// (`<root>/mydependency/`, `<root>/mydependency/src/interfaces/IConfig.sol`)
296pub fn resolve_absolute_library(
297    root: &Path,
298    cwd: &Path,
299    import: &Path,
300) -> Option<(PathBuf, PathBuf)> {
301    let mut parent = cwd.parent()?;
302    while parent != root {
303        if let Ok(import) = normalize_solidity_import_path(parent, import) {
304            return Some((parent.to_path_buf(), import));
305        }
306        parent = parent.parent()?;
307    }
308    None
309}
310
311/// Returns the 36 char (deprecated) fully qualified name placeholder
312///
313/// If the name is longer than 36 char, then the name gets truncated,
314/// If the name is shorter than 36 char, then the name is filled with trailing `_`
315pub fn library_fully_qualified_placeholder(name: &str) -> String {
316    name.chars().chain(std::iter::repeat('_')).take(36).collect()
317}
318
319/// Returns the library hash placeholder as `$hex(library_hash(name))$`
320pub fn library_hash_placeholder(name: impl AsRef<[u8]>) -> String {
321    let mut s = String::with_capacity(34 + 2);
322    s.push('$');
323    s.push_str(hex::Buffer::<17, false>::new().format(&library_hash(name)));
324    s.push('$');
325    s
326}
327
328/// Returns the library placeholder for the given name
329/// The placeholder is a 34 character prefix of the hex encoding of the keccak256 hash of the fully
330/// qualified library name.
331///
332/// See also <https://docs.soliditylang.org/en/develop/using-the-compiler.html#library-linking>
333pub fn library_hash(name: impl AsRef<[u8]>) -> [u8; 17] {
334    let hash = keccak256(name);
335    hash[..17].try_into().unwrap()
336}
337
338/// Find the common ancestor, if any, between the given paths
339///
340/// # Examples
341///
342/// ```
343/// use foundry_compilers_core::utils::common_ancestor_all;
344/// use std::path::{Path, PathBuf};
345///
346/// let baz = Path::new("/foo/bar/baz");
347/// let bar = Path::new("/foo/bar/bar");
348/// let foo = Path::new("/foo/bar/foo");
349/// let common = common_ancestor_all([baz, bar, foo]).unwrap();
350/// assert_eq!(common, Path::new("/foo/bar").to_path_buf());
351/// ```
352pub fn common_ancestor_all<I, P>(paths: I) -> Option<PathBuf>
353where
354    I: IntoIterator<Item = P>,
355    P: AsRef<Path>,
356{
357    let mut iter = paths.into_iter();
358    let mut ret = iter.next()?.as_ref().to_path_buf();
359    for path in iter {
360        if let Some(r) = common_ancestor(&ret, path.as_ref()) {
361            ret = r;
362        } else {
363            return None;
364        }
365    }
366    Some(ret)
367}
368
369/// Finds the common ancestor of both paths
370///
371/// # Examples
372///
373/// ```
374/// use foundry_compilers_core::utils::common_ancestor;
375/// use std::path::{Path, PathBuf};
376///
377/// let foo = Path::new("/foo/bar/foo");
378/// let bar = Path::new("/foo/bar/bar");
379/// let ancestor = common_ancestor(foo, bar).unwrap();
380/// assert_eq!(ancestor, Path::new("/foo/bar"));
381/// ```
382pub fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
383    let a = a.components();
384    let b = b.components();
385    let mut ret = PathBuf::new();
386    let mut found = false;
387    for (c1, c2) in a.zip(b) {
388        if c1 == c2 {
389            ret.push(c1);
390            found = true;
391        } else {
392            break;
393        }
394    }
395    if found {
396        Some(ret)
397    } else {
398        None
399    }
400}
401
402/// Returns the right subpath in a dir
403///
404/// Returns `<root>/<fave>` if it exists or `<root>/<alt>` does not exist,
405/// Returns `<root>/<alt>` if it exists and `<root>/<fave>` does not exist.
406pub fn find_fave_or_alt_path(root: &Path, fave: &str, alt: &str) -> PathBuf {
407    let p = root.join(fave);
408    if !p.exists() {
409        let alt = root.join(alt);
410        if alt.exists() {
411            return alt;
412        }
413    }
414    p
415}
416
417cfg_if! {
418    if #[cfg(any(feature = "async", feature = "svm-solc"))] {
419        use tokio::runtime::{Handle, Runtime};
420
421        #[derive(Debug)]
422        pub enum RuntimeOrHandle {
423            Runtime(Runtime),
424            Handle(Handle),
425        }
426
427        impl Default for RuntimeOrHandle {
428            fn default() -> Self {
429                Self::new()
430            }
431        }
432
433        impl RuntimeOrHandle {
434            pub fn new() -> Self {
435                match Handle::try_current() {
436                    Ok(handle) => Self::Handle(handle),
437                    Err(_) => Self::Runtime(Runtime::new().expect("Failed to start runtime")),
438                }
439            }
440
441            pub fn block_on<F: std::future::Future>(&self, f: F) -> F::Output {
442                match &self {
443                    Self::Runtime(runtime) => runtime.block_on(f),
444                    Self::Handle(handle) => tokio::task::block_in_place(|| handle.block_on(f)),
445                }
446            }
447        }
448    }
449}
450
451/// Creates a new named tempdir.
452#[cfg(any(test, feature = "project-util", feature = "test-utils"))]
453pub fn tempdir(name: &str) -> Result<tempfile::TempDir, SolcIoError> {
454    tempfile::Builder::new().prefix(name).tempdir().map_err(|err| SolcIoError::new(err, name))
455}
456
457/// Reads the json file and deserialize it into the provided type.
458pub fn read_json_file<T: DeserializeOwned>(path: &Path) -> Result<T, SolcError> {
459    // See: https://github.com/serde-rs/json/issues/160
460    let s = fs::read_to_string(path).map_err(|err| SolcError::io(err, path))?;
461    serde_json::from_str(&s).map_err(Into::into)
462}
463
464/// Writes serializes the provided value to JSON and writes it to a file.
465pub fn write_json_file<T: Serialize>(
466    value: &T,
467    path: &Path,
468    capacity: usize,
469) -> Result<(), SolcError> {
470    let file = fs::File::create(path).map_err(|err| SolcError::io(err, path))?;
471    let mut writer = std::io::BufWriter::with_capacity(capacity, file);
472    serde_json::to_writer(&mut writer, value)?;
473    writer.flush().map_err(|e| SolcError::io(e, path))
474}
475
476/// Creates the parent directory of the `file` and all its ancestors if it does not exist.
477///
478/// See [`fs::create_dir_all()`].
479pub fn create_parent_dir_all(file: &Path) -> Result<(), SolcError> {
480    if let Some(parent) = file.parent() {
481        fs::create_dir_all(parent).map_err(|err| {
482            SolcError::msg(format!(
483                "Failed to create artifact parent folder \"{}\": {}",
484                parent.display(),
485                err
486            ))
487        })?;
488    }
489    Ok(())
490}
491
492#[cfg(any(test, feature = "test-utils"))]
493// <https://doc.rust-lang.org/rust-by-example/std_misc/fs.html>
494pub fn touch(path: &std::path::Path) -> std::io::Result<()> {
495    match std::fs::OpenOptions::new().create(true).write(true).truncate(false).open(path) {
496        Ok(_) => Ok(()),
497        Err(e) => Err(e),
498    }
499}
500
501#[cfg(any(test, feature = "test-utils"))]
502pub fn mkdir_or_touch(tmp: &std::path::Path, paths: &[&str]) {
503    for path in paths {
504        if let Some(parent) = Path::new(path).parent() {
505            std::fs::create_dir_all(tmp.join(parent)).unwrap();
506        }
507        if path.ends_with(".sol") {
508            let path = tmp.join(path);
509            touch(&path).unwrap();
510        } else {
511            let path: PathBuf = tmp.join(path);
512            std::fs::create_dir_all(path).unwrap();
513        }
514    }
515}
516
517#[cfg(test)]
518mod tests {
519    pub use super::*;
520    pub use std::fs::{create_dir_all, File};
521
522    #[test]
523    fn can_create_parent_dirs_with_ext() {
524        let tmp_dir = tempdir("out").unwrap();
525        let path = tmp_dir.path().join("IsolationModeMagic.sol/IsolationModeMagic.json");
526        create_parent_dir_all(&path).unwrap();
527        assert!(path.parent().unwrap().is_dir());
528    }
529
530    #[test]
531    fn can_create_parent_dirs_versioned() {
532        let tmp_dir = tempdir("out").unwrap();
533        let path = tmp_dir.path().join("IVersioned.sol/IVersioned.0.8.16.json");
534        create_parent_dir_all(&path).unwrap();
535        assert!(path.parent().unwrap().is_dir());
536        let path = tmp_dir.path().join("IVersioned.sol/IVersioned.json");
537        create_parent_dir_all(&path).unwrap();
538        assert!(path.parent().unwrap().is_dir());
539    }
540
541    #[test]
542    fn can_determine_local_paths() {
543        assert!(is_local_source_name(&[""], "./local/contract.sol"));
544        assert!(is_local_source_name(&[""], "../local/contract.sol"));
545        assert!(!is_local_source_name(&[""], "/ds-test/test.sol"));
546
547        let tmp_dir = tempdir("contracts").unwrap();
548        let dir = tmp_dir.path().join("ds-test");
549        create_dir_all(&dir).unwrap();
550        File::create(dir.join("test.sol")).unwrap();
551
552        assert!(!is_local_source_name(&[tmp_dir.path()], "ds-test/test.sol"));
553    }
554
555    #[test]
556    fn can_normalize_solidity_import_path() {
557        let dir = tempfile::tempdir().unwrap();
558        let dir_path = dir.path();
559
560        // File structure:
561        //
562        // `dir_path`
563        // └── src (`cwd`)
564        //     ├── Token.sol
565        //     └── common
566        //         └── Burnable.sol
567
568        fs::create_dir_all(dir_path.join("src/common")).unwrap();
569        fs::write(dir_path.join("src/Token.sol"), "").unwrap();
570        fs::write(dir_path.join("src/common/Burnable.sol"), "").unwrap();
571
572        // assume that the import path is specified in Token.sol
573        let cwd = dir_path.join("src");
574
575        assert_eq!(
576            normalize_solidity_import_path(&cwd, "./common/Burnable.sol".as_ref()).unwrap(),
577            dir_path.join("src/common/Burnable.sol"),
578        );
579
580        assert!(normalize_solidity_import_path(&cwd, "./common/Pausable.sol".as_ref()).is_err());
581    }
582
583    // This test is exclusive to unix because creating a symlink is a privileged action on Windows.
584    // https://doc.rust-lang.org/std/os/windows/fs/fn.symlink_dir.html#limitations
585    #[test]
586    #[cfg(unix)]
587    fn can_normalize_solidity_import_path_symlink() {
588        let dir = tempfile::tempdir().unwrap();
589        let dir_path = dir.path();
590
591        // File structure:
592        //
593        // `dir_path`
594        // ├── dependency
595        // │   └── Math.sol
596        // └── project
597        //     ├── node_modules
598        //     │   └── dependency -> symlink to actual 'dependency' directory
599        //     └── src (`cwd`)
600        //         └── Token.sol
601
602        fs::create_dir_all(dir_path.join("project/src")).unwrap();
603        fs::write(dir_path.join("project/src/Token.sol"), "").unwrap();
604        fs::create_dir(dir_path.join("project/node_modules")).unwrap();
605
606        fs::create_dir(dir_path.join("dependency")).unwrap();
607        fs::write(dir_path.join("dependency/Math.sol"), "").unwrap();
608
609        std::os::unix::fs::symlink(
610            dir_path.join("dependency"),
611            dir_path.join("project/node_modules/dependency"),
612        )
613        .unwrap();
614
615        // assume that the import path is specified in Token.sol
616        let cwd = dir_path.join("project/src");
617
618        assert_eq!(
619            normalize_solidity_import_path(&cwd, "../node_modules/dependency/Math.sol".as_ref())
620                .unwrap(),
621            dir_path.join("project/node_modules/dependency/Math.sol"),
622        );
623    }
624
625    #[test]
626    fn can_clean_solidity_path() {
627        let clean_solidity_path = |s: &str| clean_solidity_path(s.as_ref());
628        assert_eq!(clean_solidity_path("a"), PathBuf::from("a"));
629        assert_eq!(clean_solidity_path("./a"), PathBuf::from("a"));
630        assert_eq!(clean_solidity_path("../a"), PathBuf::from("../a"));
631        assert_eq!(clean_solidity_path("/a/"), PathBuf::from("/a"));
632        assert_eq!(clean_solidity_path("//a"), PathBuf::from("/a"));
633        assert_eq!(clean_solidity_path("a/b"), PathBuf::from("a/b"));
634        assert_eq!(clean_solidity_path("a//b"), PathBuf::from("a/b"));
635        assert_eq!(clean_solidity_path("/a/b"), PathBuf::from("/a/b"));
636        assert_eq!(clean_solidity_path("a/./b"), PathBuf::from("a/b"));
637        assert_eq!(clean_solidity_path("a/././b"), PathBuf::from("a/b"));
638        assert_eq!(clean_solidity_path("/a/../b"), PathBuf::from("/b"));
639        assert_eq!(clean_solidity_path("a/./../b/."), PathBuf::from("b"));
640        assert_eq!(clean_solidity_path("a/b/c"), PathBuf::from("a/b/c"));
641        assert_eq!(clean_solidity_path("a/b/../c"), PathBuf::from("a/c"));
642        assert_eq!(clean_solidity_path("a/b/../../c"), PathBuf::from("c"));
643        assert_eq!(clean_solidity_path("a/b/../../../c"), PathBuf::from("../c"));
644        assert_eq!(
645            clean_solidity_path("a/../b/../../c/./Token.sol"),
646            PathBuf::from("../c/Token.sol")
647        );
648    }
649
650    #[test]
651    fn can_find_ancestor() {
652        let a = Path::new("/foo/bar/bar/test.txt");
653        let b = Path::new("/foo/bar/foo/example/constract.sol");
654        let expected = Path::new("/foo/bar");
655        assert_eq!(common_ancestor(a, b).unwrap(), expected.to_path_buf())
656    }
657
658    #[test]
659    fn no_common_ancestor_path() {
660        let a = Path::new("/foo/bar");
661        let b = Path::new("./bar/foo");
662        assert!(common_ancestor(a, b).is_none());
663    }
664
665    #[test]
666    fn can_find_all_ancestor() {
667        let a = Path::new("/foo/bar/foo/example.txt");
668        let b = Path::new("/foo/bar/foo/test.txt");
669        let c = Path::new("/foo/bar/bar/foo/bar");
670        let expected = Path::new("/foo/bar");
671        let paths = vec![a, b, c];
672        assert_eq!(common_ancestor_all(paths).unwrap(), expected.to_path_buf())
673    }
674}