code_moniker_check/check/
exclude.rs1use std::path::Path;
2
3use regex::Regex;
4
5use code_moniker_workspace::extract;
6use code_moniker_workspace::glob::compile_glob;
7
8#[derive(Debug, Clone)]
9pub struct UriExclusionMatcher {
10 patterns: Vec<CompiledUriPattern>,
11}
12
13#[derive(Debug, Clone)]
14struct CompiledUriPattern {
15 regex: Regex,
16}
17
18impl UriExclusionMatcher {
19 pub fn new(patterns: &[String]) -> Self {
20 let patterns = patterns
21 .iter()
22 .map(|raw| CompiledUriPattern {
23 regex: compile_glob(&normalize_uri(raw)),
24 })
25 .collect();
26 Self { patterns }
27 }
28
29 pub fn matches_path(&self, path: &Path) -> bool {
30 if self.patterns.is_empty() {
31 return false;
32 }
33 let candidates = path_candidates(path);
34 self.patterns.iter().any(|pattern| {
35 candidates
36 .iter()
37 .any(|candidate| pattern.regex.is_match(candidate))
38 })
39 }
40}
41
42fn path_candidates(path: &Path) -> Vec<String> {
43 let mut candidates = Vec::new();
44 push_unique(&mut candidates, normalize_uri(&extract::file_uri(path)));
45 push_unique(&mut candidates, normalize_path(path));
46 if let Ok(abs) = path.canonicalize() {
47 push_unique(&mut candidates, normalize_path(&abs));
48 push_unique(&mut candidates, normalize_uri(&extract::file_uri(&abs)));
49 }
50 candidates
51}
52
53fn push_unique(values: &mut Vec<String>, value: String) {
54 if !values.iter().any(|existing| existing == &value) {
55 values.push(value);
56 }
57}
58
59fn normalize_path(path: &Path) -> String {
60 path.to_string_lossy().replace('\\', "/")
61}
62
63fn normalize_uri(value: &str) -> String {
64 value.replace('\\', "/")
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn double_star_slash_can_match_no_prefix() {
73 let matcher = UriExclusionMatcher::new(&["**/crates/core/tests/fixtures/**".to_string()]);
74
75 assert!(matcher.matches_path(Path::new(
76 "crates/core/tests/fixtures/extractors/rs/accounts.rs"
77 )));
78 }
79
80 #[test]
81 fn uri_pattern_matches_absolute_file_uri_candidate() {
82 let matcher = UriExclusionMatcher::new(&["**/crates/core/tests/fixtures/**".to_string()]);
83 let path =
84 Path::new("/tmp/project/crates/core/tests/fixtures/extractors/java/UserService.java");
85
86 assert!(matcher.matches_path(path));
87 }
88
89 #[test]
90 fn single_star_stays_within_one_path_segment() {
91 let matcher = UriExclusionMatcher::new(&["**/fixtures/*.rs".to_string()]);
92
93 assert!(matcher.matches_path(Path::new("crates/core/tests/fixtures/accounts.rs")));
94 assert!(!matcher.matches_path(Path::new(
95 "crates/core/tests/fixtures/extractors/rs/accounts.rs"
96 )));
97 }
98}