1use crate::config::IgnoreConfig;
2use ignore::gitignore::{Gitignore, GitignoreBuilder};
3use std::collections::HashSet;
4use std::path::Path;
5
6#[derive(Default)]
7pub struct IgnoreFilter {
8 gitignore: Option<Gitignore>,
9 include_tests: bool,
10 include_node_modules: bool,
11 include_vendor: bool,
12 extra_directories: HashSet<String>,
14 custom_patterns: Option<Gitignore>,
16}
17
18impl IgnoreFilter {
19 pub fn new(root: &Path) -> Self {
20 let gitignore = Self::load_ignorefiles(root);
21
22 Self {
23 gitignore,
24 include_tests: false,
25 include_node_modules: false,
26 include_vendor: false,
27 extra_directories: HashSet::new(),
28 custom_patterns: None,
29 }
30 }
31
32 pub fn from_config(root: &Path, config: &IgnoreConfig) -> Self {
34 let gitignore = Self::load_ignorefiles(root);
35 let custom_patterns = Self::build_custom_patterns(root, &config.patterns);
36
37 Self {
38 gitignore,
39 include_tests: config.include_tests,
40 include_node_modules: config.include_node_modules,
41 include_vendor: config.include_vendor,
42 extra_directories: config.directories.clone(),
43 custom_patterns,
44 }
45 }
46
47 pub fn with_config(mut self, config: &IgnoreConfig) -> Self {
49 self.include_tests = config.include_tests;
50 self.include_node_modules = config.include_node_modules;
51 self.include_vendor = config.include_vendor;
52 self.extra_directories = config.directories.clone();
53 self
55 }
56
57 pub fn with_include_tests(mut self, include: bool) -> Self {
58 self.include_tests = include;
59 self
60 }
61
62 pub fn with_include_node_modules(mut self, include: bool) -> Self {
63 self.include_node_modules = include;
64 self
65 }
66
67 pub fn with_include_vendor(mut self, include: bool) -> Self {
68 self.include_vendor = include;
69 self
70 }
71
72 fn build_custom_patterns(root: &Path, patterns: &[String]) -> Option<Gitignore> {
74 if patterns.is_empty() {
75 return None;
76 }
77
78 let mut builder = GitignoreBuilder::new(root);
79 for pattern in patterns {
80 let _ = builder.add_line(None, pattern);
82 }
83
84 builder.build().ok()
85 }
86
87 fn load_ignorefiles(root: &Path) -> Option<Gitignore> {
88 let mut builder = GitignoreBuilder::new(root);
89 let mut has_patterns = false;
90
91 let git_dir = root.join(".git");
93 let gitignore_file = root.join(".gitignore");
94 if git_dir.exists() && gitignore_file.exists() && builder.add(&gitignore_file).is_none() {
95 has_patterns = true;
96 }
97
98 let cc_audit_ignore = root.join(".cc-auditignore");
100 if cc_audit_ignore.exists() && builder.add(&cc_audit_ignore).is_none() {
101 has_patterns = true;
102 }
103
104 if has_patterns {
105 builder.build().ok()
106 } else {
107 None
108 }
109 }
110
111 pub fn is_ignored(&self, path: &Path) -> bool {
112 if !self.include_tests && self.is_test_path(path) {
114 return true;
115 }
116
117 if !self.include_node_modules && self.is_node_modules_path(path) {
118 return true;
119 }
120
121 if !self.include_vendor && self.is_vendor_path(path) {
122 return true;
123 }
124
125 if self.is_in_extra_directories(path) {
127 return true;
128 }
129
130 if let Some(ref custom) = self.custom_patterns {
132 let is_dir = path.is_dir();
133 if custom.matched(path, is_dir).is_ignore() {
134 return true;
135 }
136 }
137
138 if let Some(ref gitignore) = self.gitignore {
140 let is_dir = path.is_dir();
141 return gitignore.matched(path, is_dir).is_ignore();
142 }
143
144 false
145 }
146
147 fn is_in_extra_directories(&self, path: &Path) -> bool {
148 if self.extra_directories.is_empty() {
149 return false;
150 }
151
152 path.components().any(|c| {
153 let name = c.as_os_str().to_string_lossy();
154 if self.include_node_modules && name == "node_modules" {
156 return false;
157 }
158 if self.include_vendor
160 && (name == "vendor" || name == "vendors" || name == "third_party")
161 {
162 return false;
163 }
164 self.extra_directories.contains(name.as_ref())
165 })
166 }
167
168 fn is_test_path(&self, path: &Path) -> bool {
169 path.components().any(|c| {
170 let name = c.as_os_str().to_string_lossy();
171 name == "tests"
172 || name == "test"
173 || name == "__tests__"
174 || name == "spec"
175 || name == "specs"
176 || name.ends_with("_test")
177 || name.ends_with(".test")
178 })
179 }
180
181 fn is_node_modules_path(&self, path: &Path) -> bool {
182 path.components()
183 .any(|c| c.as_os_str().to_string_lossy() == "node_modules")
184 }
185
186 fn is_vendor_path(&self, path: &Path) -> bool {
187 path.components().any(|c| {
188 let name = c.as_os_str().to_string_lossy();
189 name == "vendor" || name == "vendors" || name == "third_party"
190 })
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197 use std::fs;
198 use tempfile::TempDir;
199
200 #[test]
201 fn test_default_excludes_tests() {
202 let dir = TempDir::new().unwrap();
203 let filter = IgnoreFilter::new(dir.path());
204
205 assert!(filter.is_ignored(Path::new("/project/tests/test_file.rs")));
206 assert!(filter.is_ignored(Path::new("/project/__tests__/spec.js")));
207 assert!(filter.is_ignored(Path::new("/project/spec/helpers.rb")));
208 assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
209 }
210
211 #[test]
212 fn test_default_excludes_node_modules() {
213 let dir = TempDir::new().unwrap();
214 let filter = IgnoreFilter::new(dir.path());
215
216 assert!(filter.is_ignored(Path::new("/project/node_modules/package/index.js")));
217 assert!(!filter.is_ignored(Path::new("/project/src/index.js")));
218 }
219
220 #[test]
221 fn test_default_excludes_vendor() {
222 let dir = TempDir::new().unwrap();
223 let filter = IgnoreFilter::new(dir.path());
224
225 assert!(filter.is_ignored(Path::new("/project/vendor/bundle/gems")));
226 assert!(filter.is_ignored(Path::new("/project/third_party/lib")));
227 assert!(!filter.is_ignored(Path::new("/project/src/lib")));
228 }
229
230 #[test]
231 fn test_include_tests() {
232 let dir = TempDir::new().unwrap();
233 let filter = IgnoreFilter::new(dir.path()).with_include_tests(true);
234
235 assert!(!filter.is_ignored(Path::new("/project/tests/test_file.rs")));
236 }
237
238 #[test]
239 fn test_include_node_modules() {
240 let dir = TempDir::new().unwrap();
241 let filter = IgnoreFilter::new(dir.path()).with_include_node_modules(true);
242
243 assert!(!filter.is_ignored(Path::new("/project/node_modules/package/index.js")));
244 }
245
246 #[test]
247 fn test_include_vendor() {
248 let dir = TempDir::new().unwrap();
249 let filter = IgnoreFilter::new(dir.path()).with_include_vendor(true);
250
251 assert!(!filter.is_ignored(Path::new("/project/vendor/bundle/gems")));
252 }
253
254 #[test]
255 fn test_custom_ignorefile() {
256 let dir = TempDir::new().unwrap();
257 let ignore_file = dir.path().join(".cc-auditignore");
258 fs::write(&ignore_file, "*.generated.js\nbuild/\n").unwrap();
259
260 let filter = IgnoreFilter::new(dir.path());
261
262 let generated_file = dir.path().join("app.generated.js");
263 fs::write(&generated_file, "").unwrap();
264
265 assert!(filter.is_ignored(&generated_file));
266 }
267
268 #[test]
269 fn test_no_ignorefile() {
270 let dir = TempDir::new().unwrap();
271 let filter = IgnoreFilter::new(dir.path());
272
273 assert!(!filter.is_ignored(&dir.path().join("src/main.rs")));
274 }
275
276 #[test]
277 fn test_default_trait() {
278 let filter = IgnoreFilter::default();
279
280 assert!(filter.is_ignored(Path::new("/project/tests/test.rs")));
282 assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
283 assert!(filter.is_ignored(Path::new("/project/vendor/lib")));
284 }
285
286 #[test]
287 fn test_chained_configuration() {
288 let dir = TempDir::new().unwrap();
289 let filter = IgnoreFilter::new(dir.path())
290 .with_include_tests(true)
291 .with_include_node_modules(true)
292 .with_include_vendor(true);
293
294 assert!(!filter.is_ignored(Path::new("/project/tests/test.rs")));
295 assert!(!filter.is_ignored(Path::new("/project/node_modules/pkg")));
296 assert!(!filter.is_ignored(Path::new("/project/vendor/lib")));
297 }
298
299 #[test]
300 fn test_gitignore_patterns() {
301 let dir = TempDir::new().unwrap();
302 let ignore_file = dir.path().join(".cc-auditignore");
303 fs::write(
304 &ignore_file,
305 r#"
306# Comment
307*.log
308/dist/
309!important.log
310"#,
311 )
312 .unwrap();
313
314 let filter = IgnoreFilter::new(dir.path());
315
316 let log_file = dir.path().join("debug.log");
317 fs::write(&log_file, "").unwrap();
318 assert!(filter.is_ignored(&log_file));
319
320 let src_file = dir.path().join("main.rs");
322 fs::write(&src_file, "").unwrap();
323 assert!(!filter.is_ignored(&src_file));
324 }
325
326 #[test]
327 fn test_is_test_path_variations() {
328 let filter = IgnoreFilter::default();
329
330 assert!(filter.is_test_path(Path::new("/project/tests/unit")));
331 assert!(filter.is_test_path(Path::new("/project/test/fixtures")));
332 assert!(filter.is_test_path(Path::new("/project/__tests__/spec")));
333 assert!(filter.is_test_path(Path::new("/project/spec/helpers")));
334 assert!(filter.is_test_path(Path::new("/project/specs/api")));
335 assert!(filter.is_test_path(Path::new("/project/file_test")));
336 assert!(filter.is_test_path(Path::new("/project/api.test")));
337 assert!(!filter.is_test_path(Path::new("/project/src/main.rs")));
338 assert!(!filter.is_test_path(Path::new("/project/contest/app.js"))); }
340
341 #[test]
342 fn test_from_config() {
343 use crate::config::IgnoreConfig;
344
345 let dir = TempDir::new().unwrap();
346 let config = IgnoreConfig {
347 directories: ["custom_ignore_dir", "my_cache"]
348 .into_iter()
349 .map(String::from)
350 .collect(),
351 patterns: vec!["*.generated.js".to_string()],
352 include_tests: true,
353 include_node_modules: false,
354 include_vendor: true,
355 };
356
357 let filter = IgnoreFilter::from_config(dir.path(), &config);
358
359 assert!(!filter.is_ignored(Path::new("/project/tests/test.rs")));
361 assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
363 assert!(!filter.is_ignored(Path::new("/project/vendor/lib")));
365 assert!(filter.is_ignored(Path::new("/project/custom_ignore_dir/file.rs")));
367 assert!(filter.is_ignored(Path::new("/project/my_cache/data")));
368 }
369
370 #[test]
371 fn test_extra_directories_ignored() {
372 use crate::config::IgnoreConfig;
373
374 let dir = TempDir::new().unwrap();
375 let mut config = IgnoreConfig::default();
376 config.directories.insert("my_special_dir".to_string());
377
378 let filter = IgnoreFilter::from_config(dir.path(), &config);
379
380 assert!(filter.is_ignored(Path::new("/project/my_special_dir/file.rs")));
382 assert!(filter.is_ignored(Path::new("/project/src/my_special_dir/nested/file.rs")));
384 }
385
386 #[test]
387 fn test_custom_patterns_from_config() {
388 use crate::config::IgnoreConfig;
389
390 let dir = TempDir::new().unwrap();
391 let config = IgnoreConfig {
392 directories: std::collections::HashSet::new(),
393 patterns: vec!["*.log".to_string(), "temp/**".to_string()],
394 include_tests: true,
395 include_node_modules: true,
396 include_vendor: true,
397 };
398
399 let filter = IgnoreFilter::from_config(dir.path(), &config);
400
401 let log_file = dir.path().join("debug.log");
403 fs::write(&log_file, "").unwrap();
404 assert!(filter.is_ignored(&log_file));
405
406 let temp_file = dir.path().join("temp/cache.txt");
407 fs::create_dir_all(dir.path().join("temp")).unwrap();
408 fs::write(&temp_file, "").unwrap();
409 assert!(filter.is_ignored(&temp_file));
410
411 let src_file = dir.path().join("main.rs");
413 fs::write(&src_file, "").unwrap();
414 assert!(!filter.is_ignored(&src_file));
415 }
416
417 #[test]
418 fn test_with_config_method() {
419 use crate::config::IgnoreConfig;
420
421 let dir = TempDir::new().unwrap();
422 let config = IgnoreConfig {
423 directories: ["target", "dist"].into_iter().map(String::from).collect(),
424 patterns: vec![],
425 include_tests: true,
426 include_node_modules: true,
427 include_vendor: false,
428 };
429
430 let filter = IgnoreFilter::new(dir.path()).with_config(&config);
431
432 assert!(!filter.is_ignored(Path::new("/project/tests/test.rs")));
434 assert!(!filter.is_ignored(Path::new("/project/node_modules/pkg")));
436 assert!(filter.is_ignored(Path::new("/project/vendor/lib")));
438 assert!(filter.is_ignored(Path::new("/project/target/debug")));
440 }
441}