From 53ee8a191997f7bc6e311a6444a3e23d49ae4823 Mon Sep 17 00:00:00 2001 From: RAprogramm Date: Sun, 5 Jul 2026 14:05:22 +0700 Subject: [PATCH] #447 fix: handle thiserror 2.x derive edge cases --- masterror-derive/src/display/enum_impl.rs | 12 +- masterror-derive/src/input/parse.rs | 81 +++++-- masterror-derive/src/input/parse_attr.rs | 150 ++++++++++++- masterror-derive/src/input/types.rs | 40 +++- masterror-derive/src/lib.rs | 72 ++++-- tests/error_derive_edge_cases.rs | 207 ++++++++++++++++++ .../ui/formatter/fail/enum_level_template.rs | 14 ++ .../formatter/fail/enum_level_template.stderr | 5 + .../fail/enum_level_template.stderr.license | 3 + tests/ui/formatter/pass/enum_level_fmt.rs | 31 +++ 10 files changed, 573 insertions(+), 42 deletions(-) create mode 100644 tests/error_derive_edge_cases.rs create mode 100644 tests/ui/formatter/fail/enum_level_template.rs create mode 100644 tests/ui/formatter/fail/enum_level_template.stderr create mode 100644 tests/ui/formatter/fail/enum_level_template.stderr.license create mode 100644 tests/ui/formatter/pass/enum_level_fmt.rs diff --git a/masterror-derive/src/display/enum_impl.rs b/masterror-derive/src/display/enum_impl.rs index f95678f..bf2bc5d 100644 --- a/masterror-derive/src/display/enum_impl.rs +++ b/masterror-derive/src/display/enum_impl.rs @@ -11,7 +11,7 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote; -use syn::Error; +use syn::{Error, ext::IdentExt}; use super::{ format_args::FormatArgumentsEnv, @@ -392,10 +392,12 @@ pub fn variant_named_placeholder( } match &placeholder.identifier { TemplateIdentifierSpec::Named(name) => { - if let Some(index) = fields - .iter() - .position(|field| field.ident.as_ref().is_some_and(|ident| ident == name)) - { + if let Some(index) = fields.iter().position(|field| { + field + .ident + .as_ref() + .is_some_and(|ident| ident.unraw() == name.as_str()) + }) { let binding = &bindings[index]; Ok(ResolvedPlaceholderExpr::with( quote!(#binding), diff --git a/masterror-derive/src/input/parse.rs b/masterror-derive/src/input/parse.rs index c6c776c..b7ef176 100644 --- a/masterror-derive/src/input/parse.rs +++ b/masterror-derive/src/input/parse.rs @@ -7,11 +7,16 @@ //! Handles top-level parsing of struct and enum error definitions, //! coordinating attribute extraction and validation. -use syn::{Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, Ident, spanned::Spanned}; +use syn::{ + Attribute, Data, DataEnum, DataStruct, DeriveInput, Error, ExprPath, Ident, spanned::Spanned +}; use super::{ - parse_attr::{extract_app_error_spec, extract_display_spec, extract_masterror_spec}, - types::{ErrorData, ErrorInput, Fields, FormatArgsSpec, StructData, VariantData}, + parse_attr::{ + extract_app_error_spec, extract_display_spec, extract_display_spec_optional, + extract_enum_fmt_spec, extract_masterror_spec + }, + types::{DisplaySpec, ErrorData, ErrorInput, Fields, FormatArgsSpec, StructData, VariantData}, utils::{ collect_errors, path_is, validate_backtrace_usage, validate_from_usage, validate_transparent @@ -77,28 +82,28 @@ fn parse_struct( } /// Parses enum error definition. +/// +/// An enum-level `#[error(fmt = ...)]` attribute provides a shared formatter +/// for variants that do not declare their own `#[error]` attribute. fn parse_enum( attrs: &[Attribute], data: DataEnum, errors: &mut Vec ) -> Result { - for attr in attrs { - if path_is(attr, "error") { - errors.push(Error::new_spanned( - attr, - "type-level #[error] attributes are not supported" - )); - } - } + let shared_fmt = extract_enum_fmt_spec(attrs, errors)?; let mut variants = Vec::new(); for variant in data.variants { - variants.push(parse_variant(variant, errors)?); + variants.push(parse_variant(variant, shared_fmt.as_ref(), errors)?); } Ok(ErrorData::Enum(variants)) } /// Parses single enum variant. -fn parse_variant(variant: syn::Variant, errors: &mut Vec) -> Result { +fn parse_variant( + variant: syn::Variant, + shared_fmt: Option<&ExprPath>, + errors: &mut Vec +) -> Result { let span = variant.span(); for attr in &variant.attrs { if path_is(attr, "from") { @@ -108,7 +113,19 @@ fn parse_variant(variant: syn::Variant, errors: &mut Vec) -> Result spec, + None => match shared_fmt { + Some(path) => DisplaySpec::FormatterPath { + path: path.clone(), + args: FormatArgsSpec::default() + }, + None => { + errors.push(Error::new(span, "missing #[error(...)] attribute")); + return Err(()); + } + } + }; let app_error = extract_app_error_spec(&variant.attrs, errors)?; let masterror = extract_masterror_spec(&variant.attrs, errors)?; let fields = Fields::from_syn(&variant.fields, errors); @@ -182,6 +199,42 @@ mod tests { assert!(result.is_err()); } + #[test] + fn parse_enum_level_fmt_fallback() { + let input: DeriveInput = parse_quote! { + #[error(fmt = crate::render)] + enum TestError { + A, + #[error("explicit")] + B + } + }; + let result = parse_input(input); + let parsed = result.expect("enum-level fmt fallback"); + let ErrorData::Enum(variants) = parsed.data else { + panic!("expected enum data"); + }; + assert!(matches!( + variants[0].display, + super::super::types::DisplaySpec::FormatterPath { .. } + )); + assert!(matches!( + variants[1].display, + super::super::types::DisplaySpec::Template(_) + )); + } + + #[test] + fn parse_enum_variant_missing_error_without_fallback() { + let input: DeriveInput = parse_quote! { + enum TestError { + A + } + }; + let result = parse_input(input); + assert!(result.is_err()); + } + #[test] fn parse_variant_from_attr() { let input: DeriveInput = parse_quote! { diff --git a/masterror-derive/src/input/parse_attr.rs b/masterror-derive/src/input/parse_attr.rs index 6e2f53a..6000ae1 100644 --- a/masterror-derive/src/input/parse_attr.rs +++ b/masterror-derive/src/input/parse_attr.rs @@ -91,6 +91,23 @@ pub(crate) fn extract_display_spec( missing_span: Span, errors: &mut Vec ) -> Result { + match extract_display_spec_optional(attrs, errors)? { + Some(spec) => Ok(spec), + None => { + errors.push(Error::new(missing_span, "missing #[error(...)] attribute")); + Err(()) + } + } +} + +/// Extracts display specification when the `#[error]` attribute is optional. +/// +/// Returns `Ok(None)` when no `#[error]` attribute is present, allowing the +/// caller to fall back to an enum-level formatter. +pub(crate) fn extract_display_spec_optional( + attrs: &[Attribute], + errors: &mut Vec +) -> Result, ()> { let mut display = None; let mut saw_error_attribute = false; for attr in attrs { @@ -108,14 +125,63 @@ pub(crate) fn extract_display_spec( } } match display { - Some(spec) => Ok(spec), - None => { - if !saw_error_attribute { - errors.push(Error::new(missing_span, "missing #[error(...)] attribute")); + Some(spec) => Ok(Some(spec)), + None if saw_error_attribute => Err(()), + None => Ok(None) + } +} + +/// Extracts an enum-level `#[error(fmt = ...)]` formatter path. +/// +/// The enum-level attribute serves as a shared formatter for variants that +/// do not carry their own `#[error]` attribute. Any other enum-level display +/// form (template or `transparent`) is rejected. +pub(crate) fn extract_enum_fmt_spec( + attrs: &[Attribute], + errors: &mut Vec +) -> Result, ()> { + let mut fmt = None; + let mut had_error = false; + for attr in attrs { + if !path_is(attr, "error") { + continue; + } + if fmt.is_some() { + errors.push(Error::new_spanned( + attr, + "duplicate enum-level #[error] attribute" + )); + had_error = true; + continue; + } + match parse_error_attribute(attr) { + Ok(DisplaySpec::FormatterPath { + path, + args + }) if args.args.is_empty() => fmt = Some(path), + Ok(DisplaySpec::FormatterPath { + .. + }) => { + errors.push(Error::new_spanned( + attr, + "enum-level #[error(fmt = ...)] does not accept format arguments" + )); + had_error = true; + } + Ok(_) => { + errors.push(Error::new_spanned( + attr, + "enum-level #[error] attributes support only `fmt = ...`" + )); + had_error = true; + } + Err(err) => { + errors.push(err); + had_error = true; } - Err(()) } } + if had_error { Err(()) } else { Ok(fmt) } } /// Parses #[app_error(...)] attribute contents. @@ -1233,6 +1299,80 @@ mod tests { assert!(result.is_err()); } + #[test] + fn extract_display_spec_optional_missing() { + let attrs: Vec = vec![parse_quote! { #[derive(Debug)] }]; + let mut errors = Vec::new(); + let result = extract_display_spec_optional(&attrs, &mut errors); + assert!(matches!(result, Ok(None))); + assert!(errors.is_empty()); + } + + #[test] + fn extract_display_spec_optional_present() { + let attrs: Vec = vec![parse_quote! { #[error("message")] }]; + let mut errors = Vec::new(); + let result = extract_display_spec_optional(&attrs, &mut errors); + assert!(matches!(result, Ok(Some(_)))); + } + + #[test] + fn extract_enum_fmt_spec_valid() { + let attrs: Vec = vec![parse_quote! { #[error(fmt = crate::render)] }]; + let mut errors = Vec::new(); + let result = extract_enum_fmt_spec(&attrs, &mut errors); + assert!(matches!(result, Ok(Some(_)))); + assert!(errors.is_empty()); + } + + #[test] + fn extract_enum_fmt_spec_missing() { + let attrs: Vec = vec![parse_quote! { #[derive(Debug)] }]; + let mut errors = Vec::new(); + let result = extract_enum_fmt_spec(&attrs, &mut errors); + assert!(matches!(result, Ok(None))); + assert!(errors.is_empty()); + } + + #[test] + fn extract_enum_fmt_spec_rejects_template() { + let attrs: Vec = vec![parse_quote! { #[error("template")] }]; + let mut errors = Vec::new(); + let result = extract_enum_fmt_spec(&attrs, &mut errors); + assert!(result.is_err()); + assert!(!errors.is_empty()); + } + + #[test] + fn extract_enum_fmt_spec_rejects_transparent() { + let attrs: Vec = vec![parse_quote! { #[error(transparent)] }]; + let mut errors = Vec::new(); + let result = extract_enum_fmt_spec(&attrs, &mut errors); + assert!(result.is_err()); + assert!(!errors.is_empty()); + } + + #[test] + fn extract_enum_fmt_spec_rejects_arguments() { + let attrs: Vec = vec![parse_quote! { #[error(fmt = render, arg)] }]; + let mut errors = Vec::new(); + let result = extract_enum_fmt_spec(&attrs, &mut errors); + assert!(result.is_err()); + assert!(!errors.is_empty()); + } + + #[test] + fn extract_enum_fmt_spec_rejects_duplicate() { + let attrs: Vec = vec![ + parse_quote! { #[error(fmt = first)] }, + parse_quote! { #[error(fmt = second)] }, + ]; + let mut errors = Vec::new(); + let result = extract_enum_fmt_spec(&attrs, &mut errors); + assert!(result.is_err()); + assert!(!errors.is_empty()); + } + #[test] fn parse_provide_attribute_missing_comma() { let attr: Attribute = parse_quote! { #[provide(ref = A value = B)] }; diff --git a/masterror-derive/src/input/types.rs b/masterror-derive/src/input/types.rs index f9beda5..cd4c64a 100644 --- a/masterror-derive/src/input/types.rs +++ b/masterror-derive/src/input/types.rs @@ -11,8 +11,8 @@ use proc_macro2::Span; use syn::{ AngleBracketedGenericArguments, Attribute, Error, Expr, ExprPath, Field as SynField, - Fields as SynFields, Ident, LitStr, Token, TypePath, punctuated::Punctuated, spanned::Spanned, - token::Paren + Fields as SynFields, Ident, LitStr, Token, TypePath, ext::IdentExt, punctuated::Punctuated, + spanned::Spanned, token::Paren }; use super::{parse_attr::parse_provide_attribute, utils::is_backtrace_storage}; @@ -138,11 +138,17 @@ impl Fields { } /// Finds a named field by identifier. + /// + /// Raw identifiers are matched by their unraw form, so a template + /// placeholder `{source}` resolves a field named `r#source`. pub fn get_named(&self, name: &str) -> Option<&Field> { match self { - Self::Named(fields) => fields - .iter() - .find(|field| field.ident.as_ref().is_some_and(|ident| ident == name)), + Self::Named(fields) => fields.iter().find(|field| { + field + .ident + .as_ref() + .is_some_and(|ident| ident.unraw() == name) + }), _ => None } } @@ -567,6 +573,14 @@ mod tests { assert!(parsed.get_named("y").is_some()); } + #[test] + fn fields_get_named_raw_ident_unraw_match() { + let fields: syn::FieldsNamed = parse_quote! { { r#source: String } }; + let mut errors = Vec::new(); + let parsed = Fields::from_syn(&syn::Fields::Named(fields), &mut errors); + assert!(parsed.get_named("source").is_some()); + } + #[test] fn fields_get_named_not_found() { let fields: syn::FieldsNamed = parse_quote! { { x: i32 } }; @@ -706,6 +720,22 @@ mod tests { assert!(parsed.attrs.has_source()); } + #[test] + fn field_attrs_raw_source_not_inferred() { + let field: SynField = parse_quote! { r#source: String }; + let mut errors = Vec::new(); + let parsed = Field::from_syn(&field, 0, &mut errors); + assert!(!parsed.attrs.has_source()); + } + + #[test] + fn field_attrs_raw_source_explicit_attribute() { + let field: SynField = parse_quote! { #[source] r#source: io::Error }; + let mut errors = Vec::new(); + let parsed = Field::from_syn(&field, 0, &mut errors); + assert!(parsed.attrs.has_source()); + } + #[test] fn field_attrs_has_source_from() { let field: SynField = parse_quote! { #[from] e: io::Error }; diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index d64d2ba..c9927fe 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -19,7 +19,7 @@ mod template_support; use proc_macro::TokenStream; use quote::quote; -use syn::{DeriveInput, Error, parse_macro_input}; +use syn::{Attribute, Data, DeriveInput, Error, parse_macro_input}; #[proc_macro_derive(Error, attributes(error, source, from, backtrace, app_error, provide))] pub fn derive_error(tokens: TokenStream) -> TokenStream { @@ -43,29 +43,75 @@ pub fn derive_masterror(tokens: TokenStream) -> TokenStream { } fn expand(input: DeriveInput) -> Result { + let deprecated = references_deprecated(&input); let parsed = input::parse_input(input)?; let display_impl = display::expand(&parsed)?; let error_impl = error_trait::expand(&parsed)?; let from_impls = from_impl::expand(&parsed)?; let app_error_impls = app_error_impl::expand(&parsed)?; - Ok(quote! { - #display_impl - #error_impl - #(#from_impls)* - #(#app_error_impls)* - }) + Ok(allow_deprecated( + deprecated, + quote! { + #display_impl + #error_impl + #(#from_impls)* + #(#app_error_impls)* + } + )) } fn expand_masterror(input: DeriveInput) -> Result { + let deprecated = references_deprecated(&input); let parsed = input::parse_input(input)?; let display_impl = display::expand(&parsed)?; let error_impl = error_trait::expand(&parsed)?; let from_impls = from_impl::expand(&parsed)?; let masterror_impl = masterror_impl::expand(&parsed)?; - Ok(quote! { - #display_impl - #error_impl - #(#from_impls)* - #masterror_impl - }) + Ok(allow_deprecated( + deprecated, + quote! { + #display_impl + #error_impl + #(#from_impls)* + #masterror_impl + } + )) +} + +/// Checks whether generated code will reference a deprecated item. +/// +/// Returns `true` when the type itself or any enum variant carries a +/// `#[deprecated]` attribute, in which case the expansion must be shielded +/// from the `deprecated` lint. +fn references_deprecated(input: &DeriveInput) -> bool { + fn has_deprecated(attrs: &[Attribute]) -> bool { + attrs.iter().any(|attr| attr.path().is_ident("deprecated")) + } + if has_deprecated(&input.attrs) { + return true; + } + match &input.data { + Data::Enum(data) => data + .variants + .iter() + .any(|variant| has_deprecated(&variant.attrs)), + _ => false + } +} + +/// Wraps generated implementations in `#[allow(deprecated)]` when needed. +/// +/// Deriving on a `#[deprecated]` type (or an enum with deprecated variants) +/// must not trigger the `deprecated` lint in the expansion. +fn allow_deprecated(deprecated: bool, body: proc_macro2::TokenStream) -> proc_macro2::TokenStream { + if deprecated { + quote! { + #[allow(deprecated)] + const _: () = { + #body + }; + } + } else { + body + } } diff --git a/tests/error_derive_edge_cases.rs b/tests/error_derive_edge_cases.rs new file mode 100644 index 0000000..bd11e21 --- /dev/null +++ b/tests/error_derive_edge_cases.rs @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2025 RAprogramm +// +// SPDX-License-Identifier: MIT + +//! Edge cases hardened against by thiserror 2.x, verified for masterror. +//! +//! Covers raw-identifier `r#source` fields opting out of implicit source +//! detection, dynamically sized final fields, associated types of generic +//! parameters in display templates, enum-level `#[error(fmt = ...)]` shared +//! across variants, and lint hygiene of generated code. + +use std::{error::Error as StdError, fmt}; + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("leaf failure")] +struct LeafError; + +#[derive(Debug, Error)] +#[error("raw source: {source}")] +struct RawSourceStruct { + r#source: String +} + +#[derive(Debug, Error)] +enum RawSourceEnum { + #[error("raw variant: {source}")] + Named { r#source: String } +} + +#[derive(Debug, Error)] +#[error("explicit: {source}")] +struct ExplicitRawSource { + #[source] + r#source: LeafError +} + +#[derive(Debug, Error)] +#[error("dst message: {msg}, tail: {tail:?}")] +struct DstTail { + msg: String, + tail: [u8] +} + +pub trait Repository { + type Entity: fmt::Debug + fmt::Display; +} + +#[derive(Debug)] +pub struct UserRepository; + +impl Repository for UserRepository { + type Entity = String; +} + +#[derive(Debug, Error)] +#[error("missing entity: {entity}")] +struct AssocTypeError +where + T::Entity: fmt::Debug + fmt::Display +{ + entity: T::Entity +} + +fn shared_formatter(f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("shared failure") +} + +fn value_formatter(value: &u8, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "value failure: {value}") +} + +#[derive(Debug, Error)] +#[error(fmt = shared_formatter)] +enum SharedFmtEnum { + First, + Second, + #[error(fmt = value_formatter)] + Custom(u8), + #[error("templated failure: {0}")] + Templated(u8), + #[error(transparent)] + Transparent(LeafError) +} + +mod hygiene { + #![deny( + deprecated, + unused_qualifications, + clippy::needless_lifetimes, + clippy::elidable_lifetime_names, + clippy::allow_attributes, + clippy::missing_inline_in_public_items + )] + + use masterror::Error; + + #[derive(Debug, Error)] + #[error("lifetime failure: {value}")] + pub struct LifetimeError<'a> { + pub value: &'a str + } + + #[deprecated] + #[derive(Debug, Error)] + #[error("deprecated type failure")] + pub struct DeprecatedError; + + #[derive(Debug, Error)] + pub enum DeprecatedVariantError { + #[deprecated] + #[error("deprecated variant failure")] + Old, + #[error("current variant failure")] + Current + } +} + +fn assert_error_impl() {} + +#[test] +fn raw_source_field_is_not_implicit_source() { + let error = RawSourceStruct { + r#source: "payload".to_owned() + }; + assert_eq!(error.to_string(), "raw source: payload"); + assert!(StdError::source(&error).is_none()); +} + +#[test] +fn raw_source_variant_field_is_not_implicit_source() { + let error = RawSourceEnum::Named { + r#source: "payload".to_owned() + }; + assert_eq!(error.to_string(), "raw variant: payload"); + assert!(StdError::source(&error).is_none()); +} + +#[test] +fn raw_source_field_with_explicit_attribute_is_source() { + let error = ExplicitRawSource { + r#source: LeafError + }; + assert_eq!(error.to_string(), "explicit: leaf failure"); + let source = StdError::source(&error).expect("explicit source"); + assert_eq!(source.to_string(), "leaf failure"); +} + +#[test] +fn dynamically_sized_final_field_compiles() { + assert_error_impl::(); +} + +#[test] +fn associated_type_field_uses_declared_bounds() { + let error = AssocTypeError:: { + entity: "user".to_owned() + }; + assert_eq!(error.to_string(), "missing entity: user"); + assert!(StdError::source(&error).is_none()); +} + +#[test] +fn enum_level_fmt_shared_across_variants() { + assert_eq!(SharedFmtEnum::First.to_string(), "shared failure"); + assert_eq!(SharedFmtEnum::Second.to_string(), "shared failure"); +} + +#[test] +fn enum_level_fmt_variant_overrides() { + assert_eq!(SharedFmtEnum::Custom(7).to_string(), "value failure: 7"); + assert_eq!( + SharedFmtEnum::Templated(9).to_string(), + "templated failure: 9" + ); + assert_eq!( + SharedFmtEnum::Transparent(LeafError).to_string(), + "leaf failure" + ); +} + +#[test] +fn deprecated_type_derive_does_not_warn() { + #[allow(deprecated)] + let error = hygiene::DeprecatedError; + assert_eq!(error.to_string(), "deprecated type failure"); +} + +#[test] +fn deprecated_variant_derive_does_not_warn() { + #[allow(deprecated)] + let old = hygiene::DeprecatedVariantError::Old; + assert_eq!(old.to_string(), "deprecated variant failure"); + assert_eq!( + hygiene::DeprecatedVariantError::Current.to_string(), + "current variant failure" + ); +} + +#[test] +fn lifetime_error_display() { + let error = hygiene::LifetimeError { + value: "borrowed" + }; + assert_eq!(error.to_string(), "lifetime failure: borrowed"); +} diff --git a/tests/ui/formatter/fail/enum_level_template.rs b/tests/ui/formatter/fail/enum_level_template.rs new file mode 100644 index 0000000..87b181f --- /dev/null +++ b/tests/ui/formatter/fail/enum_level_template.rs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025 RAprogramm +// +// SPDX-License-Identifier: MIT + +use masterror::Error; + +#[derive(Debug, Error)] +#[error("enum-level template")] +enum EnumLevelTemplate { + #[error("variant")] + Variant +} + +fn main() {} diff --git a/tests/ui/formatter/fail/enum_level_template.stderr b/tests/ui/formatter/fail/enum_level_template.stderr new file mode 100644 index 0000000..9516ffd --- /dev/null +++ b/tests/ui/formatter/fail/enum_level_template.stderr @@ -0,0 +1,5 @@ +error: enum-level #[error] attributes support only `fmt = ...` + --> tests/ui/formatter/fail/enum_level_template.rs:8:1 + | +8 | #[error("enum-level template")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/formatter/fail/enum_level_template.stderr.license b/tests/ui/formatter/fail/enum_level_template.stderr.license new file mode 100644 index 0000000..e4b3548 --- /dev/null +++ b/tests/ui/formatter/fail/enum_level_template.stderr.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 RAprogramm + +SPDX-License-Identifier: MIT diff --git a/tests/ui/formatter/pass/enum_level_fmt.rs b/tests/ui/formatter/pass/enum_level_fmt.rs new file mode 100644 index 0000000..24e0686 --- /dev/null +++ b/tests/ui/formatter/pass/enum_level_fmt.rs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2025 RAprogramm +// +// SPDX-License-Identifier: MIT + +use masterror::Error; + +fn shared(f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("shared") +} + +fn custom(value: &u8, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "custom={value}") +} + +#[derive(Debug, Error)] +#[error(fmt = crate::shared)] +enum SharedFormatter { + First, + Second, + #[error(fmt = crate::custom)] + Custom(u8), + #[error("template {0}")] + Template(u8) +} + +fn main() { + assert_eq!(SharedFormatter::First.to_string(), "shared"); + assert_eq!(SharedFormatter::Second.to_string(), "shared"); + assert_eq!(SharedFormatter::Custom(1).to_string(), "custom=1"); + assert_eq!(SharedFormatter::Template(2).to_string(), "template 2"); +}