stylex_compiler_rs/utils/
path_filter.rs1use crate::enums::PathFilterUnion;
2use fancy_regex::Regex;
3use glob::Pattern as GlobPattern;
4use log::warn;
5use std::env;
6use std::path::Path;
7
8pub(crate) fn should_transform_file(
10 file_path: &str,
11 include: &Option<Vec<PathFilterUnion>>,
12 exclude: &Option<Vec<PathFilterUnion>>,
13) -> bool {
14 let cwd = env::current_dir().unwrap_or_default();
15 let file_path_buf = Path::new(file_path);
16
17 let relative_path = file_path_buf
19 .strip_prefix(&cwd)
20 .unwrap_or(file_path_buf)
21 .to_string_lossy();
22
23 let relative_path = relative_path.replace('\\', "/");
25
26 if let Some(include_patterns) = include
28 && !include_patterns.is_empty()
29 {
30 let included = include_patterns
31 .iter()
32 .any(|pattern| match_pattern(&relative_path, pattern));
33 if !included {
34 return false;
35 }
36 }
37
38 if let Some(exclude_patterns) = exclude {
40 let excluded = exclude_patterns
41 .iter()
42 .any(|pattern| match_pattern(&relative_path, pattern));
43 if excluded {
44 return false;
45 }
46 }
47
48 true
49}
50
51fn match_pattern(file_path: &str, pattern: &PathFilterUnion) -> bool {
53 match pattern {
54 PathFilterUnion::Glob(glob) => GlobPattern::new(glob)
55 .map(|p| p.matches(file_path))
56 .unwrap_or_else(|e| {
57 warn!(
58 "Invalid glob pattern '{}': {}. Skipping pattern match.",
59 glob, e
60 );
61 false
62 }),
63 PathFilterUnion::Regex(regex_str) => match Regex::new(regex_str) {
64 Ok(r) => match r.is_match(file_path) {
65 Ok(matched) => matched,
66 Err(e) => {
67 warn!(
68 "Error matching regex pattern '{}' against '{}': {}. Skipping pattern match.",
69 regex_str, file_path, e
70 );
71 false
72 },
73 },
74 Err(e) => {
75 warn!(
76 "Invalid regex pattern '{}': {}. Skipping pattern match.",
77 regex_str, e
78 );
79 false
80 },
81 },
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_match_pattern_glob() {
91 let pattern = PathFilterUnion::Glob("src/**/*.rs".to_string());
92
93 assert!(match_pattern("src/main.rs", &pattern));
94 assert!(match_pattern("src/lib/utils.rs", &pattern));
95 assert!(match_pattern("src/deep/nested/file.rs", &pattern));
96 assert!(!match_pattern("lib/main.rs", &pattern));
97 assert!(!match_pattern("src/main.ts", &pattern));
98 }
99
100 #[test]
101 fn test_match_pattern_regex() {
102 let pattern = PathFilterUnion::Regex(r"\.test\.rs$".to_string());
103
104 assert!(match_pattern("src/button.test.rs", &pattern));
105 assert!(match_pattern("lib/component.test.rs", &pattern));
106 assert!(!match_pattern("src/button.rs", &pattern));
107 assert!(!match_pattern("src/button.test.ts", &pattern));
108 }
109
110 #[test]
111 fn test_match_pattern_regex_negative_lookbehind() {
112 let pattern = PathFilterUnion::Regex(r"(?<!src/).*\.tsx$".to_string());
115
116 assert!(match_pattern("lib/components/Button.tsx", &pattern));
119 assert!(match_pattern("src/components/Button.tsx", &pattern));
120
121 let pattern2 = PathFilterUnion::Regex(r"^(?!src/).*\.tsx$".to_string());
124 assert!(match_pattern("lib/components/Button.tsx", &pattern2));
125 assert!(!match_pattern("src/components/Button.tsx", &pattern2));
126 }
127
128 #[test]
129 fn test_match_pattern_regex_negative_lookahead() {
130 let pattern = PathFilterUnion::Regex(r"node_modules(?!/@stylexjs)".to_string());
132
133 assert!(match_pattern(
135 "node_modules/some-package/index.js",
136 &pattern
137 ));
138
139 assert!(!match_pattern(
141 "node_modules/@stylexjs/stylex/index.js",
142 &pattern
143 ));
144 }
145
146 #[test]
147 fn test_match_pattern_complex_glob() {
148 let pattern_rs = PathFilterUnion::Glob("**/*.rs".to_string());
151 let pattern_toml = PathFilterUnion::Glob("**/*.toml".to_string());
152
153 assert!(match_pattern("src/main.rs", &pattern_rs));
154 assert!(match_pattern("Cargo.toml", &pattern_toml));
155
156 let pattern_braces = PathFilterUnion::Glob("**/*.{rs,toml}".to_string());
158 assert!(!match_pattern("Cargo.toml", &pattern_braces));
159 }
160
161 #[test]
162 fn test_should_transform_file_no_patterns() {
163 let result = should_transform_file("src/main.rs", &None, &None);
164 assert!(result);
165 }
166
167 #[test]
168 fn test_should_transform_file_empty_patterns() {
169 let result = should_transform_file("src/main.rs", &Some(vec![]), &Some(vec![]));
170 assert!(result);
171 }
172
173 #[test]
174 fn test_should_transform_file_include_glob() {
175 let include = Some(vec![PathFilterUnion::Glob("src/**/*.rs".to_string())]);
176
177 assert!(should_transform_file("src/main.rs", &include, &None));
179 assert!(should_transform_file("src/lib/utils.rs", &include, &None));
180 assert!(!should_transform_file("lib/main.rs", &include, &None));
181 }
182
183 #[test]
184 fn test_should_transform_file_include_regex() {
185 let include = Some(vec![PathFilterUnion::Regex(r"^src/.*\.rs$".to_string())]);
186
187 assert!(should_transform_file("src/main.rs", &include, &None));
188 assert!(should_transform_file("src/utils.rs", &include, &None));
189 assert!(!should_transform_file("lib/main.rs", &include, &None));
190 }
191
192 #[test]
193 fn test_should_transform_file_exclude_glob() {
194 let exclude = Some(vec![PathFilterUnion::Glob("**/*.test.rs".to_string())]);
195
196 assert!(should_transform_file("src/main.rs", &None, &exclude));
197 assert!(!should_transform_file("src/main.test.rs", &None, &exclude));
198 assert!(!should_transform_file("lib/utils.test.rs", &None, &exclude));
199 }
200
201 #[test]
202 fn test_should_transform_file_exclude_regex() {
203 let exclude = Some(vec![PathFilterUnion::Regex(r"\.test\.rs$".to_string())]);
204
205 assert!(should_transform_file("src/main.rs", &None, &exclude));
206 assert!(!should_transform_file("src/main.test.rs", &None, &exclude));
207 }
208
209 #[test]
210 fn test_should_transform_file_multiple_include_patterns() {
211 let include = Some(vec![
212 PathFilterUnion::Glob("src/**/*.rs".to_string()),
213 PathFilterUnion::Glob("app/**/*.rs".to_string()),
214 ]);
215
216 assert!(should_transform_file("src/main.rs", &include, &None));
217 assert!(should_transform_file("app/main.rs", &include, &None));
218 assert!(!should_transform_file("lib/main.rs", &include, &None));
219 }
220
221 #[test]
222 fn test_should_transform_file_multiple_exclude_patterns() {
223 let exclude = Some(vec![
224 PathFilterUnion::Glob("**/*.test.rs".to_string()),
225 PathFilterUnion::Glob("**/*.spec.rs".to_string()),
226 ]);
227
228 assert!(should_transform_file("src/main.rs", &None, &exclude));
229 assert!(!should_transform_file("src/main.test.rs", &None, &exclude));
230 assert!(!should_transform_file("src/main.spec.rs", &None, &exclude));
231 }
232
233 #[test]
234 fn test_should_transform_file_combined_include_exclude() {
235 let include = Some(vec![PathFilterUnion::Glob("src/**/*.rs".to_string())]);
236 let exclude = Some(vec![PathFilterUnion::Glob("**/*.test.rs".to_string())]);
237
238 assert!(should_transform_file("src/main.rs", &include, &exclude));
239 assert!(!should_transform_file(
240 "src/main.test.rs",
241 &include,
242 &exclude
243 ));
244 assert!(!should_transform_file("lib/main.rs", &include, &exclude));
245 }
246
247 #[test]
248 fn test_should_transform_file_mixed_patterns() {
249 let include = Some(vec![
250 PathFilterUnion::Glob("src/**/*.rs".to_string()),
251 PathFilterUnion::Regex(r"^app/.*\.rs$".to_string()),
252 ]);
253 let exclude = Some(vec![
254 PathFilterUnion::Glob("**/__tests__/**".to_string()),
255 PathFilterUnion::Regex(r"\.test\.rs$".to_string()),
256 ]);
257
258 assert!(should_transform_file("src/main.rs", &include, &exclude));
259 assert!(should_transform_file("app/main.rs", &include, &exclude));
260 assert!(!should_transform_file(
261 "src/__tests__/main.rs",
262 &include,
263 &exclude
264 ));
265 assert!(!should_transform_file(
266 "src/main.test.rs",
267 &include,
268 &exclude
269 ));
270 assert!(!should_transform_file("lib/main.rs", &include, &exclude));
271 }
272
273 #[test]
274 fn test_should_transform_file_exclude_takes_precedence() {
275 let include = Some(vec![PathFilterUnion::Glob("src/**/*.rs".to_string())]);
277 let exclude = Some(vec![PathFilterUnion::Glob("src/__tests__/**".to_string())]);
278
279 assert!(should_transform_file("src/main.rs", &include, &exclude));
280 assert!(!should_transform_file(
281 "src/__tests__/main.rs",
282 &include,
283 &exclude
284 ));
285 }
286
287 #[test]
288 fn test_match_pattern_edge_cases() {
289 let pattern = PathFilterUnion::Glob("**/.*.rs".to_string());
291 assert!(match_pattern(".hidden.rs", &pattern));
292
293 let pattern2 = PathFilterUnion::Glob("**/*.test.rs".to_string());
295 assert!(match_pattern("component.test.rs", &pattern2));
296 }
297
298 #[test]
299 fn test_invalid_regex_pattern() {
300 let pattern = PathFilterUnion::Regex("[invalid(".to_string());
302 assert!(!match_pattern("src/main.rs", &pattern));
303 }
304
305 #[test]
306 fn test_invalid_glob_pattern() {
307 let pattern = PathFilterUnion::Glob("[invalid".to_string());
309 assert!(!match_pattern("src/main.rs", &pattern));
310 }
311}