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 self.extra_directories.contains(name.as_ref())
155 })
156 }
157
158 fn is_test_path(&self, path: &Path) -> bool {
159 path.components().any(|c| {
160 let name = c.as_os_str().to_string_lossy();
161 name == "tests"
162 || name == "test"
163 || name == "__tests__"
164 || name == "spec"
165 || name == "specs"
166 || name.ends_with("_test")
167 || name.ends_with(".test")
168 })
169 }
170
171 fn is_node_modules_path(&self, path: &Path) -> bool {
172 path.components()
173 .any(|c| c.as_os_str().to_string_lossy() == "node_modules")
174 }
175
176 fn is_vendor_path(&self, path: &Path) -> bool {
177 path.components().any(|c| {
178 let name = c.as_os_str().to_string_lossy();
179 name == "vendor" || name == "vendors" || name == "third_party"
180 })
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::fs;
188 use tempfile::TempDir;
189
190 #[test]
191 fn test_default_excludes_tests() {
192 let dir = TempDir::new().unwrap();
193 let filter = IgnoreFilter::new(dir.path());
194
195 assert!(filter.is_ignored(Path::new("/project/tests/test_file.rs")));
196 assert!(filter.is_ignored(Path::new("/project/__tests__/spec.js")));
197 assert!(filter.is_ignored(Path::new("/project/spec/helpers.rb")));
198 assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
199 }
200
201 #[test]
202 fn test_default_excludes_node_modules() {
203 let dir = TempDir::new().unwrap();
204 let filter = IgnoreFilter::new(dir.path());
205
206 assert!(filter.is_ignored(Path::new("/project/node_modules/package/index.js")));
207 assert!(!filter.is_ignored(Path::new("/project/src/index.js")));
208 }
209
210 #[test]
211 fn test_default_excludes_vendor() {
212 let dir = TempDir::new().unwrap();
213 let filter = IgnoreFilter::new(dir.path());
214
215 assert!(filter.is_ignored(Path::new("/project/vendor/bundle/gems")));
216 assert!(filter.is_ignored(Path::new("/project/third_party/lib")));
217 assert!(!filter.is_ignored(Path::new("/project/src/lib")));
218 }
219
220 #[test]
221 fn test_include_tests() {
222 let dir = TempDir::new().unwrap();
223 let filter = IgnoreFilter::new(dir.path()).with_include_tests(true);
224
225 assert!(!filter.is_ignored(Path::new("/project/tests/test_file.rs")));
226 }
227
228 #[test]
229 fn test_include_node_modules() {
230 let dir = TempDir::new().unwrap();
231 let filter = IgnoreFilter::new(dir.path()).with_include_node_modules(true);
232
233 assert!(!filter.is_ignored(Path::new("/project/node_modules/package/index.js")));
234 }
235
236 #[test]
237 fn test_include_vendor() {
238 let dir = TempDir::new().unwrap();
239 let filter = IgnoreFilter::new(dir.path()).with_include_vendor(true);
240
241 assert!(!filter.is_ignored(Path::new("/project/vendor/bundle/gems")));
242 }
243
244 #[test]
245 fn test_custom_ignorefile() {
246 let dir = TempDir::new().unwrap();
247 let ignore_file = dir.path().join(".cc-auditignore");
248 fs::write(&ignore_file, "*.generated.js\nbuild/\n").unwrap();
249
250 let filter = IgnoreFilter::new(dir.path());
251
252 let generated_file = dir.path().join("app.generated.js");
253 fs::write(&generated_file, "").unwrap();
254
255 assert!(filter.is_ignored(&generated_file));
256 }
257
258 #[test]
259 fn test_no_ignorefile() {
260 let dir = TempDir::new().unwrap();
261 let filter = IgnoreFilter::new(dir.path());
262
263 assert!(!filter.is_ignored(&dir.path().join("src/main.rs")));
264 }
265
266 #[test]
267 fn test_default_trait() {
268 let filter = IgnoreFilter::default();
269
270 assert!(filter.is_ignored(Path::new("/project/tests/test.rs")));
272 assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
273 assert!(filter.is_ignored(Path::new("/project/vendor/lib")));
274 }
275
276 #[test]
277 fn test_chained_configuration() {
278 let dir = TempDir::new().unwrap();
279 let filter = IgnoreFilter::new(dir.path())
280 .with_include_tests(true)
281 .with_include_node_modules(true)
282 .with_include_vendor(true);
283
284 assert!(!filter.is_ignored(Path::new("/project/tests/test.rs")));
285 assert!(!filter.is_ignored(Path::new("/project/node_modules/pkg")));
286 assert!(!filter.is_ignored(Path::new("/project/vendor/lib")));
287 }
288
289 #[test]
290 fn test_gitignore_patterns() {
291 let dir = TempDir::new().unwrap();
292 let ignore_file = dir.path().join(".cc-auditignore");
293 fs::write(
294 &ignore_file,
295 r#"
296# Comment
297*.log
298/dist/
299!important.log
300"#,
301 )
302 .unwrap();
303
304 let filter = IgnoreFilter::new(dir.path());
305
306 let log_file = dir.path().join("debug.log");
307 fs::write(&log_file, "").unwrap();
308 assert!(filter.is_ignored(&log_file));
309
310 let src_file = dir.path().join("main.rs");
312 fs::write(&src_file, "").unwrap();
313 assert!(!filter.is_ignored(&src_file));
314 }
315
316 #[test]
317 fn test_is_test_path_variations() {
318 let filter = IgnoreFilter::default();
319
320 assert!(filter.is_test_path(Path::new("/project/tests/unit")));
321 assert!(filter.is_test_path(Path::new("/project/test/fixtures")));
322 assert!(filter.is_test_path(Path::new("/project/__tests__/spec")));
323 assert!(filter.is_test_path(Path::new("/project/spec/helpers")));
324 assert!(filter.is_test_path(Path::new("/project/specs/api")));
325 assert!(filter.is_test_path(Path::new("/project/file_test")));
326 assert!(filter.is_test_path(Path::new("/project/api.test")));
327 assert!(!filter.is_test_path(Path::new("/project/src/main.rs")));
328 assert!(!filter.is_test_path(Path::new("/project/contest/app.js"))); }
330
331 #[test]
332 fn test_from_config() {
333 use crate::config::IgnoreConfig;
334
335 let dir = TempDir::new().unwrap();
336 let config = IgnoreConfig {
337 directories: ["custom_ignore_dir", "my_cache"]
338 .into_iter()
339 .map(String::from)
340 .collect(),
341 patterns: vec!["*.generated.js".to_string()],
342 include_tests: true,
343 include_node_modules: false,
344 include_vendor: true,
345 };
346
347 let filter = IgnoreFilter::from_config(dir.path(), &config);
348
349 assert!(!filter.is_ignored(Path::new("/project/tests/test.rs")));
351 assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
353 assert!(!filter.is_ignored(Path::new("/project/vendor/lib")));
355 assert!(filter.is_ignored(Path::new("/project/custom_ignore_dir/file.rs")));
357 assert!(filter.is_ignored(Path::new("/project/my_cache/data")));
358 }
359
360 #[test]
361 fn test_extra_directories_ignored() {
362 use crate::config::IgnoreConfig;
363
364 let dir = TempDir::new().unwrap();
365 let mut config = IgnoreConfig::default();
366 config.directories.insert("my_special_dir".to_string());
367
368 let filter = IgnoreFilter::from_config(dir.path(), &config);
369
370 assert!(filter.is_ignored(Path::new("/project/my_special_dir/file.rs")));
372 assert!(filter.is_ignored(Path::new("/project/src/my_special_dir/nested/file.rs")));
374 }
375
376 #[test]
377 fn test_custom_patterns_from_config() {
378 use crate::config::IgnoreConfig;
379
380 let dir = TempDir::new().unwrap();
381 let config = IgnoreConfig {
382 directories: std::collections::HashSet::new(),
383 patterns: vec!["*.log".to_string(), "temp/**".to_string()],
384 include_tests: true,
385 include_node_modules: true,
386 include_vendor: true,
387 };
388
389 let filter = IgnoreFilter::from_config(dir.path(), &config);
390
391 let log_file = dir.path().join("debug.log");
393 fs::write(&log_file, "").unwrap();
394 assert!(filter.is_ignored(&log_file));
395
396 let temp_file = dir.path().join("temp/cache.txt");
397 fs::create_dir_all(dir.path().join("temp")).unwrap();
398 fs::write(&temp_file, "").unwrap();
399 assert!(filter.is_ignored(&temp_file));
400
401 let src_file = dir.path().join("main.rs");
403 fs::write(&src_file, "").unwrap();
404 assert!(!filter.is_ignored(&src_file));
405 }
406
407 #[test]
408 fn test_with_config_method() {
409 use crate::config::IgnoreConfig;
410
411 let dir = TempDir::new().unwrap();
412 let config = IgnoreConfig {
413 directories: ["target", "dist"].into_iter().map(String::from).collect(),
414 patterns: vec![],
415 include_tests: true,
416 include_node_modules: true,
417 include_vendor: false,
418 };
419
420 let filter = IgnoreFilter::new(dir.path()).with_config(&config);
421
422 assert!(!filter.is_ignored(Path::new("/project/tests/test.rs")));
424 assert!(!filter.is_ignored(Path::new("/project/node_modules/pkg")));
426 assert!(filter.is_ignored(Path::new("/project/vendor/lib")));
428 assert!(filter.is_ignored(Path::new("/project/target/debug")));
430 }
431}