Skip to main content

cc_audit/
ignore.rs

1//! Ignore filter for scanning.
2//!
3//! Simple regex-based filtering for paths during scanning.
4
5use crate::config::IgnoreConfig;
6use regex::Regex;
7use std::path::Path;
8use tracing::warn;
9
10/// Filter for ignoring paths during scanning.
11///
12/// Uses regex patterns to determine which paths to skip.
13#[derive(Default)]
14pub struct IgnoreFilter {
15    /// Compiled regex patterns for ignoring paths.
16    patterns: Vec<Regex>,
17}
18
19impl IgnoreFilter {
20    /// Create a new empty IgnoreFilter.
21    pub fn new() -> Self {
22        Self {
23            patterns: Vec::new(),
24        }
25    }
26
27    /// Create IgnoreFilter from config.
28    pub fn from_config(config: &IgnoreConfig) -> Self {
29        let patterns = config
30            .patterns
31            .iter()
32            .filter_map(|p| match Regex::new(p) {
33                Ok(regex) => Some(regex),
34                Err(e) => {
35                    warn!(pattern = %p, error = %e, "Invalid ignore pattern");
36                    None
37                }
38            })
39            .collect();
40
41        Self { patterns }
42    }
43
44    /// Add a regex pattern to the filter.
45    pub fn add_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
46        let regex = Regex::new(pattern)?;
47        self.patterns.push(regex);
48        Ok(())
49    }
50
51    /// Check if a path should be ignored.
52    ///
53    /// Path separators are normalized to forward slashes for cross-platform
54    /// compatibility.
55    pub fn is_ignored(&self, path: &Path) -> bool {
56        if self.patterns.is_empty() {
57            return false;
58        }
59
60        // Normalize path separators to forward slashes for cross-platform matching
61        let path_str = path.to_string_lossy().replace('\\', "/");
62        self.patterns.iter().any(|p| p.is_match(&path_str))
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn test_empty_filter() {
72        let filter = IgnoreFilter::new();
73        assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
74    }
75
76    #[test]
77    fn test_simple_pattern() {
78        let config = IgnoreConfig {
79            patterns: vec!["node_modules".to_string()],
80        };
81        let filter = IgnoreFilter::from_config(&config);
82
83        assert!(filter.is_ignored(Path::new("/project/node_modules/pkg/index.js")));
84        assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
85    }
86
87    #[test]
88    fn test_regex_pattern() {
89        let config = IgnoreConfig {
90            patterns: vec![r"\.test\.(js|ts)$".to_string()],
91        };
92        let filter = IgnoreFilter::from_config(&config);
93
94        assert!(filter.is_ignored(Path::new("/project/src/app.test.js")));
95        assert!(filter.is_ignored(Path::new("/project/src/app.test.ts")));
96        assert!(!filter.is_ignored(Path::new("/project/src/app.js")));
97    }
98
99    #[test]
100    fn test_multiple_patterns() {
101        let config = IgnoreConfig {
102            patterns: vec![
103                "node_modules".to_string(),
104                "target".to_string(),
105                r"\.git/".to_string(),
106            ],
107        };
108        let filter = IgnoreFilter::from_config(&config);
109
110        assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
111        assert!(filter.is_ignored(Path::new("/project/target/debug/main")));
112        assert!(filter.is_ignored(Path::new("/project/.git/config")));
113        assert!(!filter.is_ignored(Path::new("/project/src/main.rs")));
114    }
115
116    #[test]
117    fn test_invalid_pattern_is_skipped() {
118        let config = IgnoreConfig {
119            patterns: vec![
120                "valid".to_string(),
121                "[invalid".to_string(), // Invalid regex
122                "also_valid".to_string(),
123            ],
124        };
125        let filter = IgnoreFilter::from_config(&config);
126
127        // Should have only 2 valid patterns
128        assert_eq!(filter.patterns.len(), 2);
129        assert!(filter.is_ignored(Path::new("/project/valid/file")));
130        assert!(filter.is_ignored(Path::new("/project/also_valid/file")));
131    }
132
133    #[test]
134    fn test_add_pattern() {
135        let mut filter = IgnoreFilter::new();
136        filter.add_pattern("node_modules").unwrap();
137
138        assert!(filter.is_ignored(Path::new("/project/node_modules/pkg")));
139    }
140
141    #[test]
142    fn test_directory_pattern() {
143        let config = IgnoreConfig {
144            patterns: vec![r"/tests?/".to_string()],
145        };
146        let filter = IgnoreFilter::from_config(&config);
147
148        assert!(filter.is_ignored(Path::new("/project/tests/unit.rs")));
149        assert!(filter.is_ignored(Path::new("/project/test/unit.rs")));
150        assert!(!filter.is_ignored(Path::new("/project/src/contest.rs")));
151    }
152
153    #[test]
154    fn test_extension_pattern() {
155        let config = IgnoreConfig {
156            patterns: vec![r"\.(log|tmp|bak)$".to_string()],
157        };
158        let filter = IgnoreFilter::from_config(&config);
159
160        assert!(filter.is_ignored(Path::new("/project/debug.log")));
161        assert!(filter.is_ignored(Path::new("/project/session.tmp")));
162        assert!(filter.is_ignored(Path::new("/project/config.bak")));
163        assert!(!filter.is_ignored(Path::new("/project/main.rs")));
164    }
165}