use globset::{Glob, GlobMatcher};
pub fn matches_any_pattern(path: &str, patterns: &[String]) -> bool {
if patterns.is_empty() {
return true; }
patterns.iter().any(|pattern| {
match Glob::new(pattern) {
Ok(glob) => {
let matcher = glob.compile_matcher();
if matcher.is_match(path) {
return true;
}
let path_no_slash = path.trim_start_matches('/');
if matcher.is_match(path_no_slash) {
return true;
}
if pattern.contains("**") || pattern.contains('*') {
let path_parts: Vec<&str> = path.split('/').collect();
for i in 0..path_parts.len() {
let suffix = path_parts[i..].join("/");
if matcher.is_match(&suffix) {
return true;
}
}
}
false
}
Err(e) => {
tracing::warn!(
"Invalid glob pattern '{}', falling back to substring match: {}",
pattern,
e
);
path.contains(pattern)
}
}
})
}
pub fn compile_patterns(patterns: &[String]) -> Option<Vec<GlobMatcher>> {
patterns
.iter()
.map(|pattern| {
Glob::new(pattern)
.map(|g| g.compile_matcher())
.map_err(|e| {
tracing::warn!("Failed to compile glob pattern '{}': {}", pattern, e);
e
})
.ok()
})
.collect()
}
pub fn matches_any_matcher(path: &str, matchers: &[GlobMatcher]) -> bool {
if matchers.is_empty() {
return true;
}
matchers.iter().any(|matcher| {
if matcher.is_match(path) {
return true;
}
let path_no_slash = path.trim_start_matches('/');
if matcher.is_match(path_no_slash) {
return true;
}
let path_parts: Vec<&str> = path.split('/').collect();
for i in 0..path_parts.len() {
let suffix = path_parts[i..].join("/");
if matcher.is_match(&suffix) {
return true;
}
}
false
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_matches_directory_glob() {
let patterns = vec!["lib/**".to_string()];
assert!(matches_any_pattern("/project/lib/utils.ts", &patterns));
assert!(matches_any_pattern("lib/nested/file.rs", &patterns));
assert!(!matches_any_pattern("/project/src/main.rs", &patterns));
}
#[test]
fn test_matches_extension_glob() {
let patterns = vec!["**/*.ts".to_string()];
assert!(matches_any_pattern("/project/src/main.ts", &patterns));
assert!(matches_any_pattern("lib/utils.ts", &patterns));
assert!(!matches_any_pattern("/project/src/main.rs", &patterns));
}
#[test]
fn test_matches_multiple_patterns() {
let patterns = vec!["lib/**".to_string(), "**/*.tsx".to_string()];
assert!(matches_any_pattern("/project/lib/utils.ts", &patterns));
assert!(matches_any_pattern("/project/src/Component.tsx", &patterns));
assert!(!matches_any_pattern("/project/src/main.rs", &patterns));
}
#[test]
fn test_matches_complex_glob() {
let patterns = vec!["src/components/**/*.ts".to_string()];
assert!(matches_any_pattern(
"/project/src/components/Button.ts",
&patterns
));
assert!(!matches_any_pattern("/project/lib/utils.ts", &patterns));
}
#[test]
fn test_empty_patterns() {
let patterns = vec![];
assert!(matches_any_pattern("/any/path.rs", &patterns));
}
#[test]
fn test_invalid_pattern_fallback() {
let patterns = vec!["[invalid".to_string()];
assert!(matches_any_pattern("/path/[invalid/file.rs", &patterns));
assert!(!matches_any_pattern("/path/valid/file.rs", &patterns));
}
#[test]
fn test_compile_patterns() {
let patterns = vec!["lib/**".to_string(), "**/*.rs".to_string()];
let matchers = compile_patterns(&patterns);
assert!(matchers.is_some());
let matchers = matchers.unwrap();
assert_eq!(matchers.len(), 2);
assert!(matches_any_matcher("/project/lib/utils.ts", &matchers));
assert!(matches_any_matcher("/project/src/main.rs", &matchers));
assert!(!matches_any_matcher("/project/test.txt", &matchers));
}
#[test]
fn test_compile_invalid_patterns() {
let patterns = vec!["lib/**".to_string(), "[invalid".to_string()];
let matchers = compile_patterns(&patterns);
assert!(matchers.is_none());
}
#[test]
fn test_matches_without_leading_slash() {
let patterns = vec!["lib/**".to_string()];
assert!(matches_any_pattern("lib/file.rs", &patterns));
assert!(matches_any_pattern("/lib/file.rs", &patterns));
}
#[test]
fn test_specific_file_pattern() {
let patterns = vec!["**/test.rs".to_string()];
assert!(matches_any_pattern("/project/src/test.rs", &patterns));
assert!(matches_any_pattern("test.rs", &patterns));
assert!(!matches_any_pattern("/project/src/main.rs", &patterns));
}
}