use std::path::Path;
use crate::glob::glob_match;
use crate::glob_path::GlobPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterResult {
Include,
Exclude,
NoMatch,
}
#[derive(Debug, Clone, Default)]
pub struct IncludeExclude {
rules: Vec<(FilterAction, CompiledRule)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FilterAction {
Include,
Exclude,
}
#[derive(Debug, Clone)]
struct CompiledRule {
glob: Option<GlobPath>,
raw: String,
}
impl CompiledRule {
fn new(pattern: &str) -> Self {
let glob = GlobPath::new(pattern).ok();
Self {
glob,
raw: pattern.to_string(),
}
}
fn matches(&self, path: &Path) -> bool {
if let Some(ref glob) = self.glob {
return glob.matches(path);
}
let path_str = path.to_string_lossy();
if let Some(name) = path.file_name() {
let name_str = name.to_string_lossy();
if glob_match(&self.raw, &name_str) {
return true;
}
}
glob_match(&self.raw, &path_str)
}
}
impl IncludeExclude {
pub fn new() -> Self {
Self::default()
}
pub fn include(&mut self, pattern: &str) {
self.rules
.push((FilterAction::Include, CompiledRule::new(pattern)));
}
pub fn exclude(&mut self, pattern: &str) {
self.rules
.push((FilterAction::Exclude, CompiledRule::new(pattern)));
}
pub fn check(&self, path: &Path) -> FilterResult {
for (action, rule) in &self.rules {
if rule.matches(path) {
return match action {
FilterAction::Include => FilterResult::Include,
FilterAction::Exclude => FilterResult::Exclude,
};
}
}
FilterResult::NoMatch
}
pub fn excludes_entry(&self, relative: &Path, name: Option<&Path>, is_dir: bool) -> bool {
for path in [Some(relative), name].into_iter().flatten() {
match self.check(path) {
FilterResult::Exclude => return true,
FilterResult::Include => return false,
FilterResult::NoMatch => {}
}
}
!is_dir && self.has_includes()
}
pub fn has_includes(&self) -> bool {
self.rules
.iter()
.any(|(action, _)| *action == FilterAction::Include)
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn len(&self) -> usize {
self.rules.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_filter() {
let filter = IncludeExclude::new();
assert_eq!(filter.check(Path::new("any.txt")), FilterResult::NoMatch);
assert!(!filter.excludes_entry(Path::new("any.txt"), None, false));
}
#[test]
fn test_include() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.txt")), FilterResult::NoMatch);
}
#[test]
fn test_exclude() {
let mut filter = IncludeExclude::new();
filter.exclude("*.log");
assert_eq!(filter.check(Path::new("app.log")), FilterResult::Exclude);
assert!(filter.excludes_entry(Path::new("app.log"), None, false));
assert!(!filter.excludes_entry(Path::new("app.txt"), None, false));
}
#[test]
fn test_order_matters() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
filter.exclude("*_test.rs");
assert_eq!(
filter.check(Path::new("parser_test.rs")),
FilterResult::Include
);
let mut filter = IncludeExclude::new();
filter.exclude("*_test.rs");
filter.include("*.rs");
assert_eq!(
filter.check(Path::new("parser_test.rs")),
FilterResult::Exclude
);
}
#[test]
fn test_globstar_patterns() {
let mut filter = IncludeExclude::new();
filter.include("**/*.rs");
filter.exclude("**/test/**");
assert_eq!(filter.check(Path::new("src/main.rs")), FilterResult::Include);
assert_eq!(
filter.check(Path::new("src/lib/utils.rs")),
FilterResult::Include
);
}
#[test]
fn test_path_patterns() {
let mut filter = IncludeExclude::new();
filter.exclude("logs/*");
assert!(filter.excludes_entry(Path::new("logs/app.log"), None, false));
assert!(!filter.excludes_entry(Path::new("other/app.log"), None, false));
}
#[test]
fn test_multiple_patterns() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
filter.include("*.go");
filter.include("*.py");
filter.exclude("*_test.*");
assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
assert_eq!(filter.check(Path::new("server.go")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main_test.rs")), FilterResult::Include); }
#[test]
fn test_brace_expansion() {
let mut filter = IncludeExclude::new();
filter.include("*.{rs,go,py}");
assert_eq!(filter.check(Path::new("main.rs")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.go")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.py")), FilterResult::Include);
assert_eq!(filter.check(Path::new("main.js")), FilterResult::NoMatch);
}
#[test]
fn include_list_excludes_nonmatching_files_but_not_dirs() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
assert!(filter.excludes_entry(Path::new("notes.txt"), None, false));
assert!(!filter.excludes_entry(Path::new("main.rs"), None, false));
assert!(!filter.excludes_entry(
Path::new("src/lib.rs"),
Some(Path::new("lib.rs")),
false
));
assert!(!filter.excludes_entry(Path::new("src"), None, true));
}
#[test]
fn exclude_rules_still_prune_directories() {
let mut filter = IncludeExclude::new();
filter.include("*.rs");
filter.exclude("target");
assert!(filter.excludes_entry(Path::new("target"), None, true));
}
}