1use core::fmt;
4use foundry_compilers::FileFilter;
5use serde::{Deserialize, Serialize};
6use std::{
7 convert::Infallible,
8 path::{Path, PathBuf},
9 str::FromStr,
10};
11
12#[derive(Clone, Debug)]
15pub struct GlobMatcher {
16 pub matcher: globset::GlobMatcher,
18}
19
20impl GlobMatcher {
21 pub fn new(glob: globset::Glob) -> Self {
23 Self { matcher: glob.compile_matcher() }
24 }
25
26 pub fn is_match(&self, path: &Path) -> bool {
31 if self.matcher.is_match(path) {
32 return true;
33 }
34
35 if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
36 && file_name.contains(self.as_str())
37 {
38 return true;
39 }
40
41 if !path.starts_with("./") && self.as_str().starts_with("./") {
42 return self.matcher.is_match(format!("./{}", path.display()));
43 }
44
45 if path.is_relative() && Path::new(self.glob().glob()).is_absolute() {
46 if let Ok(canonicalized_path) = dunce::canonicalize(path) {
47 return self.matcher.is_match(&canonicalized_path);
48 }
49 return false;
50 }
51
52 false
53 }
54
55 fn is_match_exclude(&self, path: &Path) -> bool {
59 !self.is_match(path)
60 }
61
62 pub fn glob(&self) -> &globset::Glob {
64 self.matcher.glob()
65 }
66
67 pub fn as_str(&self) -> &str {
69 self.glob().glob()
70 }
71}
72
73impl fmt::Display for GlobMatcher {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 self.glob().fmt(f)
76 }
77}
78
79impl FromStr for GlobMatcher {
80 type Err = globset::Error;
81
82 fn from_str(s: &str) -> Result<Self, Self::Err> {
83 s.parse::<globset::Glob>().map(Self::new)
84 }
85}
86
87impl From<globset::Glob> for GlobMatcher {
88 fn from(glob: globset::Glob) -> Self {
89 Self::new(glob)
90 }
91}
92
93impl Serialize for GlobMatcher {
94 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95 self.glob().glob().serialize(serializer)
96 }
97}
98
99impl<'de> Deserialize<'de> for GlobMatcher {
100 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 let s = String::deserialize(deserializer)?;
102 s.parse().map_err(serde::de::Error::custom)
103 }
104}
105
106impl PartialEq for GlobMatcher {
107 fn eq(&self, other: &Self) -> bool {
108 self.as_str() == other.as_str()
109 }
110}
111
112impl Eq for GlobMatcher {}
113
114#[derive(Clone, Debug)]
116pub struct SkipBuildFilters {
117 pub matchers: Vec<GlobMatcher>,
119 pub project_root: PathBuf,
121}
122
123impl FileFilter for SkipBuildFilters {
124 fn is_match(&self, file: &Path) -> bool {
126 self.matchers.iter().all(|matcher| {
127 if matcher.is_match_exclude(file) {
128 file.strip_prefix(&self.project_root)
129 .map_or(true, |stripped| matcher.is_match_exclude(stripped))
130 } else {
131 false
132 }
133 })
134 }
135}
136
137impl SkipBuildFilters {
138 pub fn new<G: Into<GlobMatcher>>(
140 filters: impl IntoIterator<Item = G>,
141 project_root: PathBuf,
142 ) -> Self {
143 let matchers = filters.into_iter().map(|m| m.into()).collect();
144 Self { matchers, project_root }
145 }
146}
147
148#[derive(Clone, Debug, PartialEq, Eq)]
150pub enum SkipBuildFilter {
151 Tests,
153 Scripts,
155 Custom(String),
157}
158
159impl SkipBuildFilter {
160 fn new(s: &str) -> Self {
161 match s {
162 "test" | "tests" => Self::Tests,
163 "script" | "scripts" => Self::Scripts,
164 s => Self::Custom(s.to_string()),
165 }
166 }
167
168 pub const fn file_pattern(&self) -> &str {
170 match self {
171 Self::Tests => ".t.sol",
172 Self::Scripts => ".s.sol",
173 Self::Custom(s) => s.as_str(),
174 }
175 }
176}
177
178impl FromStr for SkipBuildFilter {
179 type Err = Infallible;
180
181 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 Ok(Self::new(s))
183 }
184}
185
186pub fn expand_globs(
191 root: &Path,
192 patterns: impl IntoIterator<Item = impl AsRef<str>>,
193) -> eyre::Result<Vec<PathBuf>> {
194 let mut expanded = Vec::new();
195 for pattern in patterns {
196 let mut pattern = Path::new(pattern.as_ref());
197 if pattern.ends_with("**") {
198 pattern = pattern.parent().unwrap_or_else(|| Path::new(""));
199 }
200 for path in glob::glob(&root.join(pattern).display().to_string())? {
201 expanded.push(path?);
202 }
203 }
204 Ok(expanded)
205}
206
207pub fn is_ignored_path(file: &Path, paths: &[PathBuf], base: &Path) -> bool {
214 let joined = base.join(file);
215 paths.iter().any(|path| file.starts_with(path) || joined.starts_with(path))
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use std::fs;
222 use tempfile::tempdir;
223
224 #[test]
225 fn test_expand_trailing_recursive_glob() {
226 let root = tempdir().unwrap();
227 let src = root.path().join("src");
228 fs::create_dir(&src).unwrap();
229
230 assert_eq!(expand_globs(root.path(), ["src/**"]).unwrap(), vec![src]);
231 }
232
233 #[test]
234 fn test_is_ignored_path() {
235 let base = Path::new("/root");
236 let ignored = [PathBuf::from("/root/test"), PathBuf::from("/root/src/A.sol")];
237
238 assert!(is_ignored_path(Path::new("/root/src/A.sol"), &ignored, base));
240 assert!(is_ignored_path(Path::new("src/A.sol"), &ignored, base));
241
242 assert!(is_ignored_path(Path::new("/root/test/B.t.sol"), &ignored, base));
244 assert!(is_ignored_path(Path::new("test/sub/C.t.sol"), &ignored, base));
245
246 assert!(!is_ignored_path(Path::new("/root/testOther.sol"), &ignored, base));
248 assert!(!is_ignored_path(Path::new("/root/src/B.sol"), &ignored, base));
249 }
250
251 #[test]
252 fn test_build_filter() {
253 let tests = GlobMatcher::from_str(SkipBuildFilter::Tests.file_pattern()).unwrap();
254 let scripts = GlobMatcher::from_str(SkipBuildFilter::Scripts.file_pattern()).unwrap();
255 let custom = |s| GlobMatcher::from_str(s).unwrap();
256
257 let file = Path::new("A.t.sol");
258 assert!(!tests.is_match_exclude(file));
259 assert!(scripts.is_match_exclude(file));
260 assert!(!custom("A.t").is_match_exclude(file));
261
262 let file = Path::new("A.s.sol");
263 assert!(tests.is_match_exclude(file));
264 assert!(!scripts.is_match_exclude(file));
265 assert!(!custom("A.s").is_match_exclude(file));
266
267 let file = Path::new("/home/test/Foo.sol");
268 assert!(!custom("*/test/**").is_match_exclude(file));
269
270 let file = Path::new("/home/script/Contract.sol");
271 assert!(!custom("*/script/**").is_match_exclude(file));
272 }
273
274 #[test]
275 fn can_match_relative_glob_paths() {
276 let matcher: GlobMatcher = "./test/*".parse().unwrap();
277
278 assert!(matcher.is_match(Path::new("test/Contract.t.sol")));
280
281 assert!(matcher.is_match(Path::new("./test/Contract.t.sol")));
283 }
284
285 #[test]
286 fn can_match_absolute_glob_paths() {
287 let matcher: GlobMatcher = "/home/user/projects/project/test/*".parse().unwrap();
288
289 assert!(matcher.is_match(Path::new("/home/user/projects/project/test/Contract.t.sol")));
291
292 assert!(!matcher.is_match(Path::new("/home/user/other/project/test/Contract.t.sol")));
294
295 assert!(!matcher.is_match(Path::new("projects/project/test/Contract.t.sol")));
297 }
298}