lattice_runtime/glob.rs
1//! Shared glob-set compilation (EF.1).
2//!
3//! One place to turn a list of string patterns into a compiled
4//! [`globset::GlobSet`]. Used by [`crate::EventFilter`]'s `path_glob`
5//! field (mode-activation triggers, mode-architecture.md ยง7.4) and
6//! the major-mode resolver (MA.2); the LSP file-watcher and
7//! workspace-exclude builders each carried their own
8//! parse-skip-build loop before this, so the directive (EF.1) was
9//! to consolidate rather than grow a third copy.
10//!
11//! Consolidating here means one graceful-degradation policy
12//! (CLAUDE.md "graceful error handling -- log + skip on recoverable
13//! failures, never panic"): an unparsable pattern is logged at
14//! `warn` and skipped; a failed final build falls back to an empty
15//! set (matches nothing) instead of panicking.
16
17use globset::{Glob, GlobSet, GlobSetBuilder};
18
19/// Compile `patterns` into a single [`GlobSet`] that matches a path
20/// against any of them in one pass.
21///
22/// Recoverable failures degrade rather than panic:
23/// - an individual unparsable pattern is logged at `warn` and
24/// skipped (the surviving patterns still compile);
25/// - if the final `GlobSet` build fails, an empty set is returned
26/// (matches nothing) and the failure is logged at `warn`.
27///
28/// An empty `patterns` iterator yields an empty set, which matches
29/// nothing -- callers that want "unconstrained" must not build a
30/// glob at all (see [`crate::EventFilter::path_glob`], where `None`
31/// is the unconstrained case).
32pub fn compile_glob_set<I, S>(patterns: I) -> GlobSet
33where
34 I: IntoIterator<Item = S>,
35 S: AsRef<str>,
36{
37 let mut builder = GlobSetBuilder::new();
38 for pat in patterns {
39 let pat = pat.as_ref();
40 match Glob::new(pat) {
41 Ok(g) => {
42 builder.add(g);
43 }
44 Err(e) => {
45 tracing::warn!(pattern = pat, error = %e, "skipping unparsable glob pattern");
46 }
47 }
48 }
49 builder.build().unwrap_or_else(|e| {
50 tracing::warn!(error = %e, "glob set build failed; falling back to empty set");
51 GlobSet::empty()
52 })
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn compiles_and_matches_listed_patterns() {
61 let set = compile_glob_set(["**/*.rs", "**/*.toml"]);
62 assert!(set.is_match("src/lib.rs"));
63 assert!(set.is_match("Cargo.toml"));
64 assert!(!set.is_match("README.md"));
65 }
66
67 #[test]
68 fn unparsable_pattern_is_skipped_survivors_compile() {
69 // `[invalid` is an unterminated character class. It must be
70 // skipped without taking down the valid patterns around it.
71 let set = compile_glob_set(["**/*.rs", "[invalid", "**/*.md"]);
72 assert!(set.is_match("src/lib.rs"));
73 assert!(set.is_match("docs/x.md"));
74 assert!(!set.is_match("Cargo.toml"));
75 }
76
77 #[test]
78 fn empty_input_matches_nothing() {
79 let set = compile_glob_set(Vec::<String>::new());
80 assert!(!set.is_match("anything.rs"));
81 }
82}