Skip to main content

code_moniker_check/check/
exclude.rs

1use 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}