Skip to main content

lechange_core/patterns/
matcher.rs

1//! Pattern matching with parallel filtering
2
3use crate::error::Result;
4use crate::interner::StringInterner;
5use crate::types::ChangedFile;
6use globset::{Glob, GlobSet, GlobSetBuilder};
7use rayon::prelude::*;
8
9/// Pattern matcher with precompiled glob patterns
10pub struct PatternMatcher {
11    include_set: GlobSet,
12    exclude_set: GlobSet,
13    negation_first: bool,
14}
15
16impl PatternMatcher {
17    /// Create a new pattern matcher
18    pub fn new(includes: &[&str], excludes: &[&str], negation_first: bool) -> Result<Self> {
19        let mut include_builder = GlobSetBuilder::new();
20        for pattern in includes {
21            include_builder.add(Glob::new(pattern)?);
22        }
23
24        let mut exclude_builder = GlobSetBuilder::new();
25        for pattern in excludes {
26            exclude_builder.add(Glob::new(pattern)?);
27        }
28
29        Ok(Self {
30            include_set: include_builder.build()?,
31            exclude_set: exclude_builder.build()?,
32            negation_first,
33        })
34    }
35
36    /// Synchronous match for use with rayon - zero allocation
37    #[inline]
38    pub fn matches_sync(&self, path: &str) -> bool {
39        if self.negation_first {
40            if self.exclude_set.is_match(path) {
41                return false;
42            }
43            self.include_set.is_empty() || self.include_set.is_match(path)
44        } else {
45            if !self.include_set.is_empty() && !self.include_set.is_match(path) {
46                return false;
47            }
48            !self.exclude_set.is_match(path)
49        }
50    }
51
52    /// Index-based partitioning replacing clone-based filter
53    ///
54    /// Returns (matched_indices, unmatched_indices) - zero cloning, 4 bytes per file.
55    pub fn partition_files_parallel(
56        &self,
57        files: &[ChangedFile],
58        interner: &StringInterner,
59    ) -> (Vec<u32>, Vec<u32>) {
60        use rayon::iter::Either;
61
62        (0..files.len() as u32).into_par_iter().partition_map(|i| {
63            let file = &files[i as usize];
64            match interner.resolve(file.path) {
65                Some(path) if self.matches_sync(path) => Either::Left(i),
66                _ => Either::Right(i),
67            }
68        })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn test_basic_matching() {
78        let matcher = PatternMatcher::new(&["**/*.rs"], &[], false).unwrap();
79
80        assert!(matcher.matches_sync("src/main.rs"));
81        assert!(matcher.matches_sync("lib/mod.rs"));
82        assert!(!matcher.matches_sync("README.md"));
83    }
84
85    #[test]
86    fn test_exclusion() {
87        let matcher = PatternMatcher::new(&["**/*.rs"], &["**/test_*.rs"], false).unwrap();
88
89        assert!(matcher.matches_sync("src/main.rs"));
90        assert!(!matcher.matches_sync("src/test_utils.rs"));
91    }
92}