Skip to main content

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}