Skip to main content

foundry_compilers_core/utils/
mod.rs

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