foundry_config/
filter.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
//! Helpers for constructing and using [FileFilter]s.

use core::fmt;
use foundry_compilers::FileFilter;
use serde::{Deserialize, Serialize};
use std::{
    convert::Infallible,
    path::{Path, PathBuf},
    str::FromStr,
};

/// Expand globs with a root path.
pub fn expand_globs(
    root: &Path,
    patterns: impl IntoIterator<Item = impl AsRef<str>>,
) -> eyre::Result<Vec<PathBuf>> {
    let mut expanded = Vec::new();
    for pattern in patterns {
        for paths in glob::glob(&root.join(pattern.as_ref()).display().to_string())? {
            expanded.push(paths?);
        }
    }
    Ok(expanded)
}

/// A `globset::Glob` that creates its `globset::GlobMatcher` when its created, so it doesn't need
/// to be compiled when the filter functions `TestFilter` functions are called.
#[derive(Clone, Debug)]
pub struct GlobMatcher {
    /// The compiled glob
    pub matcher: globset::GlobMatcher,
}

impl GlobMatcher {
    /// Creates a new `GlobMatcher` from a `globset::Glob`.
    pub fn new(glob: globset::Glob) -> Self {
        Self { matcher: glob.compile_matcher() }
    }

    /// Tests whether the given path matches this pattern or not.
    ///
    /// The glob `./test/*` won't match absolute paths like `test/Contract.sol`, which is common
    /// format here, so we also handle this case here
    pub fn is_match(&self, path: &Path) -> bool {
        if self.matcher.is_match(path) {
            return true;
        }

        if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
            if file_name.contains(self.as_str()) {
                return true;
            }
        }

        if !path.starts_with("./") && self.as_str().starts_with("./") {
            return self.matcher.is_match(format!("./{}", path.display()));
        }

        if path.is_relative() && Path::new(self.glob().glob()).is_absolute() {
            if let Ok(canonicalized_path) = dunce::canonicalize(path) {
                return self.matcher.is_match(&canonicalized_path);
            } else {
                return false;
            }
        }

        false
    }

    /// Matches file only if the filter does not apply.
    ///
    /// This returns the inverse of `self.is_match(file)`.
    fn is_match_exclude(&self, path: &Path) -> bool {
        !self.is_match(path)
    }

    /// Returns the `globset::Glob`.
    pub fn glob(&self) -> &globset::Glob {
        self.matcher.glob()
    }

    /// Returns the `Glob` string used to compile this matcher.
    pub fn as_str(&self) -> &str {
        self.glob().glob()
    }
}

impl fmt::Display for GlobMatcher {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.glob().fmt(f)
    }
}

impl FromStr for GlobMatcher {
    type Err = globset::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        s.parse::<globset::Glob>().map(Self::new)
    }
}

impl From<globset::Glob> for GlobMatcher {
    fn from(glob: globset::Glob) -> Self {
        Self::new(glob)
    }
}

impl Serialize for GlobMatcher {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        self.glob().glob().serialize(serializer)
    }
}

impl<'de> Deserialize<'de> for GlobMatcher {
    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        s.parse().map_err(serde::de::Error::custom)
    }
}

impl PartialEq for GlobMatcher {
    fn eq(&self, other: &Self) -> bool {
        self.as_str() == other.as_str()
    }
}

impl Eq for GlobMatcher {}

/// Bundles multiple `SkipBuildFilter` into a single `FileFilter`
#[derive(Clone, Debug)]
pub struct SkipBuildFilters {
    /// All provided filters.
    pub matchers: Vec<GlobMatcher>,
    /// Root of the project.
    pub project_root: PathBuf,
}

impl FileFilter for SkipBuildFilters {
    /// Only returns a match if _no_  exclusion filter matches
    fn is_match(&self, file: &Path) -> bool {
        self.matchers.iter().all(|matcher| {
            if !matcher.is_match_exclude(file) {
                false
            } else {
                file.strip_prefix(&self.project_root)
                    .map_or(true, |stripped| matcher.is_match_exclude(stripped))
            }
        })
    }
}

impl SkipBuildFilters {
    /// Creates a new `SkipBuildFilters` from multiple `SkipBuildFilter`.
    pub fn new<G: Into<GlobMatcher>>(
        filters: impl IntoIterator<Item = G>,
        project_root: PathBuf,
    ) -> Self {
        let matchers = filters.into_iter().map(|m| m.into()).collect();
        Self { matchers, project_root }
    }
}

/// A filter that excludes matching contracts from the build
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SkipBuildFilter {
    /// Exclude all `.t.sol` contracts
    Tests,
    /// Exclude all `.s.sol` contracts
    Scripts,
    /// Exclude if the file matches
    Custom(String),
}

impl SkipBuildFilter {
    fn new(s: &str) -> Self {
        match s {
            "test" | "tests" => Self::Tests,
            "script" | "scripts" => Self::Scripts,
            s => Self::Custom(s.to_string()),
        }
    }

    /// Returns the pattern to match against a file
    pub fn file_pattern(&self) -> &str {
        match self {
            Self::Tests => ".t.sol",
            Self::Scripts => ".s.sol",
            Self::Custom(s) => s.as_str(),
        }
    }
}

impl FromStr for SkipBuildFilter {
    type Err = Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Self::new(s))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_filter() {
        let tests = GlobMatcher::from_str(SkipBuildFilter::Tests.file_pattern()).unwrap();
        let scripts = GlobMatcher::from_str(SkipBuildFilter::Scripts.file_pattern()).unwrap();
        let custom = |s| GlobMatcher::from_str(s).unwrap();

        let file = Path::new("A.t.sol");
        assert!(!tests.is_match_exclude(file));
        assert!(scripts.is_match_exclude(file));
        assert!(!custom("A.t").is_match_exclude(file));

        let file = Path::new("A.s.sol");
        assert!(tests.is_match_exclude(file));
        assert!(!scripts.is_match_exclude(file));
        assert!(!custom("A.s").is_match_exclude(file));

        let file = Path::new("/home/test/Foo.sol");
        assert!(!custom("*/test/**").is_match_exclude(file));

        let file = Path::new("/home/script/Contract.sol");
        assert!(!custom("*/script/**").is_match_exclude(file));
    }

    #[test]
    fn can_match_relative_glob_paths() {
        let matcher: GlobMatcher = "./test/*".parse().unwrap();

        // Absolute path that should match the pattern
        assert!(matcher.is_match(Path::new("test/Contract.t.sol")));

        // Relative path that should match the pattern
        assert!(matcher.is_match(Path::new("./test/Contract.t.sol")));
    }

    #[test]
    fn can_match_absolute_glob_paths() {
        let matcher: GlobMatcher = "/home/user/projects/project/test/*".parse().unwrap();

        // Absolute path that should match the pattern
        assert!(matcher.is_match(Path::new("/home/user/projects/project/test/Contract.t.sol")));

        // Absolute path that should not match the pattern
        assert!(!matcher.is_match(Path::new("/home/user/other/project/test/Contract.t.sol")));

        // Relative path that should not match an absolute pattern
        assert!(!matcher.is_match(Path::new("projects/project/test/Contract.t.sol")));
    }
}