1use std::path::Path;
7
8use crate::glob::glob_match;
9use crate::glob_path::GlobPath;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum FilterResult {
14 Include,
16 Exclude,
18 NoMatch,
20}
21
22#[derive(Debug, Clone, Default)]
41pub struct IncludeExclude {
42 rules: Vec<(FilterAction, CompiledRule)>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum FilterAction {
47 Include,
48 Exclude,
49}
50
51#[derive(Debug, Clone)]
52struct CompiledRule {
53 glob: Option<GlobPath>,
54 raw: String,
55}
56
57impl CompiledRule {
58 fn new(pattern: &str) -> Self {
59 let glob = GlobPath::new(pattern).ok();
60
61 Self {
62 glob,
63 raw: pattern.to_string(),
64 }
65 }
66
67 fn matches(&self, path: &Path) -> bool {
68 if let Some(ref glob) = self.glob {
69 return glob.matches(path);
70 }
71
72 let path_str = path.to_string_lossy();
74
75 if let Some(name) = path.file_name() {
77 let name_str = name.to_string_lossy();
78 if glob_match(&self.raw, &name_str) {
79 return true;
80 }
81 }
82
83 glob_match(&self.raw, &path_str)
84 }
85}
86
87impl IncludeExclude {
88 pub fn new() -> Self {
90 Self::default()
91 }
92
93 pub fn include(&mut self, pattern: &str) {
98 self.rules
99 .push((FilterAction::Include, CompiledRule::new(pattern)));
100 }
101
102 pub fn exclude(&mut self, pattern: &str) {
107 self.rules
108 .push((FilterAction::Exclude, CompiledRule::new(pattern)));
109 }
110
111 pub fn check(&self, path: &Path) -> FilterResult {
115 for (action, rule) in &self.rules {
116 if rule.matches(path) {
117 return match action {
118 FilterAction::Include => FilterResult::Include,
119 FilterAction::Exclude => FilterResult::Exclude,
120 };
121 }
122 }
123
124 FilterResult::NoMatch
125 }
126
127 pub fn excludes_entry(&self, relative: &Path, name: Option<&Path>, is_dir: bool) -> bool {
136 for path in [Some(relative), name].into_iter().flatten() {
137 match self.check(path) {
138 FilterResult::Exclude => return true,
139 FilterResult::Include => return false,
140 FilterResult::NoMatch => {}
141 }
142 }
143 !is_dir && self.has_includes()
144 }
145
146 pub fn has_includes(&self) -> bool {
148 self.rules
149 .iter()
150 .any(|(action, _)| *action == FilterAction::Include)
151 }
152
153 pub fn is_empty(&self) -> bool {
155 self.rules.is_empty()
156 }
157
158 pub fn len(&self) -> usize {
160 self.rules.len()
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn test_empty_filter() {
170 let filter = IncludeExclude::new();
171 assert_eq!(filter.check(Path::new("any.txt")), FilterResult::NoMatch);
172 assert!(!filter.excludes_entry(Path::new("any.txt"), None, false));
173 }
174
175 #[test]
176 fn test_include() {
177 let mut filter = IncludeExclude::new();
178 filter.include("*.rs");
179
180 assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
181 assert_eq!(filter.check(Path::new("main.txt")), FilterResult::NoMatch);
182 }
183
184 #[test]
185 fn test_exclude() {
186 let mut filter = IncludeExclude::new();
187 filter.exclude("*.log");
188
189 assert_eq!(filter.check(Path::new("app.log")), FilterResult::Exclude);
190 assert!(filter.excludes_entry(Path::new("app.log"), None, false));
191 assert!(!filter.excludes_entry(Path::new("app.txt"), None, false));
192 }
193
194 #[test]
195 fn test_order_matters() {
196 let mut filter = IncludeExclude::new();
197 filter.include("*.rs");
198 filter.exclude("*_test.rs");
199
200 assert_eq!(
201 filter.check(Path::new("parser_test.rs")),
202 FilterResult::Include
203 );
204
205 let mut filter = IncludeExclude::new();
206 filter.exclude("*_test.rs");
207 filter.include("*.rs");
208
209 assert_eq!(
210 filter.check(Path::new("parser_test.rs")),
211 FilterResult::Exclude
212 );
213 }
214
215 #[test]
216 fn test_globstar_patterns() {
217 let mut filter = IncludeExclude::new();
218 filter.include("**/*.rs");
219 filter.exclude("**/test/**");
220
221 assert_eq!(filter.check(Path::new("src/main.rs")), FilterResult::Include);
222 assert_eq!(
223 filter.check(Path::new("src/lib/utils.rs")),
224 FilterResult::Include
225 );
226 }
227
228 #[test]
229 fn test_path_patterns() {
230 let mut filter = IncludeExclude::new();
231 filter.exclude("logs/*");
232
233 assert!(filter.excludes_entry(Path::new("logs/app.log"), None, false));
234 assert!(!filter.excludes_entry(Path::new("other/app.log"), None, false));
235 }
236
237 #[test]
238 fn test_multiple_patterns() {
239 let mut filter = IncludeExclude::new();
240 filter.include("*.rs");
241 filter.include("*.go");
242 filter.include("*.py");
243 filter.exclude("*_test.*");
244
245 assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
246 assert_eq!(filter.check(Path::new("server.go")), FilterResult::Include);
247 assert_eq!(filter.check(Path::new("main_test.rs")), FilterResult::Include); }
249
250 #[test]
251 fn test_brace_expansion() {
252 let mut filter = IncludeExclude::new();
253 filter.include("*.{rs,go,py}");
254
255 assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
256 assert_eq!(filter.check(Path::new("main.go")), FilterResult::Include);
257 assert_eq!(filter.check(Path::new("main.py")), FilterResult::Include);
258 assert_eq!(filter.check(Path::new("main.js")), FilterResult::NoMatch);
259 }
260
261 #[test]
262 fn include_list_excludes_nonmatching_files_but_not_dirs() {
263 let mut filter = IncludeExclude::new();
264 filter.include("*.rs");
265
266 assert!(filter.excludes_entry(Path::new("notes.txt"), None, false));
268 assert!(!filter.excludes_entry(Path::new("main.rs"), None, false));
270 assert!(!filter.excludes_entry(
271 Path::new("src/lib.rs"),
272 Some(Path::new("lib.rs")),
273 false
274 ));
275 assert!(!filter.excludes_entry(Path::new("src"), None, true));
277 }
278
279 #[test]
280 fn exclude_rules_still_prune_directories() {
281 let mut filter = IncludeExclude::new();
282 filter.include("*.rs");
283 filter.exclude("target");
284
285 assert!(filter.excludes_entry(Path::new("target"), None, true));
286 }
287}