foundry_config/
filter.rs
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 if file_name.contains(self.as_str()) {
51 return true;
52 }
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 } else {
63 return false;
64 }
65 }
66
67 false
68 }
69
70 fn is_match_exclude(&self, path: &Path) -> bool {
74 !self.is_match(path)
75 }
76
77 pub fn glob(&self) -> &globset::Glob {
79 self.matcher.glob()
80 }
81
82 pub fn as_str(&self) -> &str {
84 self.glob().glob()
85 }
86}
87
88impl fmt::Display for GlobMatcher {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 self.glob().fmt(f)
91 }
92}
93
94impl FromStr for GlobMatcher {
95 type Err = globset::Error;
96
97 fn from_str(s: &str) -> Result<Self, Self::Err> {
98 s.parse::<globset::Glob>().map(Self::new)
99 }
100}
101
102impl From<globset::Glob> for GlobMatcher {
103 fn from(glob: globset::Glob) -> Self {
104 Self::new(glob)
105 }
106}
107
108impl Serialize for GlobMatcher {
109 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
110 self.glob().glob().serialize(serializer)
111 }
112}
113
114impl<'de> Deserialize<'de> for GlobMatcher {
115 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
116 let s = String::deserialize(deserializer)?;
117 s.parse().map_err(serde::de::Error::custom)
118 }
119}
120
121impl PartialEq for GlobMatcher {
122 fn eq(&self, other: &Self) -> bool {
123 self.as_str() == other.as_str()
124 }
125}
126
127impl Eq for GlobMatcher {}
128
129#[derive(Clone, Debug)]
131pub struct SkipBuildFilters {
132 pub matchers: Vec<GlobMatcher>,
134 pub project_root: PathBuf,
136}
137
138impl FileFilter for SkipBuildFilters {
139 fn is_match(&self, file: &Path) -> bool {
141 self.matchers.iter().all(|matcher| {
142 if !matcher.is_match_exclude(file) {
143 false
144 } else {
145 file.strip_prefix(&self.project_root)
146 .map_or(true, |stripped| matcher.is_match_exclude(stripped))
147 }
148 })
149 }
150}
151
152impl SkipBuildFilters {
153 pub fn new<G: Into<GlobMatcher>>(
155 filters: impl IntoIterator<Item = G>,
156 project_root: PathBuf,
157 ) -> Self {
158 let matchers = filters.into_iter().map(|m| m.into()).collect();
159 Self { matchers, project_root }
160 }
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
165pub enum SkipBuildFilter {
166 Tests,
168 Scripts,
170 Custom(String),
172}
173
174impl SkipBuildFilter {
175 fn new(s: &str) -> Self {
176 match s {
177 "test" | "tests" => Self::Tests,
178 "script" | "scripts" => Self::Scripts,
179 s => Self::Custom(s.to_string()),
180 }
181 }
182
183 pub fn file_pattern(&self) -> &str {
185 match self {
186 Self::Tests => ".t.sol",
187 Self::Scripts => ".s.sol",
188 Self::Custom(s) => s.as_str(),
189 }
190 }
191}
192
193impl FromStr for SkipBuildFilter {
194 type Err = Infallible;
195
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
197 Ok(Self::new(s))
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_build_filter() {
207 let tests = GlobMatcher::from_str(SkipBuildFilter::Tests.file_pattern()).unwrap();
208 let scripts = GlobMatcher::from_str(SkipBuildFilter::Scripts.file_pattern()).unwrap();
209 let custom = |s| GlobMatcher::from_str(s).unwrap();
210
211 let file = Path::new("A.t.sol");
212 assert!(!tests.is_match_exclude(file));
213 assert!(scripts.is_match_exclude(file));
214 assert!(!custom("A.t").is_match_exclude(file));
215
216 let file = Path::new("A.s.sol");
217 assert!(tests.is_match_exclude(file));
218 assert!(!scripts.is_match_exclude(file));
219 assert!(!custom("A.s").is_match_exclude(file));
220
221 let file = Path::new("/home/test/Foo.sol");
222 assert!(!custom("*/test/**").is_match_exclude(file));
223
224 let file = Path::new("/home/script/Contract.sol");
225 assert!(!custom("*/script/**").is_match_exclude(file));
226 }
227
228 #[test]
229 fn can_match_relative_glob_paths() {
230 let matcher: GlobMatcher = "./test/*".parse().unwrap();
231
232 assert!(matcher.is_match(Path::new("test/Contract.t.sol")));
234
235 assert!(matcher.is_match(Path::new("./test/Contract.t.sol")));
237 }
238
239 #[test]
240 fn can_match_absolute_glob_paths() {
241 let matcher: GlobMatcher = "/home/user/projects/project/test/*".parse().unwrap();
242
243 assert!(matcher.is_match(Path::new("/home/user/projects/project/test/Contract.t.sol")));
245
246 assert!(!matcher.is_match(Path::new("/home/user/other/project/test/Contract.t.sol")));
248
249 assert!(!matcher.is_match(Path::new("projects/project/test/Contract.t.sol")));
251 }
252}