Skip to content

Commit 1619421

Browse files
committed
perf(core): optimise pattern matching
1 parent 1b43030 commit 1619421

File tree

4 files changed

+222
-26
lines changed

4 files changed

+222
-26
lines changed

src/group_selector.rs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
use {
2-
crate::{dependency_type::DependencyType, instance::InstanceDescriptor, packages::Packages},
3-
globset::{Glob, GlobMatcher},
2+
crate::{
3+
dependency_type::DependencyType, instance::InstanceDescriptor, packages::Packages,
4+
pattern_matcher::PatternMatcher,
5+
},
46
log::error,
57
std::process,
68
};
79

810
#[derive(Clone, Debug)]
911
pub struct GroupSelector {
10-
/// Glob patterns to match against the installed dependency name.
12+
/// Patterns to match against the installed dependency name.
1113
///
1214
/// The keyword "$LOCAL" can also be used to match every locally-developed
1315
/// package used as a dependency.
14-
pub include_dependencies: Vec<GlobMatcher>,
15-
pub exclude_dependencies: Vec<GlobMatcher>,
16+
pub include_dependencies: Vec<PatternMatcher>,
17+
pub exclude_dependencies: Vec<PatternMatcher>,
1618
/// Named locations where dependencies should be found.
1719
///
1820
/// Possible values:
@@ -27,10 +29,9 @@ pub struct GroupSelector {
2729
pub exclude_dependency_types: Vec<String>,
2830
/// Optional label to describe the group.
2931
pub label: String,
30-
/// Glob patterns to match against the package name the dependency is located
31-
/// in.
32-
pub include_packages: Vec<GlobMatcher>,
33-
pub exclude_packages: Vec<GlobMatcher>,
32+
/// Patterns to match against the package name the dependency is located in.
33+
pub include_packages: Vec<PatternMatcher>,
34+
pub exclude_packages: Vec<PatternMatcher>,
3435
/// Types of version specifier the installed dependency should have.
3536
///
3637
/// Possible values:
@@ -66,12 +67,12 @@ impl GroupSelector {
6667
) -> GroupSelector {
6768
let dependencies = with_resolved_keywords(&dependencies, all_packages);
6869

69-
let include_dependencies = create_globs(true, &dependencies);
70-
let exclude_dependencies = create_globs(false, &dependencies);
70+
let include_dependencies = create_patterns(true, &dependencies);
71+
let exclude_dependencies = create_patterns(false, &dependencies);
7172
let include_dependency_types = create_identifiers(true, &dependency_types);
7273
let exclude_dependency_types = create_identifiers(false, &dependency_types);
73-
let include_packages = create_globs(true, &packages);
74-
let exclude_packages = create_globs(false, &packages);
74+
let include_packages = create_patterns(true, &packages);
75+
let exclude_packages = create_patterns(false, &packages);
7576
let include_specifier_types = create_identifiers(true, &specifier_types);
7677
let exclude_specifier_types = create_identifiers(false, &specifier_types);
7778

@@ -115,12 +116,12 @@ impl GroupSelector {
115116
return false;
116117
}
117118

118-
// 3. Dependencies (glob matching, more expensive)
119+
// 3. Dependencies (pattern matching, optimized for common cases)
119120
if self.has_dependency_filters && !self.matches_dependencies(descriptor) {
120121
return false;
121122
}
122123

123-
// 4. Packages (glob matching + borrow, most expensive)
124+
// 4. Packages (pattern matching + borrow, most expensive)
124125
if self.has_package_filters && !self.matches_packages(descriptor) {
125126
return false;
126127
}
@@ -141,12 +142,12 @@ impl GroupSelector {
141142
fn matches_packages(&self, descriptor: &InstanceDescriptor) -> bool {
142143
// Cache the borrow result to avoid repeated borrow checks
143144
let package_name = &descriptor.package.borrow().name;
144-
matches_globs(package_name, &self.include_packages, &self.exclude_packages)
145+
matches_patterns(package_name, &self.include_packages, &self.exclude_packages)
145146
}
146147

147148
#[inline]
148149
fn matches_dependencies(&self, descriptor: &InstanceDescriptor) -> bool {
149-
matches_globs(&descriptor.internal_name, &self.include_dependencies, &self.exclude_dependencies)
150+
matches_patterns(&descriptor.internal_name, &self.include_dependencies, &self.exclude_dependencies)
150151
}
151152

152153
#[inline]
@@ -159,26 +160,25 @@ impl GroupSelector {
159160
}
160161
}
161162

162-
fn create_globs(is_include: bool, patterns: &[String]) -> Vec<GlobMatcher> {
163+
fn create_patterns(is_include: bool, patterns: &[String]) -> Vec<PatternMatcher> {
163164
patterns
164165
.iter()
165166
.filter(|pattern| *pattern != "**" && pattern.starts_with('!') != is_include)
166167
.map(|pattern| {
167-
Glob::new(&pattern.replace('!', ""))
168-
.expect("invalid glob pattern")
169-
.compile_matcher()
168+
let pattern = pattern.replace('!', "");
169+
PatternMatcher::from_pattern(&pattern)
170170
})
171171
.collect()
172172
}
173173

174-
fn matches_globs(value: &str, includes: &[GlobMatcher], excludes: &[GlobMatcher]) -> bool {
175-
let is_included = includes.is_empty() || matches_any_glob(value, includes);
176-
let is_excluded = !excludes.is_empty() && matches_any_glob(value, excludes);
174+
fn matches_patterns(value: &str, includes: &[PatternMatcher], excludes: &[PatternMatcher]) -> bool {
175+
let is_included = includes.is_empty() || matches_any_pattern(value, includes);
176+
let is_excluded = !excludes.is_empty() && matches_any_pattern(value, excludes);
177177
is_included && !is_excluded
178178
}
179179

180-
fn matches_any_glob(value: &str, globs: &[GlobMatcher]) -> bool {
181-
globs.iter().any(|glob| glob.is_match(value))
180+
fn matches_any_pattern(value: &str, patterns: &[PatternMatcher]) -> bool {
181+
patterns.iter().any(|pattern| pattern.is_match(value))
182182
}
183183

184184
fn create_identifiers(is_include: bool, patterns: &[String]) -> Vec<String> {

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ mod package_json;
3232
mod packages;
3333
#[cfg(test)]
3434
mod packages_test;
35+
mod pattern_matcher;
3536
mod rcfile;
3637
#[cfg(test)]
3738
mod rcfile_test;

src/pattern_matcher.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! Fast pattern matching for dependency and package names.
2+
//!
3+
//! Optimizes common glob patterns into faster string operations:
4+
//! - "react" → exact match (==)
5+
//! - "@aws-sdk/**" → prefix match (starts_with)
6+
//! - "**-loader" → suffix match (ends_with)
7+
//! - Complex patterns → full glob matching (fallback)
8+
9+
use {globset::{Glob, GlobMatcher}, std::fmt};
10+
11+
/// Pattern matcher optimized for common npm package name patterns.
12+
#[derive(Clone)]
13+
pub enum PatternMatcher {
14+
/// Exact string match: "react" → value == "react"
15+
Exact(String),
16+
17+
/// Prefix match: "@aws-sdk/**" → value.starts_with("@aws-sdk/")
18+
Prefix(String),
19+
20+
/// Suffix match: "**-loader" → value.ends_with("-loader")
21+
Suffix(String),
22+
23+
/// Full glob matching for complex patterns
24+
Glob(GlobMatcher),
25+
}
26+
27+
impl PatternMatcher {
28+
/// Create a pattern matcher from a glob pattern string.
29+
///
30+
/// Examples:
31+
/// - "react" → Exact("react")
32+
/// - "@aws-sdk/**" → Prefix("@aws-sdk/")
33+
/// - "**-loader" → Suffix("-loader")
34+
/// - "**/test/**" → Glob(...)
35+
pub fn from_pattern(pattern: &str) -> Self {
36+
// Exact match (no wildcards)
37+
if !pattern.contains('*') && !pattern.contains('?') && !pattern.contains('[') {
38+
return Self::Exact(pattern.to_string());
39+
}
40+
41+
// Prefix: "@aws-sdk/**", "foo/**"
42+
// Must end with /** and have no wildcards before that
43+
if let Some(prefix) = pattern.strip_suffix("/**") {
44+
if !prefix.contains('*') && !prefix.contains('?') && !prefix.contains('[') {
45+
return Self::Prefix(format!("{prefix}/"));
46+
}
47+
}
48+
49+
// Suffix: "**-loader", "**/test"
50+
// Must start with **/ or ** and have no wildcards after
51+
if let Some(suffix) = pattern.strip_prefix("**/") {
52+
if !suffix.contains('*') && !suffix.contains('?') && !suffix.contains('[') {
53+
return Self::Suffix(suffix.to_string());
54+
}
55+
}
56+
if let Some(suffix) = pattern.strip_prefix("**") {
57+
if !suffix.is_empty() && !suffix.contains('*') && !suffix.contains('?') && !suffix.contains('[') {
58+
return Self::Suffix(suffix.to_string());
59+
}
60+
}
61+
62+
// Complex glob fallback
63+
Self::Glob(Glob::new(pattern).expect("invalid glob pattern").compile_matcher())
64+
}
65+
66+
/// Check if a value matches this pattern.
67+
#[inline]
68+
pub fn is_match(&self, value: &str) -> bool {
69+
match self {
70+
Self::Exact(s) => value == s,
71+
Self::Prefix(p) => value.starts_with(p),
72+
Self::Suffix(s) => value.ends_with(s),
73+
Self::Glob(g) => g.is_match(value),
74+
}
75+
}
76+
}
77+
78+
impl fmt::Debug for PatternMatcher {
79+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80+
match self {
81+
Self::Exact(s) => write!(f, "Exact({s:?})"),
82+
Self::Prefix(p) => write!(f, "Prefix({p:?})"),
83+
Self::Suffix(s) => write!(f, "Suffix({s:?})"),
84+
Self::Glob(_) => write!(f, "Glob(...)"),
85+
}
86+
}
87+
}
88+
89+
#[cfg(test)]
90+
#[path = "pattern_matcher_test.rs"]
91+
mod pattern_matcher_test;

src/pattern_matcher_test.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use crate::pattern_matcher::PatternMatcher;
2+
3+
#[test]
4+
fn from_pattern_exact() {
5+
let matcher = PatternMatcher::from_pattern("react");
6+
assert!(matches!(matcher, PatternMatcher::Exact(_)));
7+
assert!(matcher.is_match("react"));
8+
assert!(!matcher.is_match("react-dom"));
9+
assert!(!matcher.is_match("preact"));
10+
}
11+
12+
#[test]
13+
fn from_pattern_prefix_with_slash_star_star() {
14+
let matcher = PatternMatcher::from_pattern("@aws-sdk/**");
15+
assert!(matches!(matcher, PatternMatcher::Prefix(_)));
16+
assert!(matcher.is_match("@aws-sdk/client-s3"));
17+
assert!(matcher.is_match("@aws-sdk/client-dynamodb"));
18+
assert!(!matcher.is_match("@aws-sdk"));
19+
assert!(!matcher.is_match("@aws"));
20+
assert!(!matcher.is_match("aws-sdk"));
21+
}
22+
23+
#[test]
24+
fn from_pattern_prefix_scoped_package() {
25+
let matcher = PatternMatcher::from_pattern("@types/**");
26+
assert!(matches!(matcher, PatternMatcher::Prefix(_)));
27+
assert!(matcher.is_match("@types/node"));
28+
assert!(matcher.is_match("@types/react"));
29+
assert!(!matcher.is_match("@types"));
30+
assert!(!matcher.is_match("types/node"));
31+
}
32+
33+
#[test]
34+
fn from_pattern_suffix_with_star_star_slash() {
35+
let matcher = PatternMatcher::from_pattern("**-loader");
36+
assert!(matches!(matcher, PatternMatcher::Suffix(_)));
37+
assert!(matcher.is_match("css-loader"));
38+
assert!(matcher.is_match("style-loader"));
39+
assert!(matcher.is_match("webpack-dev-loader"));
40+
assert!(!matcher.is_match("loader"));
41+
assert!(!matcher.is_match("css"));
42+
}
43+
44+
#[test]
45+
fn from_pattern_suffix_with_star_star() {
46+
let matcher = PatternMatcher::from_pattern("**-test");
47+
assert!(matches!(matcher, PatternMatcher::Suffix(_)));
48+
assert!(matcher.is_match("my-test"));
49+
assert!(matcher.is_match("another-test"));
50+
assert!(!matcher.is_match("test"));
51+
}
52+
53+
#[test]
54+
fn from_pattern_glob_complex() {
55+
let matcher = PatternMatcher::from_pattern("**/test/**");
56+
assert!(matches!(matcher, PatternMatcher::Glob(_)));
57+
assert!(matcher.is_match("foo/test/bar"));
58+
assert!(matcher.is_match("test/bar"));
59+
assert!(!matcher.is_match("test"));
60+
}
61+
62+
#[test]
63+
fn from_pattern_glob_with_question_mark() {
64+
let matcher = PatternMatcher::from_pattern("react?");
65+
assert!(matches!(matcher, PatternMatcher::Glob(_)));
66+
assert!(matcher.is_match("reacta"));
67+
assert!(matcher.is_match("react1"));
68+
assert!(!matcher.is_match("react"));
69+
assert!(!matcher.is_match("reactab"));
70+
}
71+
72+
#[test]
73+
fn from_pattern_glob_with_brackets() {
74+
let matcher = PatternMatcher::from_pattern("react[0-9]");
75+
assert!(matches!(matcher, PatternMatcher::Glob(_)));
76+
assert!(matcher.is_match("react1"));
77+
assert!(matcher.is_match("react9"));
78+
assert!(!matcher.is_match("react"));
79+
assert!(!matcher.is_match("reacta"));
80+
}
81+
82+
#[test]
83+
fn from_pattern_prefix_with_wildcard_in_prefix_falls_back_to_glob() {
84+
let matcher = PatternMatcher::from_pattern("@*/client/**");
85+
assert!(matches!(matcher, PatternMatcher::Glob(_)));
86+
}
87+
88+
#[test]
89+
fn from_pattern_suffix_with_wildcard_in_suffix_falls_back_to_glob() {
90+
let matcher = PatternMatcher::from_pattern("**/*-loader");
91+
assert!(matches!(matcher, PatternMatcher::Glob(_)));
92+
}
93+
94+
#[test]
95+
fn is_match_handles_empty_strings() {
96+
let exact = PatternMatcher::from_pattern("react");
97+
assert!(!exact.is_match(""));
98+
99+
let prefix = PatternMatcher::from_pattern("@aws/**");
100+
assert!(!prefix.is_match(""));
101+
102+
let suffix = PatternMatcher::from_pattern("**-loader");
103+
assert!(!suffix.is_match(""));
104+
}

0 commit comments

Comments
 (0)