1use std::fmt;
45
46use regex::RegexSet;
47
48use crate::ModuleName;
49
50#[derive(Clone, Debug, get_size2::GetSize)]
55pub struct ModuleGlobSet {
56 #[get_size(ignore)]
57 regex_set: RegexSet,
58 globs: Box<[ModuleGlob]>,
60}
61
62impl ModuleGlobSet {
63 pub fn empty() -> Self {
64 Self {
65 regex_set: RegexSet::empty(),
66 globs: Box::default(),
67 }
68 }
69
70 pub fn from_patterns<I, S>(patterns: I) -> Result<Self, ModuleGlobError>
79 where
80 I: IntoIterator<Item = S>,
81 S: AsRef<str>,
82 {
83 let mut builder = ModuleGlobSetBuilder::new();
84 for pattern in patterns {
85 builder.add(pattern.as_ref())?;
86 }
87 builder.build()
88 }
89
90 pub fn matches(&self, module: &ModuleName) -> ModuleNameMatch {
97 if self.globs.is_empty() {
98 return ModuleNameMatch::None;
99 }
100
101 let Some(last_match_index) = self.regex_set.matches(module.as_str()).iter().next_back()
103 else {
104 return ModuleNameMatch::None;
105 };
106
107 if self.globs[last_match_index].negated {
108 ModuleNameMatch::Exclude
109 } else {
110 ModuleNameMatch::Include
111 }
112 }
113}
114
115impl PartialEq for ModuleGlobSet {
116 fn eq(&self, other: &Self) -> bool {
117 self.globs == other.globs
118 }
119}
120
121impl Eq for ModuleGlobSet {}
122
123impl fmt::Display for ModuleGlobSet {
124 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125 f.debug_list()
126 .entries(self.globs.iter().map(|g| &g.original))
127 .finish()
128 }
129}
130
131#[derive(Debug, Default)]
135pub struct ModuleGlobSetBuilder {
136 patterns: Vec<Box<str>>,
138 globs: Vec<ModuleGlob>,
140}
141
142impl ModuleGlobSetBuilder {
143 pub fn new() -> Self {
145 Self::default()
146 }
147
148 pub fn add(&mut self, pattern: &str) -> Result<&mut Self, ModuleGlobError> {
156 if pattern.is_empty() {
157 return Err(ModuleGlobError::EmptyPattern);
158 }
159
160 let (negated, pattern_without_negation) = if let Some(rest) = pattern.strip_prefix('!') {
162 (true, rest)
163 } else {
164 (false, pattern)
165 };
166
167 if pattern_without_negation.is_empty() {
168 return Err(ModuleGlobError::EmptyPattern);
169 }
170
171 let regex_pattern = glob_to_regex(pattern_without_negation)?;
172
173 self.patterns.push(regex_pattern);
174 self.globs.push(ModuleGlob {
175 original: pattern.into(),
176 negated,
177 });
178
179 Ok(self)
180 }
181
182 pub fn build(self) -> Result<ModuleGlobSet, ModuleGlobError> {
188 let regex_set = RegexSet::new(&self.patterns)?;
189
190 Ok(ModuleGlobSet {
191 regex_set,
192 globs: self.globs.into_boxed_slice(),
193 })
194 }
195}
196
197#[derive(Copy, Clone, Debug, PartialEq, Eq)]
199pub enum ModuleNameMatch {
200 None,
202
203 Include,
205
206 Exclude,
208}
209
210impl ModuleNameMatch {
211 pub const fn is_include(self) -> bool {
213 matches!(self, ModuleNameMatch::Include)
214 }
215
216 pub const fn is_exclude(self) -> bool {
218 matches!(self, ModuleNameMatch::Exclude)
219 }
220
221 pub const fn is_none(self) -> bool {
223 matches!(self, ModuleNameMatch::None)
224 }
225}
226
227#[derive(Debug, thiserror::Error)]
229pub enum ModuleGlobError {
230 #[error("module glob pattern cannot be empty")]
232 EmptyPattern,
233
234 #[error("module glob pattern cannot start with a dot")]
236 LeadingDot,
237
238 #[error("module glob pattern cannot end with a dot")]
240 TrailingDot,
241
242 #[error("module glob pattern cannot contain consecutive dots")]
244 ConsecutiveDots,
245
246 #[error(
248 "`**` can only appear as a complete component (e.g., `foo.**` or `**.bar`), not combined with other text like `{0}`"
249 )]
250 InvalidDoubleStarUsage(Box<str>),
251
252 #[error("failed to compile module glob pattern")]
254 Regex(#[from] regex::Error),
255}
256
257#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
259struct ModuleGlob {
260 original: Box<str>,
262 negated: bool,
264}
265
266fn glob_to_regex(pattern: &str) -> Result<Box<str>, ModuleGlobError> {
268 if pattern.is_empty() {
269 return Err(ModuleGlobError::EmptyPattern);
270 }
271
272 if pattern.starts_with('.') {
274 return Err(ModuleGlobError::LeadingDot);
275 }
276 if pattern.ends_with('.') {
277 return Err(ModuleGlobError::TrailingDot);
278 }
279
280 let mut regex = String::with_capacity(pattern.len());
281 regex.push('^');
282
283 let mut components = pattern.split('.').peekable();
284
285 let mut is_first = true;
286 let mut prev_was_double_star_at_start = false;
287
288 while let Some(component) = components.next() {
289 if component.is_empty() {
290 return Err(ModuleGlobError::ConsecutiveDots);
291 }
292
293 if component.contains("**") && component != "**" {
295 return Err(ModuleGlobError::InvalidDoubleStarUsage(Box::from(
296 component,
297 )));
298 }
299
300 let is_last = components.peek().is_none();
301
302 if component == "**" {
303 if is_first {
304 if is_last {
306 regex.push_str(".*");
307 } else {
308 regex.push_str("(?:[^.]+\\.)*");
312 prev_was_double_star_at_start = true;
313 }
314 } else {
315 regex.push_str("(?:\\.[^.]+)*");
318 }
319 } else {
320 if !is_first && !prev_was_double_star_at_start {
323 regex.push_str("\\.");
324 }
325 prev_was_double_star_at_start = false;
326
327 if component == "*" {
329 regex.push_str("[^.]+");
331 } else {
332 for c in component.chars() {
334 if c == '*' {
335 regex.push_str("[^.]*");
337 } else if regex_syntax::is_meta_character(c) {
338 regex.push('\\');
340 regex.push(c);
341 } else {
342 regex.push(c);
343 }
344 }
345 }
346 }
347
348 is_first = false;
349 }
350
351 regex.push('$');
352 Ok(regex.into_boxed_str())
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[track_caller]
360 fn assert_include(set: &ModuleGlobSet, name: &str) {
361 let module = ModuleName::new(name).unwrap();
362 assert_eq!(
363 set.matches(&module),
364 ModuleNameMatch::Include,
365 "expected `{name}` to be included"
366 );
367 }
368
369 #[track_caller]
370 fn assert_excludes(set: &ModuleGlobSet, name: &str) {
371 let module = ModuleName::new(name).unwrap();
372 assert_eq!(
373 set.matches(&module),
374 ModuleNameMatch::Exclude,
375 "expected `{name}` to be excluded"
376 );
377 }
378
379 #[track_caller]
380 fn assert_no_match(set: &ModuleGlobSet, name: &str) {
381 let module = ModuleName::new(name).unwrap();
382 assert_eq!(
383 set.matches(&module),
384 ModuleNameMatch::None,
385 "expected `{name}` not to match"
386 );
387 }
388
389 #[test]
390 fn test_exact_match() {
391 let set = ModuleGlobSet::from_patterns(["test"]).unwrap();
392
393 assert_include(&set, "test");
394 assert_no_match(&set, "test2");
395 assert_no_match(&set, "test_foo");
396 assert_no_match(&set, "foo");
397 assert_no_match(&set, "test.foo");
398 }
399
400 #[test]
401 fn test_single_star_direct_submodule() {
402 let set = ModuleGlobSet::from_patterns(["test.*"]).unwrap();
403
404 assert_include(&set, "test.foo");
405 assert_include(&set, "test.bar");
406 assert_no_match(&set, "test");
407 assert_no_match(&set, "test.foo.bar");
408 }
409
410 #[test]
411 fn test_single_star_prefix() {
412 let set = ModuleGlobSet::from_patterns(["*.test"]).unwrap();
413
414 assert_include(&set, "foo.test");
415 assert_include(&set, "bar.test");
416 assert_no_match(&set, "test");
417 assert_no_match(&set, "foo.bar.test");
418 }
419
420 #[test]
421 fn test_single_star_middle() {
422 let set = ModuleGlobSet::from_patterns(["foo.*.bar"]).unwrap();
423
424 assert_include(&set, "foo.x.bar");
425 assert_include(&set, "foo.y.bar");
426 assert_no_match(&set, "foo.bar");
427 assert_no_match(&set, "foo.x.y.bar");
428 }
429
430 #[test]
431 fn test_star_with_literal_text() {
432 let set = ModuleGlobSet::from_patterns(["*test.bar"]).unwrap();
433
434 assert_include(&set, "test.bar");
435 assert_include(&set, "mytest.bar");
436 assert_no_match(&set, "foobar.bar");
437 }
438
439 #[test]
440 fn test_double_star_end() {
441 let set = ModuleGlobSet::from_patterns(["test.**"]).unwrap();
442
443 assert_include(&set, "test");
444 assert_include(&set, "test.foo");
445 assert_include(&set, "test.foo.bar");
446 assert_include(&set, "test.foo.bar.baz");
447 assert_no_match(&set, "testing");
448 }
449
450 #[test]
451 fn test_double_star_start() {
452 let set = ModuleGlobSet::from_patterns(["**.bar"]).unwrap();
453
454 assert_include(&set, "bar");
455 assert_include(&set, "foo.bar");
456 assert_include(&set, "foo.baz.bar");
457 assert_include(&set, "foo.baz.qux.bar");
458 assert_no_match(&set, "bar.foo");
459 }
460
461 #[test]
462 fn test_double_star_middle() {
463 let set = ModuleGlobSet::from_patterns(["test.**.bar"]).unwrap();
464
465 assert_include(&set, "test.bar");
466 assert_include(&set, "test.foo.bar");
467 assert_include(&set, "test.foo.baz.bar");
468 assert_include(&set, "test.foo.baz.qux.bar");
469 assert_no_match(&set, "test");
470 assert_no_match(&set, "test.bar.foo");
471 }
472
473 #[test]
474 fn test_just_double_star() {
475 let set = ModuleGlobSet::from_patterns(["**"]).unwrap();
476
477 assert_include(&set, "foo");
478 assert_include(&set, "foo.bar");
479 assert_include(&set, "foo.bar.baz");
480 }
481
482 #[test]
483 fn test_negated_pattern() {
484 let set = ModuleGlobSet::from_patterns(["test.*", "!test.internal"]).unwrap();
485
486 assert_include(&set, "test.foo");
487 assert_include(&set, "test.bar");
488 assert_excludes(&set, "test.internal");
490 }
491
492 #[test]
493 fn test_negated_pattern_override() {
494 let set = ModuleGlobSet::from_patterns(["!test.internal", "test.*"]).unwrap();
496
497 assert_include(&set, "test.foo");
498 assert_include(&set, "test.bar");
499 assert_include(&set, "test.internal");
501 }
502
503 #[test]
504 fn test_negated_only() {
505 let set = ModuleGlobSet::from_patterns(["!test"]).unwrap();
506
507 assert_excludes(&set, "test");
508 assert_no_match(&set, "other");
509 }
510
511 #[test]
512 fn test_empty_set() {
513 let set = ModuleGlobSet::from_patterns::<[&str; 0], _>([]).unwrap();
514
515 assert_no_match(&set, "test");
516 }
517
518 #[test]
519 fn test_display() {
520 let set = ModuleGlobSet::from_patterns(["test.*", "!test.internal"]).unwrap();
521
522 let display = format!("{set}");
523 assert!(display.contains("test.*"));
524 assert!(display.contains("!test.internal"));
525 }
526
527 #[test]
528 fn test_invalid_empty_pattern() {
529 let result = ModuleGlobSet::from_patterns([""]);
530 assert!(matches!(result, Err(ModuleGlobError::EmptyPattern)));
531 }
532
533 #[test]
534 fn test_invalid_just_negation() {
535 let result = ModuleGlobSet::from_patterns(["!"]);
536 assert!(matches!(result, Err(ModuleGlobError::EmptyPattern)));
537 }
538
539 #[test]
540 fn test_invalid_double_star_combined() {
541 let result = ModuleGlobSet::from_patterns(["foo**"]);
542 assert!(matches!(
543 result,
544 Err(ModuleGlobError::InvalidDoubleStarUsage(_))
545 ));
546
547 let result = ModuleGlobSet::from_patterns(["**foo"]);
548 assert!(matches!(
549 result,
550 Err(ModuleGlobError::InvalidDoubleStarUsage(_))
551 ));
552
553 let result = ModuleGlobSet::from_patterns(["foo.bar**"]);
554 assert!(matches!(
555 result,
556 Err(ModuleGlobError::InvalidDoubleStarUsage(_))
557 ));
558 }
559
560 #[test]
561 fn test_invalid_consecutive_dots() {
562 let result = ModuleGlobSet::from_patterns(["foo..bar"]);
563 assert!(matches!(result, Err(ModuleGlobError::ConsecutiveDots)));
564 }
565
566 #[test]
567 fn test_invalid_leading_dot() {
568 let result = ModuleGlobSet::from_patterns([".foo"]);
569 assert!(matches!(result, Err(ModuleGlobError::LeadingDot)));
570 }
571
572 #[test]
573 fn test_invalid_trailing_dot() {
574 let result = ModuleGlobSet::from_patterns(["foo."]);
575 assert!(matches!(result, Err(ModuleGlobError::TrailingDot)));
576 }
577
578 #[test]
579 fn test_underscore_in_module_name() {
580 let set = ModuleGlobSet::from_patterns(["foo_bar.*"]).unwrap();
581
582 assert_include(&set, "foo_bar.baz");
583 }
584
585 #[test]
586 fn test_numbers_in_module_name() {
587 let set = ModuleGlobSet::from_patterns(["foo123.*"]).unwrap();
588
589 assert_include(&set, "foo123.bar");
590 }
591
592 #[test]
593 fn test_multiple_patterns() {
594 let set = ModuleGlobSet::from_patterns(["alpha.*", "beta.*", "gamma"]).unwrap();
595
596 assert_include(&set, "alpha.one");
597 assert_include(&set, "beta.two");
598 assert_include(&set, "gamma");
599 assert_no_match(&set, "delta");
600 }
601}