1use crate::config::IgnoreConfig;
6use regex::Regex;
7use std::path::Path;
8use tracing::warn;
9
10#[derive(Default)]
14pub struct IgnoreFilter {
15 patterns: Vec<Regex>,
17}
18
19impl IgnoreFilter {
20 pub fn new() -> Self {
22 Self {
23 patterns: Vec::new(),
24 }
25 }
26
27 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 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 pub fn is_ignored(&self, path: &Path) -> bool {
56 if self.patterns.is_empty() {
57 return false;
58 }
59
60 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(), "also_valid".to_string(),
123 ],
124 };
125 let filter = IgnoreFilter::from_config(&config);
126
127 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}