use glob::Pattern;
use std::collections::HashMap;
use std::sync::OnceLock;
static PATTERN_CACHE: OnceLock<std::sync::Mutex<HashMap<String, Option<Pattern>>>> =
OnceLock::new();
fn get_pattern_cache() -> &'static std::sync::Mutex<HashMap<String, Option<Pattern>>> {
PATTERN_CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
}
fn compile_pattern(pattern: &str) -> Option<Pattern> {
let cache = get_pattern_cache();
let mut cache_guard = cache.lock().unwrap();
if let Some(cached) = cache_guard.get(pattern) {
return cached.clone();
}
let compiled = Pattern::new(pattern).ok();
cache_guard.insert(pattern.to_owned(), compiled.clone());
compiled
}
pub fn matches_exclude_pattern(path: &str, pattern: &str) -> bool {
if pattern.is_empty() {
return false;
}
if pattern.contains('*') {
if let Some(glob) = compile_pattern(pattern) {
if glob.matches(path) {
return true;
}
}
}
if path.split('/').any(|part| part == pattern) {
return true;
}
if path.starts_with(pattern) {
return true;
}
false
}
pub fn matches_include_pattern(path: &str, pattern: &str) -> bool {
if pattern.is_empty() {
return false;
}
if pattern.contains('*') {
if let Some(glob) = compile_pattern(pattern) {
return glob.matches(path);
}
}
path.contains(pattern) || path.ends_with(pattern)
}
pub fn apply_exclude_patterns<T, F>(items: &mut Vec<T>, patterns: &[String], get_path: F)
where
F: Fn(&T) -> &str,
{
if patterns.is_empty() {
return;
}
items.retain(|item| {
let path = get_path(item);
!patterns
.iter()
.any(|pattern| matches_exclude_pattern(path, pattern))
});
}
pub fn apply_include_patterns<T, F>(items: &mut Vec<T>, patterns: &[String], get_path: F)
where
F: Fn(&T) -> &str,
{
if patterns.is_empty() {
return;
}
items.retain(|item| {
let path = get_path(item);
patterns
.iter()
.any(|pattern| matches_include_pattern(path, pattern))
});
}
pub fn compile_patterns(patterns: &[String]) -> Vec<Pattern> {
patterns.iter().filter_map(|p| compile_pattern(p)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_exclude_glob_patterns() {
assert!(matches_exclude_pattern("foo.min.js", "*.min.js"));
assert!(matches_exclude_pattern("dist/bundle.min.js", "*.min.js"));
assert!(!matches_exclude_pattern("foo.js", "*.min.js"));
}
#[test]
fn test_exclude_glob_recursive() {
assert!(matches_exclude_pattern("src/tests/foo.rs", "**/tests/**"));
assert!(matches_exclude_pattern("tests/unit/bar.rs", "**/tests/**"));
assert!(!matches_exclude_pattern("src/main.rs", "**/tests/**"));
}
#[test]
fn test_exclude_substring_match() {
assert!(matches_exclude_pattern("node_modules/foo/bar.js", "node_modules"));
assert!(matches_exclude_pattern("dist/bundle.js", "dist"));
assert!(!matches_exclude_pattern("src/index.ts", "dist"));
}
#[test]
fn test_exclude_prefix_match() {
assert!(matches_exclude_pattern("target/debug/main", "target"));
assert!(matches_exclude_pattern("vendor/lib.js", "vendor"));
assert!(!matches_exclude_pattern("src/target.rs", "target"));
}
#[test]
fn test_exclude_component_match() {
assert!(matches_exclude_pattern("src/tests/foo.rs", "tests"));
assert!(matches_exclude_pattern("lib/vendor/bar.js", "vendor"));
assert!(!matches_exclude_pattern("src/main.rs", "tests"));
}
#[test]
fn test_include_glob_patterns() {
assert!(matches_include_pattern("foo.rs", "*.rs"));
assert!(matches_include_pattern("src/main.rs", "*.rs"));
assert!(!matches_include_pattern("foo.py", "*.rs"));
}
#[test]
fn test_include_glob_recursive() {
assert!(matches_include_pattern("src/foo/bar.rs", "src/**/*.rs"));
assert!(matches_include_pattern("src/main.rs", "src/**/*.rs"));
assert!(!matches_include_pattern("tests/foo.rs", "src/**/*.rs"));
}
#[test]
fn test_include_substring_match() {
assert!(matches_include_pattern("src/main.rs", "src"));
assert!(matches_include_pattern("lib/index.ts", "lib"));
assert!(!matches_include_pattern("tests/foo.rs", "src"));
}
#[test]
fn test_include_suffix_match() {
assert!(matches_include_pattern("foo.test.ts", ".test.ts"));
assert!(matches_include_pattern("bar.spec.js", ".spec.js"));
assert!(!matches_include_pattern("foo.ts", ".test.ts"));
}
#[derive(Debug, Clone)]
struct TestFile {
path: String,
}
#[test]
fn test_apply_exclude_patterns_empty() {
let mut files = vec![
TestFile { path: "src/main.rs".to_owned() },
TestFile { path: "node_modules/lib.js".to_owned() },
];
apply_exclude_patterns(&mut files, &[], |f| &f.path);
assert_eq!(files.len(), 2);
}
#[test]
fn test_apply_exclude_patterns_basic() {
let mut files = vec![
TestFile { path: "src/main.rs".to_owned() },
TestFile { path: "node_modules/lib.js".to_owned() },
TestFile { path: "dist/bundle.js".to_owned() },
];
let exclude = vec!["node_modules".to_owned(), "dist".to_owned()];
apply_exclude_patterns(&mut files, &exclude, |f| &f.path);
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "src/main.rs");
}
#[test]
fn test_apply_exclude_patterns_glob() {
let mut files = vec![
TestFile { path: "foo.js".to_owned() },
TestFile { path: "foo.min.js".to_owned() },
TestFile { path: "bar.js".to_owned() },
];
let exclude = vec!["*.min.js".to_owned()];
apply_exclude_patterns(&mut files, &exclude, |f| &f.path);
assert_eq!(files.len(), 2);
assert!(files.iter().all(|f| !f.path.contains(".min.")));
}
#[test]
fn test_apply_include_patterns_empty() {
let mut files = vec![
TestFile { path: "src/main.rs".to_owned() },
TestFile { path: "src/lib.py".to_owned() },
];
apply_include_patterns(&mut files, &[], |f| &f.path);
assert_eq!(files.len(), 2);
}
#[test]
fn test_apply_include_patterns_basic() {
let mut files = vec![
TestFile { path: "src/main.rs".to_owned() },
TestFile { path: "src/lib.py".to_owned() },
TestFile { path: "src/index.ts".to_owned() },
];
let include = vec!["*.rs".to_owned(), "*.ts".to_owned()];
apply_include_patterns(&mut files, &include, |f| &f.path);
assert_eq!(files.len(), 2);
assert!(files.iter().any(|f| f.path.ends_with(".rs")));
assert!(files.iter().any(|f| f.path.ends_with(".ts")));
}
#[test]
fn test_apply_include_patterns_substring() {
let mut files = vec![
TestFile { path: "src/main.rs".to_owned() },
TestFile { path: "tests/test.rs".to_owned() },
TestFile { path: "lib/index.ts".to_owned() },
];
let include = vec!["src".to_owned()];
apply_include_patterns(&mut files, &include, |f| &f.path);
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "src/main.rs");
}
#[test]
fn test_compile_patterns() {
let patterns = vec!["*.rs".to_owned(), "*.ts".to_owned(), "src/**/*.js".to_owned()];
let compiled = compile_patterns(&patterns);
assert_eq!(compiled.len(), 3);
}
#[test]
fn test_compile_patterns_invalid() {
let patterns = vec![
"*.rs".to_owned(),
"[invalid".to_owned(), "*.ts".to_owned(),
];
let compiled = compile_patterns(&patterns);
assert_eq!(compiled.len(), 2); }
#[test]
fn test_exclude_then_include() {
let mut files = vec![
TestFile { path: "src/main.rs".to_owned() },
TestFile { path: "src/lib.rs".to_owned() },
TestFile { path: "src/main.test.rs".to_owned() },
TestFile { path: "node_modules/lib.js".to_owned() },
];
let exclude = vec!["node_modules".to_owned(), "*.test.rs".to_owned()];
apply_exclude_patterns(&mut files, &exclude, |f| &f.path);
assert_eq!(files.len(), 2);
let include = vec!["*.rs".to_owned()];
apply_include_patterns(&mut files, &include, |f| &f.path);
assert_eq!(files.len(), 2);
assert!(files.iter().all(|f| f.path.ends_with(".rs")));
}
#[test]
fn test_pattern_cache() {
let pattern1 = compile_pattern("*.rs");
assert!(pattern1.is_some());
let pattern2 = compile_pattern("*.rs");
assert!(pattern2.is_some());
assert!(pattern1.unwrap().matches("foo.rs"));
assert!(pattern2.unwrap().matches("foo.rs"));
}
}