Skip to content

Commit 75ee4ee

Browse files
committed
fix: --exclude patterns with ** now work for recursive directory matching
glob::Pattern does not support ** for multi-segment matching. Added matches_exclude() that handles dir/**, **/suffix, and general ** patterns via path-contains checks, falling back to glob::Pattern for simple globs. Reduces self-scan SARIF from 11,888 to 35 results when excluding external/ and rules/ directories.
1 parent d8459d3 commit 75ee4ee

File tree

1 file changed

+44
-10
lines changed

1 file changed

+44
-10
lines changed

crates/parser/src/walker.rs

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,10 @@ pub fn collect_files(root: &Path, config: &RmaConfig) -> Result<Vec<PathBuf>> {
4949

5050
// Check exclude patterns
5151
let path_str = path.to_string_lossy();
52-
let excluded = config.exclude_patterns.iter().any(|pattern| {
53-
glob::Pattern::new(pattern)
54-
.map(|p| p.matches(&path_str))
55-
.unwrap_or(false)
56-
});
52+
let excluded = config
53+
.exclude_patterns
54+
.iter()
55+
.any(|pattern| matches_exclude(pattern, &path_str));
5756

5857
if excluded {
5958
debug!("Excluded by pattern: {}", path.display());
@@ -72,11 +71,46 @@ pub fn collect_files(root: &Path, config: &RmaConfig) -> Result<Vec<PathBuf>> {
7271
/// Check if a path should be excluded based on patterns
7372
pub fn is_excluded(path: &Path, patterns: &[String]) -> bool {
7473
let path_str = path.to_string_lossy();
75-
patterns.iter().any(|pattern| {
76-
glob::Pattern::new(pattern)
77-
.map(|p| p.matches(&path_str))
78-
.unwrap_or(false)
79-
})
74+
patterns
75+
.iter()
76+
.any(|pattern| matches_exclude(pattern, &path_str))
77+
}
78+
79+
/// Match an exclude pattern against a path string.
80+
///
81+
/// Supports `**` for recursive directory matching (which `glob::Pattern` does not).
82+
/// Patterns like `foo/**` become a prefix check on `foo/`.
83+
/// Patterns like `**/foo` become a suffix/contains check on `/foo`.
84+
fn matches_exclude(pattern: &str, path: &str) -> bool {
85+
if pattern.contains("**") {
86+
// "dir/**" → anything under dir/
87+
if let Some(prefix) = pattern.strip_suffix("/**") {
88+
return path.contains(&format!("{prefix}/"));
89+
}
90+
// "**/suffix" → anything ending with /suffix or matching suffix
91+
if let Some(suffix) = pattern.strip_prefix("**/") {
92+
return path.ends_with(suffix) || path.contains(&format!("/{suffix}"));
93+
}
94+
// General ** — split and check segments
95+
let parts: Vec<&str> = pattern.split("**").collect();
96+
if parts.len() == 2 {
97+
return path.contains(parts[0]) && path.contains(parts[1]);
98+
}
99+
}
100+
101+
// Fall back to glob::Pattern for simple patterns
102+
glob::Pattern::new(pattern)
103+
.map(|p| {
104+
p.matches_with(
105+
path,
106+
glob::MatchOptions {
107+
case_sensitive: true,
108+
require_literal_separator: false,
109+
require_literal_leading_dot: false,
110+
},
111+
)
112+
})
113+
.unwrap_or(false)
80114
}
81115

82116
/// Get language stats from a list of files

0 commit comments

Comments
 (0)