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 should_exclude(&self, path: &Path) -> bool {
132 matches!(self.check(path), FilterResult::Exclude)
133 }
134
135 pub fn is_empty(&self) -> bool {
137 self.rules.is_empty()
138 }
139
140 pub fn len(&self) -> usize {
142 self.rules.len()
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_empty_filter() {
152 let filter = IncludeExclude::new();
153 assert_eq!(filter.check(Path::new("any.txt")), FilterResult::NoMatch);
154 assert!(!filter.should_exclude(Path::new("any.txt")));
155 }
156
157 #[test]
158 fn test_include() {
159 let mut filter = IncludeExclude::new();
160 filter.include("*.rs");
161
162 assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
163 assert_eq!(filter.check(Path::new("main.txt")), FilterResult::NoMatch);
164 }
165
166 #[test]
167 fn test_exclude() {
168 let mut filter = IncludeExclude::new();
169 filter.exclude("*.log");
170
171 assert_eq!(filter.check(Path::new("app.log")), FilterResult::Exclude);
172 assert!(filter.should_exclude(Path::new("app.log")));
173 assert!(!filter.should_exclude(Path::new("app.txt")));
174 }
175
176 #[test]
177 fn test_order_matters() {
178 let mut filter = IncludeExclude::new();
179 filter.include("*.rs");
180 filter.exclude("*_test.rs");
181
182 assert_eq!(
183 filter.check(Path::new("parser_test.rs")),
184 FilterResult::Include
185 );
186
187 let mut filter = IncludeExclude::new();
188 filter.exclude("*_test.rs");
189 filter.include("*.rs");
190
191 assert_eq!(
192 filter.check(Path::new("parser_test.rs")),
193 FilterResult::Exclude
194 );
195 }
196
197 #[test]
198 fn test_globstar_patterns() {
199 let mut filter = IncludeExclude::new();
200 filter.include("**/*.rs");
201 filter.exclude("**/test/**");
202
203 assert_eq!(filter.check(Path::new("src/main.rs")), FilterResult::Include);
204 assert_eq!(
205 filter.check(Path::new("src/lib/utils.rs")),
206 FilterResult::Include
207 );
208 }
209
210 #[test]
211 fn test_path_patterns() {
212 let mut filter = IncludeExclude::new();
213 filter.exclude("logs/*");
214
215 assert!(filter.should_exclude(Path::new("logs/app.log")));
216 assert!(!filter.should_exclude(Path::new("other/app.log")));
217 }
218
219 #[test]
220 fn test_multiple_patterns() {
221 let mut filter = IncludeExclude::new();
222 filter.include("*.rs");
223 filter.include("*.go");
224 filter.include("*.py");
225 filter.exclude("*_test.*");
226
227 assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
228 assert_eq!(filter.check(Path::new("server.go")), FilterResult::Include);
229 assert_eq!(filter.check(Path::new("main_test.rs")), FilterResult::Include); }
231
232 #[test]
233 fn test_brace_expansion() {
234 let mut filter = IncludeExclude::new();
235 filter.include("*.{rs,go,py}");
236
237 assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
238 assert_eq!(filter.check(Path::new("main.go")), FilterResult::Include);
239 assert_eq!(filter.check(Path::new("main.py")), FilterResult::Include);
240 assert_eq!(filter.check(Path::new("main.js")), FilterResult::NoMatch);
241 }
242}