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
12pub fn expand_globs(
14 root: &Path,
15 patterns: impl IntoIterator<Item = impl AsRef<str>>,
16) -> eyre::Result<Vec<PathBuf>> {
17 let mut expanded = Vec::new();
18 for pattern in patterns {
19 for paths in glob::glob(&root.join(pattern.as_ref()).display().to_string())? {
20 expanded.push(paths?);
21 }
22 }
23 Ok(expanded)
24}
25
26#[derive(Clone, Debug)]
29pub struct GlobMatcher {
30 pub matcher: globset::GlobMatcher,
32}
33
34impl GlobMatcher {
35 pub fn new(glob: globset::Glob) -> Self {
37 Self { matcher: glob.compile_matcher() }
38 }
39
40 pub fn is_match(&self, path: &Path) -> bool {
45 if self.matcher.is_match(path) {
46 return true;
47 }
48
49 if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
50 && file_name.contains(self.as_str())
51 {
52 return true;
53 }
54
55 if !path.starts_with("./") && self.as_str().starts_with("./") {
56 return self.matcher.is_match(format!("./{}", path.display()));
57 }
58
59 if path.is_relative() && Path::new(self.glob().glob()).is_absolute() {
60 if let Ok(canonicalized_path) = dunce::canonicalize(path) {
61 return self.matcher.is_match(&canonicalized_path);
62 }
63 return false;
64 }
65
66 false
67 }
68
69 fn is_match_exclude(&self, path: &Path) -> bool {
73 !self.is_match(path)
74 }
75
76 pub fn glob(&self) -> &globset::Glob {
78 self.matcher.glob()
79 }
80
81 pub fn as_str(&self) -> &str {
83 self.glob().glob()
84 }
85}
86
87impl fmt::Display for GlobMatcher {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 self.glob().fmt(f)
90 }
91}
92
93impl FromStr for GlobMatcher {
94 type Err = globset::Error;
95
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 s.parse::<globset::Glob>().map(Self::new)
98 }
99}
100
101impl From<globset::Glob> for GlobMatcher {
102 fn from(glob: globset::Glob) -> Self {
103 Self::new(glob)
104 }
105}
106
107impl Serialize for GlobMatcher {
108 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
109 self.glob().glob().serialize(serializer)
110 }
111}
112
113impl<'de> Deserialize<'de> for GlobMatcher {
114 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
115 let s = String::deserialize(deserializer)?;
116 s.parse().map_err(serde::de::Error::custom)
117 }
118}
119
120impl PartialEq for GlobMatcher {
121 fn eq(&self, other: &Self) -> bool {
122 self.as_str() == other.as_str()
123 }
124}
125
126impl Eq for GlobMatcher {}
127
128#[derive(Clone, Debug)]
130pub struct SkipBuildFilters {
131 pub matchers: Vec<GlobMatcher>,
133 pub project_root: PathBuf,
135}
136
137impl FileFilter for SkipBuildFilters {
138 fn is_match(&self, file: &Path) -> bool {
140 self.matchers.iter().all(|matcher| {
141 if matcher.is_match_exclude(file) {
142 file.strip_prefix(&self.project_root)
143 .map_or(true, |stripped| matcher.is_match_exclude(stripped))
144 } else {
145 false
146 }
147 })
148 }
149}
150
151impl SkipBuildFilters {
152 pub fn new<G: Into<GlobMatcher>>(
154 filters: impl IntoIterator<Item = G>,
155 project_root: PathBuf,
156 ) -> Self {
157 let matchers = filters.into_iter().map(|m| m.into()).collect();
158 Self { matchers, project_root }
159 }
160}
161
162#[derive(Clone, Debug, PartialEq, Eq)]
164pub enum SkipBuildFilter {
165 Tests,
167 Scripts,
169 Custom(String),
171}
172
173impl SkipBuildFilter {
174 fn new(s: &str) -> Self {
175 match s {
176 "test" | "tests" => Self::Tests,
177 "script" | "scripts" => Self::Scripts,
178 s => Self::Custom(s.to_string()),
179 }
180 }
181
182 pub fn file_pattern(&self) -> &str {
184 match self {
185 Self::Tests => ".t.sol",
186 Self::Scripts => ".s.sol",
187 Self::Custom(s) => s.as_str(),
188 }
189 }
190}
191
192impl FromStr for SkipBuildFilter {
193 type Err = Infallible;
194
195 fn from_str(s: &str) -> Result<Self, Self::Err> {
196 Ok(Self::new(s))
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn test_build_filter() {
206 let tests = GlobMatcher::from_str(SkipBuildFilter::Tests.file_pattern()).unwrap();
207 let scripts = GlobMatcher::from_str(SkipBuildFilter::Scripts.file_pattern()).unwrap();
208 let custom = |s| GlobMatcher::from_str(s).unwrap();
209
210 let file = Path::new("A.t.sol");
211 assert!(!tests.is_match_exclude(file));
212 assert!(scripts.is_match_exclude(file));
213 assert!(!custom("A.t").is_match_exclude(file));
214
215 let file = Path::new("A.s.sol");
216 assert!(tests.is_match_exclude(file));
217 assert!(!scripts.is_match_exclude(file));
218 assert!(!custom("A.s").is_match_exclude(file));
219
220 let file = Path::new("/home/test/Foo.sol");
221 assert!(!custom("*/test/**").is_match_exclude(file));
222
223 let file = Path::new("/home/script/Contract.sol");
224 assert!(!custom("*/script/**").is_match_exclude(file));
225 }
226
227 #[test]
228 fn can_match_relative_glob_paths() {
229 let matcher: GlobMatcher = "./test/*".parse().unwrap();
230
231 assert!(matcher.is_match(Path::new("test/Contract.t.sol")));
233
234 assert!(matcher.is_match(Path::new("./test/Contract.t.sol")));
236 }
237
238 #[test]
239 fn can_match_absolute_glob_paths() {
240 let matcher: GlobMatcher = "/home/user/projects/project/test/*".parse().unwrap();
241
242 assert!(matcher.is_match(Path::new("/home/user/projects/project/test/Contract.t.sol")));
244
245 assert!(!matcher.is_match(Path::new("/home/user/other/project/test/Contract.t.sol")));
247
248 assert!(!matcher.is_match(Path::new("projects/project/test/Contract.t.sol")));
250 }
251}