From ba97230b9854da12cb6b861d9a7823c667e51d21 Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 13:34:33 +0100 Subject: [PATCH 1/8] feat: Map BUILD to Python (Starlark) for Bazel (fixes #3575) See #3575. --- src/syntax_mapping/builtins/common/50-bazel.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 src/syntax_mapping/builtins/common/50-bazel.toml diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml new file mode 100644 index 00000000..5f44ae2d --- /dev/null +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -0,0 +1,2 @@ +[mappings] +"Python" = ["BUILD"] From 5a4a7de9338be2b888674259ee29db3ff6b9b6fe Mon Sep 17 00:00:00 2001 From: Michael Vorburger Date: Sun, 1 Feb 2026 13:40:51 +0100 Subject: [PATCH 2/8] docs: Document mapping BUILD to Python (Starlark) for Bazel in CHANGELOG.md (see #3576) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9934329f..bfc516c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ - Add `--quiet-empty` (`-E`) flag to suppress output when input is empty. Closes #1936, see #3563 (@NORMAL-EX) - Improve native man pages and command help syntax highlighting by stripping overstriking, see #3517 (@akirk) +- Map BUILD to Python (Starlark) for Bazel, see #3576 (@vorburger) ## Bugfixes + - `--help` now correctly honors `--pager=builtin`. See #3516 (@keith-hall) - `--help` now correctly honors custom themes. See #3524 (@keith-hall) - Fixed test compatibility with future Cargo build directory changes, see #3550 (@nmacl) From 56fe0fa2260675c2603607ac35c4c367e5d8d797 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Sat, 14 Mar 2026 09:18:56 +0200 Subject: [PATCH 3/8] Add case-sensitive glob support to syntax mapping to allow us to map `BUILD` case sensitively to Python for Skylark --- build/syntax_mapping.rs | 40 +++++++++++++-- src/syntax_mapping.rs | 50 +++++++++++++++++-- src/syntax_mapping/builtin.rs | 9 ++-- src/syntax_mapping/builtins/README.md | 6 +-- .../builtins/common/50-bazel.toml | 2 +- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 64be4bb9..f17b1cf5 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -51,7 +51,13 @@ impl ToTokens for MappingTarget { /// A single matcher. /// /// Codegen converts this into a `Lazy>`. -struct Matcher(Vec); +struct Matcher { + segments: Vec, + /// Whether the glob pattern should be matched case-insensitively. + /// + /// Defaults to `true` (case-insensitive) for backwards compatibility. + case_insensitive: bool, +} /// Parse a matcher. /// /// Note that this implementation is rather strict: it will greedily interpret @@ -116,18 +122,24 @@ impl FromStr for Matcher { bail!(r#"Invalid matcher: "{s}""#); } - Ok(Self(non_empty_segments)) + Ok(Self { + segments: non_empty_segments, + case_insensitive: true, + }) } } impl ToTokens for Matcher { fn to_tokens(&self, tokens: &mut TokenStream) { - let t = match self.0.as_slice() { + let case_insensitive = self.case_insensitive; + let t = match self.segments.as_slice() { [] => unreachable!("0-length matcher should never be created"), [MatcherSegment::Text(text)] => { - quote! { Lazy::new(|| Some(build_matcher_fixed(#text))) } + quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case_insensitive))) } } // parser logic ensures that this case can only happen when there are dynamic segments - segs @ [_, ..] => quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ])) }, + segs @ [_, ..] => { + quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case_insensitive)) } + } }; tokens.append_all(t); } @@ -175,7 +187,12 @@ impl MatcherSegment { /// A struct that models a single .toml file in /src/syntax_mapping/builtins/. #[derive(Clone, Debug, Deserialize)] struct MappingDefModel { + #[serde(default)] mappings: IndexMap>, + /// Case-sensitive mappings. Unlike `mappings`, these glob patterns are + /// matched case-sensitively. + #[serde(default)] + case_sensitive_mappings: IndexMap>, } impl MappingDefModel { fn into_mapping_list(self) -> MappingList { @@ -188,6 +205,19 @@ impl MappingDefModel { .map(|matcher| (matcher, target.clone())) .collect::>() }) + .chain( + self.case_sensitive_mappings + .into_iter() + .flat_map(|(target, matchers)| { + matchers + .into_iter() + .map(|mut matcher| { + matcher.case_insensitive = false; + (matcher, target.clone()) + }) + .collect::>() + }), + ) .collect(); MappingList(list) } diff --git a/src/syntax_mapping.rs b/src/syntax_mapping.rs index 0cd2d655..8fc4008a 100644 --- a/src/syntax_mapping.rs +++ b/src/syntax_mapping.rs @@ -17,9 +17,9 @@ use ignored_suffixes::IgnoredSuffixes; mod builtin; pub mod ignored_suffixes; -fn make_glob_matcher(from: &str) -> Result { +fn make_glob_matcher(from: &str, case_insensitive: bool) -> Result { let matcher = GlobBuilder::new(from) - .case_insensitive(true) + .case_insensitive(case_insensitive) .literal_separator(true) .build()? .compile_matcher(); @@ -97,7 +97,14 @@ impl<'a> SyntaxMapping<'a> { } pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from)?; + let matcher = make_glob_matcher(from, true)?; + self.custom_mappings.push((matcher, to)); + Ok(()) + } + + /// Like [`Self::insert`], but the glob pattern is matched case-sensitively. + pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { + let matcher = make_glob_matcher(from, false)?; self.custom_mappings.push((matcher, to)); Ok(()) } @@ -261,4 +268,41 @@ mod tests { Some(MappingTarget::MapTo("alpha")) ); } + + #[test] + fn case_sensitive_custom_mappings_work() { + let mut map = SyntaxMapping::new(); + map.insert_case_sensitive("MY_SPECIAL_FILE", MappingTarget::MapTo("Python")) + .ok(); + + // Exact case matches + assert_eq!( + map.get_syntax_for("/path/to/MY_SPECIAL_FILE"), + Some(MappingTarget::MapTo("Python")) + ); + // Different case should NOT match the case-sensitive rule + assert_eq!(map.get_syntax_for("/path/to/my_special_file"), None); + assert_eq!(map.get_syntax_for("/path/to/My_Special_File"), None); + } + + #[test] + fn builtin_mappings_build_is_case_sensitive() { + let map = SyntaxMapping::new(); + + // "BUILD" (uppercase) should map to Python via case-sensitive builtin + assert_eq!( + map.get_syntax_for("/path/to/BUILD"), + Some(MappingTarget::MapTo("Python")) + ); + // "build" (lowercase) should still map to MapToUnknown + assert_eq!( + map.get_syntax_for("/path/to/build"), + Some(MappingTarget::MapToUnknown) + ); + // Mixed case should NOT match the Python rule + assert_eq!( + map.get_syntax_for("/path/to/Build"), + Some(MappingTarget::MapToUnknown) + ); + } } diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 1822be57..52f1d47b 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -53,8 +53,9 @@ include!(concat!( /// A failure to compile is a fatal error. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_fixed(from: &str) -> GlobMatcher { - make_glob_matcher(from).expect("A builtin fixed glob matcher failed to compile") +fn build_matcher_fixed(from: &str, case_insensitive: bool) -> GlobMatcher { + make_glob_matcher(from, case_insensitive) + .expect("A builtin fixed glob matcher failed to compile") } /// Join a list of matcher segments to create a glob string, replacing all @@ -64,7 +65,7 @@ fn build_matcher_fixed(from: &str) -> GlobMatcher { /// to compile. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option { +fn build_matcher_dynamic(segs: &[MatcherSegment], case_insensitive: bool) -> Option { // join segments let mut buf = String::new(); for seg in segs { @@ -77,7 +78,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment]) -> Option { } } // compile glob matcher - let matcher = make_glob_matcher(&buf).ok()?; + let matcher = make_glob_matcher(&buf, case_insensitive).ok()?; Some(matcher) } diff --git a/src/syntax_mapping/builtins/README.md b/src/syntax_mapping/builtins/README.md index 29cf43ee..7152fd33 100644 --- a/src/syntax_mapping/builtins/README.md +++ b/src/syntax_mapping/builtins/README.md @@ -20,9 +20,9 @@ syntax mappings defined by all TOML files, and embed them into the binary. ## File syntax -Each TOML file should contain a single section named `mappings`, with each of -its keys being a language identifier (first column of `bat -L`; also referred to -as "target"). +Each TOML file should contain a single section named `mappings` and/or a single +section named `case_sensitive_mappings`, with each of its keys being a language +identifier (first column of `bat -L`; also referred to as "target"). The value of each key should be an array of strings, with each item being a glob matcher. We will call each of these items a "rule". diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml index 5f44ae2d..2b0e17e2 100644 --- a/src/syntax_mapping/builtins/common/50-bazel.toml +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -1,2 +1,2 @@ -[mappings] +[case_sensitive_mappings] "Python" = ["BUILD"] From 2a29802dd57a202589e555e896432575225fe76b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Mar 2026 03:35:38 +0000 Subject: [PATCH 4/8] Refactor: string-or-struct matcher syntax, Case enum, remove case_sensitive_mappings table Co-authored-by: keith-hall <11882719+keith-hall@users.noreply.github.com> --- build/syntax_mapping.rs | 83 +++++++++++++------ src/syntax_mapping.rs | 15 +++- src/syntax_mapping/builtin.rs | 10 +-- .../builtins/common/50-bazel.toml | 4 +- 4 files changed, 76 insertions(+), 36 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index f17b1cf5..17722f67 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -47,16 +47,34 @@ impl ToTokens for MappingTarget { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash, DeserializeFromStr)] +/// Whether a glob pattern should be matched case-sensitively or case-insensitively. +/// +/// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +enum Case { + Sensitive, + Insensitive, +} +impl ToTokens for Case { + fn to_tokens(&self, tokens: &mut TokenStream) { + let t = match self { + Self::Sensitive => quote! { Case::Sensitive }, + Self::Insensitive => quote! { Case::Insensitive }, + }; + tokens.append_all(t); + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. struct Matcher { segments: Vec, - /// Whether the glob pattern should be matched case-insensitively. + /// Whether the glob pattern should be matched case-sensitively. /// - /// Defaults to `true` (case-insensitive) for backwards compatibility. - case_insensitive: bool, + /// Defaults to `Case::Insensitive` for backwards compatibility. + case: Case, } /// Parse a matcher. /// @@ -124,21 +142,53 @@ impl FromStr for Matcher { Ok(Self { segments: non_empty_segments, - case_insensitive: true, + case: Case::Insensitive, }) } } + +/// Helper type for deserializing a `Matcher` from either a plain string or a +/// `{ glob = "...", case_sensitive = true }` struct. +#[derive(Deserialize)] +#[serde(untagged)] +enum RawMatcher { + Simple(String), + Full { + glob: String, + #[serde(default)] + case_sensitive: bool, + }, +} + +impl<'de> serde::Deserialize<'de> for Matcher { + fn deserialize>(deserializer: D) -> Result { + let raw = RawMatcher::deserialize(deserializer)?; + match raw { + RawMatcher::Simple(s) => Matcher::from_str(&s).map_err(serde::de::Error::custom), + RawMatcher::Full { glob, case_sensitive } => { + let mut matcher = + Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; + matcher.case = if case_sensitive { + Case::Sensitive + } else { + Case::Insensitive + }; + Ok(matcher) + } + } + } +} impl ToTokens for Matcher { fn to_tokens(&self, tokens: &mut TokenStream) { - let case_insensitive = self.case_insensitive; + let case = &self.case; let t = match self.segments.as_slice() { [] => unreachable!("0-length matcher should never be created"), [MatcherSegment::Text(text)] => { - quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case_insensitive))) } + quote! { Lazy::new(|| Some(build_matcher_fixed(#text, #case))) } } // parser logic ensures that this case can only happen when there are dynamic segments segs @ [_, ..] => { - quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case_insensitive)) } + quote! { Lazy::new(|| build_matcher_dynamic(&[ #(#segs),* ], #case)) } } }; tokens.append_all(t); @@ -189,10 +239,6 @@ impl MatcherSegment { struct MappingDefModel { #[serde(default)] mappings: IndexMap>, - /// Case-sensitive mappings. Unlike `mappings`, these glob patterns are - /// matched case-sensitively. - #[serde(default)] - case_sensitive_mappings: IndexMap>, } impl MappingDefModel { fn into_mapping_list(self) -> MappingList { @@ -205,19 +251,6 @@ impl MappingDefModel { .map(|matcher| (matcher, target.clone())) .collect::>() }) - .chain( - self.case_sensitive_mappings - .into_iter() - .flat_map(|(target, matchers)| { - matchers - .into_iter() - .map(|mut matcher| { - matcher.case_insensitive = false; - (matcher, target.clone()) - }) - .collect::>() - }), - ) .collect(); MappingList(list) } diff --git a/src/syntax_mapping.rs b/src/syntax_mapping.rs index 8fc4008a..584a5cb6 100644 --- a/src/syntax_mapping.rs +++ b/src/syntax_mapping.rs @@ -17,9 +17,16 @@ use ignored_suffixes::IgnoredSuffixes; mod builtin; pub mod ignored_suffixes; -fn make_glob_matcher(from: &str, case_insensitive: bool) -> Result { +/// Whether a glob pattern should be matched case-sensitively or case-insensitively. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum Case { + Sensitive, + Insensitive, +} + +fn make_glob_matcher(from: &str, case: Case) -> Result { let matcher = GlobBuilder::new(from) - .case_insensitive(case_insensitive) + .case_insensitive(matches!(case, Case::Insensitive)) .literal_separator(true) .build()? .compile_matcher(); @@ -97,14 +104,14 @@ impl<'a> SyntaxMapping<'a> { } pub fn insert(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from, true)?; + let matcher = make_glob_matcher(from, Case::Insensitive)?; self.custom_mappings.push((matcher, to)); Ok(()) } /// Like [`Self::insert`], but the glob pattern is matched case-sensitively. pub fn insert_case_sensitive(&mut self, from: &str, to: MappingTarget<'a>) -> Result<()> { - let matcher = make_glob_matcher(from, false)?; + let matcher = make_glob_matcher(from, Case::Sensitive)?; self.custom_mappings.push((matcher, to)); Ok(()) } diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 52f1d47b..41007be5 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -3,7 +3,7 @@ use std::env; use globset::GlobMatcher; use once_cell::sync::Lazy; -use crate::syntax_mapping::{make_glob_matcher, MappingTarget}; +use crate::syntax_mapping::{make_glob_matcher, Case, MappingTarget}; // Static syntax mappings generated from /src/syntax_mapping/builtins/ by the // build script (/build/syntax_mapping.rs). @@ -53,8 +53,8 @@ include!(concat!( /// A failure to compile is a fatal error. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_fixed(from: &str, case_insensitive: bool) -> GlobMatcher { - make_glob_matcher(from, case_insensitive) +fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher { + make_glob_matcher(from, case) .expect("A builtin fixed glob matcher failed to compile") } @@ -65,7 +65,7 @@ fn build_matcher_fixed(from: &str, case_insensitive: bool) -> GlobMatcher { /// to compile. /// /// Used internally by `Lazy>`'s lazy evaluation closure. -fn build_matcher_dynamic(segs: &[MatcherSegment], case_insensitive: bool) -> Option { +fn build_matcher_dynamic(segs: &[MatcherSegment], case: Case) -> Option { // join segments let mut buf = String::new(); for seg in segs { @@ -78,7 +78,7 @@ fn build_matcher_dynamic(segs: &[MatcherSegment], case_insensitive: bool) -> Opt } } // compile glob matcher - let matcher = make_glob_matcher(&buf, case_insensitive).ok()?; + let matcher = make_glob_matcher(&buf, case).ok()?; Some(matcher) } diff --git a/src/syntax_mapping/builtins/common/50-bazel.toml b/src/syntax_mapping/builtins/common/50-bazel.toml index 2b0e17e2..2ced1399 100644 --- a/src/syntax_mapping/builtins/common/50-bazel.toml +++ b/src/syntax_mapping/builtins/common/50-bazel.toml @@ -1,2 +1,2 @@ -[case_sensitive_mappings] -"Python" = ["BUILD"] +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] From c6e661d80bc9654583771f2ccd00aa1bf4a3e28a Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Wed, 18 Mar 2026 22:25:07 +0200 Subject: [PATCH 5/8] cargo fmt --- build/syntax_mapping.rs | 8 +++++--- src/syntax_mapping/builtin.rs | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 17722f67..11d778e3 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -165,9 +165,11 @@ impl<'de> serde::Deserialize<'de> for Matcher { let raw = RawMatcher::deserialize(deserializer)?; match raw { RawMatcher::Simple(s) => Matcher::from_str(&s).map_err(serde::de::Error::custom), - RawMatcher::Full { glob, case_sensitive } => { - let mut matcher = - Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; + RawMatcher::Full { + glob, + case_sensitive, + } => { + let mut matcher = Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; matcher.case = if case_sensitive { Case::Sensitive } else { diff --git a/src/syntax_mapping/builtin.rs b/src/syntax_mapping/builtin.rs index 41007be5..79d298c3 100644 --- a/src/syntax_mapping/builtin.rs +++ b/src/syntax_mapping/builtin.rs @@ -54,8 +54,7 @@ include!(concat!( /// /// Used internally by `Lazy>`'s lazy evaluation closure. fn build_matcher_fixed(from: &str, case: Case) -> GlobMatcher { - make_glob_matcher(from, case) - .expect("A builtin fixed glob matcher failed to compile") + make_glob_matcher(from, case).expect("A builtin fixed glob matcher failed to compile") } /// Join a list of matcher segments to create a glob string, replacing all From 3767f15c2abf07ef1edfa499fa27cfff03977393 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 20 Mar 2026 23:11:23 +0200 Subject: [PATCH 6/8] improvements from PR review fix documentation about syntax mappings --- src/syntax_mapping/builtins/README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/syntax_mapping/builtins/README.md b/src/syntax_mapping/builtins/README.md index 7152fd33..220e45df 100644 --- a/src/syntax_mapping/builtins/README.md +++ b/src/syntax_mapping/builtins/README.md @@ -20,12 +20,10 @@ syntax mappings defined by all TOML files, and embed them into the binary. ## File syntax -Each TOML file should contain a single section named `mappings` and/or a single -section named `case_sensitive_mappings`, with each of its keys being a language +Each TOML file should contain a single section named `mappings`, with each of its keys being a language identifier (first column of `bat -L`; also referred to as "target"). -The value of each key should be an array of strings, with each item being a glob -matcher. We will call each of these items a "rule". +The value of each key should be an array of "rules". The rules are expected to be objects with a `glob` string and a `case_sensitive` boolean. For simplification, a rule can be just a glob string, which is shorthand for the default case insensitive mode. For example, if `foo-application` uses both TOML and YAML configuration files, we could write something like this: @@ -98,6 +96,15 @@ like this: ] ``` +### Case sensitivity + +By default, all glob patterns are matched case-insensitively. To match a pattern case-sensitively, use the object form of the rule with the `case_sensitive` option: + +```toml +[mappings] +"Python" = [{ glob = "BUILD", case_sensitive = true }] +``` + ## Ordering At compile time, all TOML files applicable to the target are processed in From 7a6f442c8666bcda1649786ee48256f09b21363e Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 20 Mar 2026 23:35:27 +0200 Subject: [PATCH 7/8] improvements from PR review - prefer to use `Default::default` because it's semantically clearer why we've chosen to use a particular value --- build/syntax_mapping.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index 11d778e3..bc4601fc 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -50,9 +50,10 @@ impl ToTokens for MappingTarget { /// Whether a glob pattern should be matched case-sensitively or case-insensitively. /// /// Mirrors the runtime `Case` type in `src/syntax_mapping.rs`. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] enum Case { Sensitive, + #[default] Insensitive, } impl ToTokens for Case { @@ -65,7 +66,7 @@ impl ToTokens for Case { } } -#[derive(Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. From a19593b383f208106d019cbca3546cb0748eb377 Mon Sep 17 00:00:00 2001 From: Keith Hall Date: Fri, 20 Mar 2026 23:53:30 +0200 Subject: [PATCH 8/8] improvements from PR review 1. Added `Deserialize` derive and `#[serde(try_from = "RawMatcher")]` to the `Matcher` struct. With `try_from`, serde generates a `Deserialize` impl that first deserializes into `RawMatcher`, then calls `TryFrom for Matcher`. It never attempts to deserialize `Matcher`'s fields directly, so `Case` and `MatcherSegment` don't need `Deserialize` impls. 2. Replaced the manual `impl<'de> serde::Deserialize<'de> for Matcher` with a standard `impl TryFrom for Matcher`. The logic is identical - the conversion is fallible because `Matcher::from_str` returns `Result<_, anyhow::Error>`, so `try_from` (not `from`) is the correct choice, avoiding any panics. The net effect: same behavior, same error handling, but using the idiomatic serde pattern instead of a manual deserializer impl. The `RawMatcher` intermediate type and its `#[serde(untagged)]` derive remain unchanged. --- build/syntax_mapping.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/build/syntax_mapping.rs b/build/syntax_mapping.rs index bc4601fc..c7133be9 100644 --- a/build/syntax_mapping.rs +++ b/build/syntax_mapping.rs @@ -66,7 +66,8 @@ impl ToTokens for Case { } } -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Deserialize)] +#[serde(try_from = "RawMatcher")] /// A single matcher. /// /// Codegen converts this into a `Lazy>`. @@ -161,16 +162,17 @@ enum RawMatcher { }, } -impl<'de> serde::Deserialize<'de> for Matcher { - fn deserialize>(deserializer: D) -> Result { - let raw = RawMatcher::deserialize(deserializer)?; +impl TryFrom for Matcher { + type Error = anyhow::Error; + + fn try_from(raw: RawMatcher) -> Result { match raw { - RawMatcher::Simple(s) => Matcher::from_str(&s).map_err(serde::de::Error::custom), + RawMatcher::Simple(s) => Matcher::from_str(&s), RawMatcher::Full { glob, case_sensitive, } => { - let mut matcher = Matcher::from_str(&glob).map_err(serde::de::Error::custom)?; + let mut matcher = Matcher::from_str(&glob)?; matcher.case = if case_sensitive { Case::Sensitive } else {