foundry_compilers_core/utils/
wd.rs

1use super::SOLC_EXTENSIONS;
2use crate::error::SolcError;
3use semver::Version;
4use std::{
5    collections::HashSet,
6    path::{Path, PathBuf},
7};
8use walkdir::WalkDir;
9
10/// Returns an iterator that yields all solidity/yul files funder under the given root path or the
11/// `root` itself, if it is a sol/yul file
12///
13/// This also follows symlinks.
14pub fn source_files_iter<'a>(
15    root: &Path,
16    extensions: &'a [&'a str],
17) -> impl Iterator<Item = PathBuf> + 'a {
18    WalkDir::new(root)
19        .follow_links(true)
20        .into_iter()
21        .filter_map(Result::ok)
22        .filter(|e| e.file_type().is_file())
23        .filter(|e| {
24            e.path().extension().map(|ext| extensions.iter().any(|e| ext == *e)).unwrap_or_default()
25        })
26        .map(|e| e.path().into())
27}
28
29/// Returns a list of absolute paths to all the solidity files under the root, or the file itself,
30/// if the path is a solidity file.
31///
32/// This also follows symlinks.
33///
34/// NOTE: this does not resolve imports from other locations
35///
36/// # Examples
37///
38/// ```no_run
39/// use foundry_compilers_core::utils;
40/// let sources = utils::source_files("./contracts".as_ref(), &utils::SOLC_EXTENSIONS);
41/// ```
42pub fn source_files(root: &Path, extensions: &[&str]) -> Vec<PathBuf> {
43    source_files_iter(root, extensions).collect()
44}
45
46/// Same as [source_files] but only returns files acceptable by Solc compiler.
47pub fn sol_source_files(root: &Path) -> Vec<PathBuf> {
48    source_files(root, SOLC_EXTENSIONS)
49}
50
51/// Returns a list of _unique_ paths to all folders under `root` that contain at least one solidity
52/// file (`*.sol`).
53///
54/// # Examples
55///
56/// ```no_run
57/// use foundry_compilers_core::utils;
58/// let dirs = utils::solidity_dirs("./lib".as_ref());
59/// ```
60///
61/// for following layout will return
62/// `["lib/ds-token/src", "lib/ds-token/src/test", "lib/ds-token/lib/ds-math/src", ...]`
63///
64/// ```text
65/// lib
66/// └── ds-token
67///     ├── lib
68///     │   ├── ds-math
69///     │   │   └── src/Contract.sol
70///     │   ├── ds-stop
71///     │   │   └── src/Contract.sol
72///     │   ├── ds-test
73///     │       └── src//Contract.sol
74///     └── src
75///         ├── base.sol
76///         ├── test
77///         │   ├── base.t.sol
78///         └── token.sol
79/// ```
80pub fn solidity_dirs(root: &Path) -> Vec<PathBuf> {
81    let sources = sol_source_files(root);
82    sources
83        .iter()
84        .filter_map(|p| p.parent())
85        .collect::<HashSet<_>>()
86        .into_iter()
87        .map(|p| p.to_path_buf())
88        .collect()
89}
90
91/// Reads the list of Solc versions that have been installed in the machine.
92///
93/// The version list is sorted in ascending order.
94///
95/// Checks for installed solc versions under the given path as `<root>/<major.minor.path>`,
96/// (e.g.: `~/.svm/0.8.10`) and returns them sorted in ascending order.
97pub fn installed_versions(root: &Path) -> Result<Vec<Version>, SolcError> {
98    let mut versions: Vec<_> = walkdir::WalkDir::new(root)
99        .max_depth(1)
100        .into_iter()
101        .filter_map(std::result::Result::ok)
102        .filter(|e| e.file_type().is_dir())
103        .filter_map(|e: walkdir::DirEntry| {
104            e.path().file_name().and_then(|v| Version::parse(v.to_string_lossy().as_ref()).ok())
105        })
106        .collect();
107    versions.sort();
108    Ok(versions)
109}
110
111/// Attempts to find a file with different case that exists next to the `non_existing` file
112pub fn find_case_sensitive_existing_file(non_existing: &Path) -> Option<PathBuf> {
113    let non_existing_file_name = non_existing.file_name()?;
114    let parent = non_existing.parent()?;
115    WalkDir::new(parent)
116        .max_depth(1)
117        .into_iter()
118        .filter_map(Result::ok)
119        .filter(|e| e.file_type().is_file())
120        .find_map(|e| {
121            let existing_file_name = e.path().file_name()?;
122            if existing_file_name.eq_ignore_ascii_case(non_existing_file_name)
123                && existing_file_name != non_existing_file_name
124            {
125                return Some(e.path().to_path_buf());
126            }
127            None
128        })
129}
130
131#[cfg(test)]
132mod tests {
133    use super::{super::tests::*, *};
134
135    #[test]
136    fn can_find_solidity_sources() {
137        let tmp_dir = tempdir("contracts").unwrap();
138
139        let file_a = tmp_dir.path().join("a.sol");
140        let file_b = tmp_dir.path().join("a.sol");
141        let nested = tmp_dir.path().join("nested");
142        let file_c = nested.join("c.sol");
143        let nested_deep = nested.join("deep");
144        let file_d = nested_deep.join("d.sol");
145        File::create(&file_a).unwrap();
146        File::create(&file_b).unwrap();
147        create_dir_all(nested_deep).unwrap();
148        File::create(&file_c).unwrap();
149        File::create(&file_d).unwrap();
150
151        let files: HashSet<_> = sol_source_files(tmp_dir.path()).into_iter().collect();
152        let expected: HashSet<_> = [file_a, file_b, file_c, file_d].into();
153        assert_eq!(files, expected);
154    }
155
156    #[test]
157    fn can_find_different_case() {
158        let tmp_dir = tempdir("out").unwrap();
159        let path = tmp_dir.path().join("forge-std");
160        create_dir_all(&path).unwrap();
161        let existing = path.join("Test.sol");
162        let non_existing = path.join("test.sol");
163        fs::write(&existing, b"").unwrap();
164
165        #[cfg(target_os = "linux")]
166        assert!(!non_existing.exists());
167
168        let found = find_case_sensitive_existing_file(&non_existing).unwrap();
169        assert_eq!(found, existing);
170    }
171}