From 30e76da5b63c33841eaf66b6b130eb39a6674899 Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:39:14 +0700 Subject: [PATCH] Replace derive crate with thiserror re-export --- Cargo.lock | 49 +- Cargo.toml | 6 +- masterror-derive/Cargo.toml | 16 - masterror-derive/src/lib.rs | 1110 ----------------- src/lib.rs | 5 +- tests/error_derive.rs | 19 +- tests/ui/from/struct_multiple_fields.stderr | 4 +- tests/ui/from/variant_multiple_fields.stderr | 4 +- .../enum_variant_multiple_fields.stderr | 7 +- .../transparent/enum_variant_no_fields.stderr | 7 +- .../transparent/struct_multiple_fields.stderr | 4 +- tests/ui/transparent/struct_no_fields.stderr | 4 +- 12 files changed, 64 insertions(+), 1171 deletions(-) delete mode 100644 masterror-derive/Cargo.toml delete mode 100644 masterror-derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ae55d42..a307650 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1493,7 +1493,7 @@ checksum = "3d38aeb53944762378aa5219b9929f2f3346a25fdd9b61266c24a487200c87fd" dependencies = [ "http 1.3.1", "serde", - "thiserror", + "thiserror 2.0.16", "tracing", ] @@ -1506,7 +1506,6 @@ dependencies = [ "config", "http 1.3.1", "js-sys", - "masterror-derive", "redis", "reqwest", "serde", @@ -1515,6 +1514,7 @@ dependencies = [ "sqlx", "telegram-webapp-sdk", "teloxide-core", + "thiserror 1.0.69", "tokio", "toml 0.8.23", "toml 0.9.6", @@ -1525,15 +1525,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "masterror-derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "matchit" version = "0.8.4" @@ -1755,7 +1746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror", + "thiserror 2.0.16", "ucd-trie", ] @@ -2507,7 +2498,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror", + "thiserror 2.0.16", "tracing", "url", ] @@ -2582,7 +2573,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.16", "tracing", "whoami", ] @@ -2619,7 +2610,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 2.0.16", "tracing", "whoami", ] @@ -2642,7 +2633,7 @@ dependencies = [ "percent-encoding", "serde_urlencoded", "sqlx-core", - "thiserror", + "thiserror 2.0.16", "tracing", "url", ] @@ -2756,7 +2747,7 @@ dependencies = [ "serde-wasm-bindgen", "serde_json", "serde_urlencoded", - "thiserror", + "thiserror 2.0.16", "toml 0.8.23", "wasm-bindgen", "wasm-bindgen-futures", @@ -2788,7 +2779,7 @@ dependencies = [ "stacker", "take_mut", "takecell", - "thiserror", + "thiserror 2.0.16", "tokio", "tokio-util", "url", @@ -2804,13 +2795,33 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index cf609c4..c9dfacf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,6 @@ categories = ["rust-patterns", "web-programming"] keywords = ["error", "api", "framework"] -[workspace] -members = ["masterror-derive"] - - [features] default = [] axum = ["dep:axum", "dep:serde_json"] # IntoResponse + JSON body @@ -37,7 +33,7 @@ turnkey = [] openapi = ["dep:utoipa"] [dependencies] -masterror-derive = { path = "masterror-derive", version = "0.1.0" } +thiserror = "1" tracing = "0.1" serde = { version = "1", features = ["derive"] } diff --git a/masterror-derive/Cargo.toml b/masterror-derive/Cargo.toml deleted file mode 100644 index 31d1fb5..0000000 --- a/masterror-derive/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "masterror-derive" -version = "0.1.0" -edition = "2024" -rust-version = "1.89" -description = "Derive macro for masterror" -license = "MIT OR Apache-2.0" -authors = ["masterror maintainers"] - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs deleted file mode 100644 index c270ebb..0000000 --- a/masterror-derive/src/lib.rs +++ /dev/null @@ -1,1110 +0,0 @@ -#![forbid(unsafe_code)] -#![warn(missing_docs, clippy::all, rust_2018_idioms)] - -//! Derive macro implementing [`std::error::Error`] with `Display` formatting. -//! -//! The macro mirrors the essential functionality relied upon by `masterror` and -//! consumers of the crate: display strings with named or positional fields, -//! `#[from]` conversions for wrapper types, transparent wrappers via -//! `#[error(transparent)]`, and a configurable error source via `#[source]` -//! field attributes. - -use std::collections::BTreeSet; - -use proc_macro::TokenStream; -use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use syn::{ - Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericArgument, Generics, - Ident as SynIdent, LitStr, Member, Meta, PathArguments, Type, parse::ParseStream, - spanned::Spanned -}; - -/// Derive [`std::error::Error`] and [`core::fmt::Display`] for structs and -/// enums. -/// -/// -/// #[derive(Debug, Error)] -/// #[error("{code}: {message}")] -/// struct MiniError { -/// code: u16, -/// message: &'static str -/// } -/// -/// let err = MiniError { -/// code: 500, -/// message: "boom" -/// }; -/// assert_eq!(err.to_string(), "500: boom"); -/// assert!(std::error::Error::source(&err).is_none()); -/// ``` -#[proc_macro_derive(Error, attributes(error, source, from))] -pub fn derive_error(input: TokenStream) -> TokenStream { - match derive_error_impl(syn::parse_macro_input!(input as DeriveInput)) { - Ok(tokens) => tokens.into(), - Err(err) => err.to_compile_error().into() - } -} - -fn derive_error_impl(input: DeriveInput) -> syn::Result { - let ident = input.ident; - let generics = input.generics; - - let display_impl; - let error_impl; - let from_impls; - - match input.data { - Data::Struct(data) => { - let fields = parse_fields(&data)?; - let display_attr = parse_display_attr(&input.attrs)?; - display_impl = build_struct_display(&ident, &generics, &fields, &display_attr)?; - error_impl = build_struct_error(&ident, &generics, &fields, &display_attr)?; - from_impls = build_struct_from_impl(&ident, &generics, &fields)?; - } - Data::Enum(data) => { - let variants = parse_enum(&data)?; - display_impl = build_enum_display(&ident, &generics, &variants)?; - error_impl = build_enum_error(&ident, &generics, &variants)?; - from_impls = build_enum_from_impls(&ident, &generics, &variants)?; - } - Data::Union(_) => { - return Err(syn::Error::new( - ident.span(), - "#[derive(Error)] does not support unions" - )); - } - } - - Ok(quote! { - #display_impl - #error_impl - #from_impls - }) -} - -#[derive(Clone, Copy)] -enum FieldsStyle { - Unit, - Named, - Unnamed -} - -#[derive(Clone)] -struct FieldSpec { - member: Member, - ident: Option, - binding: Ident, - ty: Type, - attrs: FieldAttributes -} - -#[derive(Clone, Default)] -struct FieldAttributes { - from: Option, - source: Option -} - -impl FieldAttributes { - fn mark_from(&mut self, span: Span) -> syn::Result<()> { - if self.from.is_some() { - return Err(syn::Error::new(span, "duplicate #[from] attribute")); - } - self.from = Some(span); - Ok(()) - } - - fn mark_source(&mut self, span: Span) -> syn::Result<()> { - if self.source.is_some() { - return Err(syn::Error::new(span, "duplicate #[source] attribute")); - } - self.source = Some(span); - Ok(()) - } - - fn has_source(&self) -> bool { - self.source.is_some() - } - - fn span_of_from_attribute(&self) -> Option { - self.from - } - - fn source_span(&self) -> Option { - self.source - } -} - -#[derive(Clone)] -enum DisplayAttribute { - Format(LitStr), - Transparent(TransparentAttr) -} - -#[derive(Clone, Copy)] -struct TransparentAttr { - span: Span -} - -#[derive(Clone, Copy)] -enum SourceKind { - Direct { needs_deref: bool }, - Optional { needs_deref: bool } -} - -#[derive(Clone, Copy)] -struct SourceField { - index: usize, - kind: SourceKind -} - -struct ParsedFields { - style: FieldsStyle, - fields: Vec, - source: Option -} - -struct VariantInfo { - ident: Ident, - fields: ParsedFields, - display: DisplayAttribute -} - -struct FromFieldInfo<'a> { - field: &'a FieldSpec, - span: Span -} - -struct RewriteResult { - literal: LitStr, - positional_indices: BTreeSet -} - -fn parse_fields(data: &DataStruct) -> syn::Result { - parse_fields_internal(&data.fields) -} - -fn parse_enum(data: &DataEnum) -> syn::Result> { - let mut variants = Vec::with_capacity(data.variants.len()); - for variant in &data.variants { - let display = parse_display_attr(&variant.attrs)?; - let mut fields = parse_fields_internal(&variant.fields)?; - if let Some(span) = parse_variant_from_attr(&variant.attrs)? { - apply_variant_from_attr(&mut fields, span, &variant.ident)?; - } - variants.push(VariantInfo { - ident: variant.ident.clone(), - fields, - display - }); - } - Ok(variants) -} - -fn parse_fields_internal(fields: &Fields) -> syn::Result { - match fields { - Fields::Unit => Ok(ParsedFields { - style: FieldsStyle::Unit, - fields: Vec::new(), - source: None - }), - Fields::Named(named) => { - let mut specs = Vec::with_capacity(named.named.len()); - let mut source = None; - for (index, field) in named.named.iter().enumerate() { - let attrs = parse_field_attributes(field)?; - let ident = field.ident.clone().ok_or_else(|| { - syn::Error::new(field.span(), "named field missing identifier") - })?; - let member = Member::Named(ident.clone()); - let binding = ident.clone(); - if attrs.has_source() { - let kind = detect_source_kind(&field.ty)?; - if source.is_some() { - return Err(syn::Error::new( - attrs.source_span().unwrap_or_else(|| field.span()), - "only a single #[source] field is supported" - )); - } - source = Some(SourceField { - index, - kind - }); - } - specs.push(FieldSpec { - member, - ident: Some(ident), - binding, - ty: field.ty.clone(), - attrs - }); - } - Ok(ParsedFields { - style: FieldsStyle::Named, - fields: specs, - source - }) - } - Fields::Unnamed(unnamed) => { - let mut specs = Vec::with_capacity(unnamed.unnamed.len()); - let mut source = None; - for (index, field) in unnamed.unnamed.iter().enumerate() { - let attrs = parse_field_attributes(field)?; - let member = Member::Unnamed(index.into()); - let binding = format_ident!("__masterror_{index}"); - if attrs.has_source() { - let kind = detect_source_kind(&field.ty)?; - if source.is_some() { - return Err(syn::Error::new( - attrs.source_span().unwrap_or_else(|| field.span()), - "only a single #[source] field is supported" - )); - } - source = Some(SourceField { - index, - kind - }); - } - specs.push(FieldSpec { - member, - ident: None, - binding, - ty: field.ty.clone(), - attrs - }); - } - Ok(ParsedFields { - style: FieldsStyle::Unnamed, - fields: specs, - source - }) - } - } -} - -fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { - let mut result = None; - for attr in attrs.iter().filter(|attr| attr.path().is_ident("error")) { - if result.is_some() { - return Err(syn::Error::new( - attr.span(), - "multiple #[error(...)] attributes found" - )); - } - match &attr.meta { - Meta::List(_) => { - let attr_value = parse_error_attribute(attr)?; - result = Some(attr_value); - } - _ => { - return Err(syn::Error::new( - attr.span(), - r#"expected #[error("format")]"# - )); - } - } - } - result - .ok_or_else(|| syn::Error::new(Span::call_site(), r#"missing #[error("...")] attribute"#)) -} - -fn parse_error_attribute(attr: &Attribute) -> syn::Result { - attr.parse_args_with(|input: ParseStream<'_>| { - if input.is_empty() { - return Err(input.error("expected string literal or `transparent`")); - } - if input.peek(LitStr) { - let lit: LitStr = input.parse()?; - if !input.is_empty() { - return Err(input.error("unexpected tokens after format string")); - } - return Ok(DisplayAttribute::Format(lit)); - } - if input.peek(SynIdent) { - let ident: SynIdent = input.parse()?; - if ident == "transparent" { - if !input.is_empty() { - return Err(input.error("unexpected tokens after `transparent`")); - } - return Ok(DisplayAttribute::Transparent(TransparentAttr { - span: attr.span() - })); - } - return Err(syn::Error::new( - ident.span(), - "unknown #[error] attribute argument" - )); - } - Err(input.error("expected string literal or `transparent`")) - }) -} - -fn parse_field_attributes(field: &Field) -> syn::Result { - let mut attrs = FieldAttributes::default(); - for attr in &field.attrs { - if attr.path().is_ident("source") { - ensure_path_only(attr, "source")?; - attrs.mark_source(attr.span())?; - } else if attr.path().is_ident("from") { - ensure_path_only(attr, "from")?; - attrs.mark_from(attr.span())?; - } - } - Ok(attrs) -} - -fn ensure_path_only(attr: &Attribute, name: &str) -> syn::Result<()> { - if !matches!(&attr.meta, Meta::Path(_)) { - return Err(syn::Error::new( - attr.span(), - format!("#[{name}] attribute does not accept arguments") - )); - } - Ok(()) -} - -fn parse_variant_from_attr(attrs: &[Attribute]) -> syn::Result> { - let mut span = None; - for attr in attrs.iter().filter(|attr| attr.path().is_ident("from")) { - ensure_path_only(attr, "from")?; - if span.is_some() { - return Err(syn::Error::new(attr.span(), "duplicate #[from] attribute")); - } - span = Some(attr.span()); - } - Ok(span) -} - -fn apply_variant_from_attr( - fields: &mut ParsedFields, - span: Span, - variant_ident: &Ident -) -> syn::Result<()> { - if fields.fields.is_empty() { - return Err(syn::Error::new( - span, - format!( - "variant `{variant_ident}` marked with #[from] must contain exactly one field" - ) - )); - } - if fields.fields.len() != 1 { - return Err(syn::Error::new( - span, - format!( - "variant `{variant_ident}` marked with #[from] must contain exactly one field" - ) - )); - } - let field = fields - .fields - .get_mut(0) - .ok_or_else(|| syn::Error::new(span, "invalid #[from] field index"))?; - field.attrs.mark_from(span) -} - -fn find_from_field<'a>( - fields: &'a ParsedFields, - context: &str -) -> syn::Result>> { - let mut info = None; - for field in &fields.fields { - if let Some(span) = field.attrs.span_of_from_attribute() { - if info.is_some() { - return Err(syn::Error::new( - span, - format!( - "multiple #[from] attributes in {context}; only one field may use #[from]" - ) - )); - } - info = Some(FromFieldInfo { - field, - span - }); - } - } - let Some(info) = info else { - return Ok(None); - }; - if fields.fields.len() != 1 { - return Err(syn::Error::new( - info.span, - format!("using #[from] in {context} requires exactly one field") - )); - } - Ok(Some(info)) -} - -fn ensure_transparent_field<'a>( - fields: &'a ParsedFields, - attr: &TransparentAttr, - context: &str -) -> syn::Result<&'a FieldSpec> { - if fields.fields.len() != 1 { - return Err(syn::Error::new( - attr.span, - format!("using #[error(transparent)] in {context} requires exactly one field") - )); - } - fields - .fields - .first() - .ok_or_else(|| syn::Error::new(attr.span, "invalid transparent field index")) -} - -fn detect_source_kind(ty: &Type) -> syn::Result { - if let Some(inner) = option_inner_type(ty) { - Ok(SourceKind::Optional { - needs_deref: needs_deref(inner) - }) - } else { - Ok(SourceKind::Direct { - needs_deref: needs_deref(ty) - }) - } -} - -fn option_inner_type(ty: &Type) -> Option<&Type> { - if let Type::Path(type_path) = ty - && type_path.qself.is_none() - && let Some(segment) = type_path.path.segments.last() - && segment.ident == "Option" - && let PathArguments::AngleBracketed(args) = &segment.arguments - && let Some(GenericArgument::Type(inner)) = args.args.first() - { - return Some(inner); - } - None -} - -fn needs_deref(ty: &Type) -> bool { - if let Type::Path(type_path) = ty { - if type_path.qself.is_some() { - return false; - } - if let Some(segment) = type_path.path.segments.last() { - let ident = segment.ident.to_string(); - return matches!(ident.as_str(), "Box" | "Rc" | "Arc"); - } - } - false -} - -fn build_struct_display( - ident: &Ident, - generics: &Generics, - fields: &ParsedFields, - display: &DisplayAttribute -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let body = match display { - DisplayAttribute::Transparent(attr) => { - let context = format!("struct `{ident}`"); - let field = ensure_transparent_field(fields, attr, &context)?; - let member = &field.member; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - let #binding = &self.#member; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::core::fmt::Display::fmt(#binding, formatter) - } - } - DisplayAttribute::Format(literal) => { - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(literal, fields.fields.len())?; - - match fields.style { - FieldsStyle::Unit => quote! { - ::core::write!(formatter, #literal) - }, - FieldsStyle::Named => { - let field_idents: Vec<_> = fields - .fields - .iter() - .map(|f| f.ident.clone().expect("named fields must have identifiers")) - .collect(); - let positional_bindings = positional_indices.iter().map(|index| { - let binding_ident = format_ident!("__masterror_{index}"); - let field_ident = field_idents[*index].clone(); - quote! { - #[allow(unused_variables)] - let #binding_ident = &*#field_ident; - } - }); - quote! { - let Self { #( ref #field_idents ),* } = *self; - #[allow(unused_variables)] - let _ = (#(&#field_idents),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) - } - } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = - fields.fields.iter().map(|f| f.binding.clone()).collect(); - quote! { - let Self( #( ref #bindings ),* ) = *self; - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - ::core::write!(formatter, #literal) - } - } - } - } - }; - - Ok(quote! { - impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { - fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - #body - } - } - }) -} - -fn build_struct_error( - ident: &Ident, - generics: &Generics, - fields: &ParsedFields, - display: &DisplayAttribute -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let body = match display { - DisplayAttribute::Transparent(attr) => { - let context = format!("struct `{ident}`"); - let field = ensure_transparent_field(fields, attr, &context)?; - let member = &field.member; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - let #binding = &self.#member; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::std::error::Error::source(#binding) - } - } - DisplayAttribute::Format(_) => { - if let Some(source) = fields.source { - let field = fields.fields.get(source.index).ok_or_else(|| { - syn::Error::new(Span::call_site(), "invalid source field index") - })?; - let member = &field.member; - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(&self.#member as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(self.#member.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - self.#member - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - self.#member - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) - } - } - } else { - quote! { ::core::option::Option::None } - } - } - }; - - Ok(quote! { - impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { - fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { - #body - } - } - }) -} - -fn build_struct_from_impl( - ident: &Ident, - generics: &Generics, - fields: &ParsedFields -) -> syn::Result { - let context = format!("struct `{ident}`"); - let Some(from_info) = find_from_field(fields, &context)? else { - return Ok(TokenStream2::new()); - }; - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let field_ty = &from_info.field.ty; - let arg_ident = format_ident!("__masterror_from_value"); - - let construct = match fields.style { - FieldsStyle::Named => { - let field_ident = from_info.field.ident.clone().ok_or_else(|| { - syn::Error::new(from_info.span, "named field missing identifier") - })?; - quote! { Self { #field_ident: #arg_ident } } - } - FieldsStyle::Unnamed => { - if fields.fields.len() != 1 { - return Err(syn::Error::new( - from_info.span, - format!("using #[from] in {context} requires exactly one field") - )); - } - quote! { Self(#arg_ident) } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - from_info.span, - format!("using #[from] in {context} requires at least one field") - )); - } - }; - - Ok(quote! { - impl #impl_generics ::core::convert::From<#field_ty> for #ident #ty_generics #where_clause { - fn from(#arg_ident: #field_ty) -> Self { - #construct - } - } - }) -} - -fn build_enum_display( - ident: &Ident, - generics: &Generics, - variants: &[VariantInfo] -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut arms = Vec::with_capacity(variants.len()); - for variant in variants { - let variant_ident = &variant.ident; - let arm = match &variant.display { - DisplayAttribute::Transparent(attr) => { - let context = format!("variant `{variant_ident}`"); - let field = ensure_transparent_field(&variant.fields, attr, &context)?; - match variant.fields.style { - FieldsStyle::Named => { - let field_ident = field.ident.clone().ok_or_else(|| { - syn::Error::new(attr.span, "named field missing identifier") - })?; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident { #field_ident } => { - let #binding = #field_ident; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::core::fmt::Display::fmt(#binding, formatter) - } - } - } - FieldsStyle::Unnamed => { - let binding = field.binding.clone(); - let inner = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident( #binding ) => { - let #inner = #binding; - let _: &(dyn ::std::error::Error + 'static) = #inner; - ::core::fmt::Display::fmt(#inner, formatter) - } - } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - attr.span, - format!( - "variant `{variant_ident}` using #[error(transparent)] must have exactly one field" - ) - )); - } - } - } - DisplayAttribute::Format(literal) => { - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(literal, variant.fields.fields.len())?; - match variant.fields.style { - FieldsStyle::Unit => quote! { - Self::#variant_ident => ::core::write!(formatter, #literal) - }, - FieldsStyle::Named => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| { - f.ident - .clone() - .expect("named variant field requires identifier") - }) - .collect(); - let positional_bindings = positional_indices.iter().map(|index| { - let binding_ident = format_ident!("__masterror_{index}"); - let field_ident = bindings[*index].clone(); - quote! { - #[allow(unused_variables)] - let #binding_ident = &*#field_ident; - } - }); - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) - } - } - } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| f.binding.clone()) - .collect(); - quote! { - Self::#variant_ident( #( #bindings ),* ) => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - ::core::write!(formatter, #literal) - } - } - } - } - } - }; - arms.push(arm); - } - - Ok(quote! { - impl #impl_generics ::core::fmt::Display for #ident #ty_generics #where_clause { - fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - match self { - #(#arms),* - } - } - } - }) -} - -fn build_enum_error( - ident: &Ident, - generics: &Generics, - variants: &[VariantInfo] -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut arms = Vec::with_capacity(variants.len()); - for variant in variants { - let variant_ident = &variant.ident; - let arm = match &variant.display { - DisplayAttribute::Transparent(attr) => { - let context = format!("variant `{variant_ident}`"); - let field = ensure_transparent_field(&variant.fields, attr, &context)?; - match variant.fields.style { - FieldsStyle::Named => { - let field_ident = field.ident.clone().ok_or_else(|| { - syn::Error::new(attr.span, "named field missing identifier") - })?; - let binding = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident { #field_ident } => { - let #binding = #field_ident; - let _: &(dyn ::std::error::Error + 'static) = #binding; - ::std::error::Error::source(#binding) - } - } - } - FieldsStyle::Unnamed => { - let binding = field.binding.clone(); - let inner = format_ident!("__masterror_transparent_inner"); - quote! { - Self::#variant_ident( #binding ) => { - let #inner = #binding; - let _: &(dyn ::std::error::Error + 'static) = #inner; - ::std::error::Error::source(#inner) - } - } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - attr.span, - format!( - "variant `{variant_ident}` using #[error(transparent)] must have exactly one field" - ) - )); - } - } - } - DisplayAttribute::Format(_) => match variant.fields.style { - FieldsStyle::Unit => quote! { - Self::#variant_ident => ::core::option::Option::None - }, - FieldsStyle::Named => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| { - f.ident - .clone() - .expect("named variant field requires identifier") - }) - .collect(); - let source_expr = if let Some(source) = variant.fields.source { - let binding = bindings[source.index].clone(); - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - #binding - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - #binding - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) - } - } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #source_expr - } - } - } - FieldsStyle::Unnamed => { - let bindings: Vec<_> = variant - .fields - .fields - .iter() - .map(|f| f.binding.clone()) - .collect(); - let source_expr = if let Some(source) = variant.fields.source { - let binding = bindings[source.index].clone(); - match source.kind { - SourceKind::Direct { - needs_deref: false - } => quote! { - ::core::option::Option::Some(#binding as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Direct { - needs_deref: true - } => quote! { - ::core::option::Option::Some(#binding.as_ref() as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: false - } => quote! { - #binding - .as_ref() - .map(|source| source as &(dyn ::std::error::Error + 'static)) - }, - SourceKind::Optional { - needs_deref: true - } => quote! { - #binding - .as_ref() - .map(|source| source.as_ref() as &(dyn ::std::error::Error + 'static)) - } - } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident( #( #bindings ),* ) => { - #source_expr - } - } - } - } - }; - arms.push(arm); - } - - Ok(quote! { - impl #impl_generics ::std::error::Error for #ident #ty_generics #where_clause { - fn source(&self) -> ::core::option::Option<&(dyn ::std::error::Error + 'static)> { - match self { - #(#arms),* - } - } - } - }) -} - -fn build_enum_from_impls( - ident: &Ident, - generics: &Generics, - variants: &[VariantInfo] -) -> syn::Result { - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let mut impls = Vec::new(); - - for variant in variants { - let context = format!("variant `{}`", variant.ident); - if let Some(from_info) = find_from_field(&variant.fields, &context)? { - let field_ty = &from_info.field.ty; - let variant_ident = &variant.ident; - let arg_ident = format_ident!("__masterror_from_value"); - - let body = match variant.fields.style { - FieldsStyle::Named => { - let field_ident = from_info.field.ident.clone().ok_or_else(|| { - syn::Error::new(from_info.span, "named field missing identifier") - })?; - quote! { Self::#variant_ident { #field_ident: #arg_ident } } - } - FieldsStyle::Unnamed => { - if variant.fields.fields.len() != 1 { - return Err(syn::Error::new( - from_info.span, - format!("using #[from] in {context} requires exactly one field") - )); - } - quote! { Self::#variant_ident(#arg_ident) } - } - FieldsStyle::Unit => { - return Err(syn::Error::new( - from_info.span, - format!("{context} cannot be unit-like when using #[from]") - )); - } - }; - - impls.push(quote! { - impl #impl_generics ::core::convert::From<#field_ty> for #ident #ty_generics #where_clause { - fn from(#arg_ident: #field_ty) -> Self { - #body - } - } - }); - } - } - - Ok(quote! { #(#impls)* }) -} - -fn rewrite_format_string(original: &LitStr, field_count: usize) -> syn::Result { - let src = original.value(); - let mut result = String::with_capacity(src.len()); - let mut positional_indices = BTreeSet::new(); - let bytes = src.as_bytes(); - let mut i = 0; - let len = bytes.len(); - let mut next_implicit = 0usize; - - while i < len { - match bytes[i] { - b'{' => { - if i + 1 < len && bytes[i + 1] == b'{' { - result.push_str("{{"); - i += 2; - continue; - } - let start = i + 1; - let mut j = start; - while j < len { - if bytes[j] == b'}' { - break; - } - if bytes[j] == b'{' { - return Err(syn::Error::new( - original.span(), - "nested '{' inside format string is not supported" - )); - } - j += 1; - } - if j == len { - return Err(syn::Error::new( - original.span(), - "unmatched '{' in format string" - )); - } - let content = &src[start..j]; - let (arg, rest) = if let Some(pos) = content.find(':') { - (&content[..pos], Some(&content[pos + 1..])) - } else { - (content, None) - }; - let trimmed = arg.trim(); - let mut used_index = None; - if trimmed.is_empty() { - used_index = Some(next_implicit); - next_implicit += 1; - } else if trimmed.chars().all(|ch| ch.is_ascii_digit()) { - let idx: usize = trimmed.parse().map_err(|_| { - syn::Error::new(original.span(), "invalid positional index") - })?; - used_index = Some(idx); - } - result.push('{'); - if let Some(idx) = used_index { - if idx >= field_count { - return Err(syn::Error::new( - original.span(), - "format index exceeds field count" - )); - } - positional_indices.insert(idx); - let ident = format!("__masterror_{}", idx); - result.push_str(&ident); - } else { - result.push_str(arg); - } - if let Some(rest) = rest { - result.push(':'); - result.push_str(rest); - } - result.push('}'); - i = j + 1; - } - b'}' => { - if i + 1 < len && bytes[i + 1] == b'}' { - result.push_str("}}"); - i += 2; - } else { - return Err(syn::Error::new( - original.span(), - "unmatched '}' in format string" - )); - } - } - _ => { - let start = i; - i += 1; - while i < len && bytes[i] != b'{' && bytes[i] != b'}' { - i += 1; - } - result.push_str(&src[start..i]); - } - } - } - - Ok(RewriteResult { - literal: LitStr::new(&result, original.span()), - positional_indices - }) -} diff --git a/src/lib.rs b/src/lib.rs index 6ac640c..8214c92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,6 +202,8 @@ pub mod prelude; pub use app_error::{AppError, AppResult}; pub use code::AppCode; pub use kind::AppErrorKind; +pub use response::{ErrorResponse, RetryAdvice}; +#[cfg_attr(docsrs, doc(cfg(feature = "derive")))] /// Derive macro replicating the ergonomics of `thiserror::Error`. /// /// ``` @@ -223,5 +225,4 @@ pub use kind::AppErrorKind; /// assert_eq!(err.to_string(), "500: boom"); /// assert!(StdError::source(&err).is_none()); /// ``` -pub use masterror_derive::Error; -pub use response::{ErrorResponse, RetryAdvice}; +pub use thiserror::Error; diff --git a/tests/error_derive.rs b/tests/error_derive.rs index d5265d4..c257384 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -64,10 +64,17 @@ struct TupleWrapper( #[derive(Debug, Error)] #[error("message: {message}")] struct MessageWrapper { - #[from] message: String } +impl From for MessageWrapper { + fn from(message: String) -> Self { + Self { + message + } + } +} + #[derive(Debug, Error)] enum MixedFromError { #[error("tuple variant {0}")] @@ -77,8 +84,11 @@ enum MixedFromError { LeafError ), #[error("variant attr {0}")] - #[from] - VariantAttr(#[source] PrimaryError), + VariantAttr( + #[from] + #[source] + PrimaryError + ), #[error("named variant {source:?}")] Named { #[from] @@ -92,8 +102,7 @@ enum TransparentEnum { #[error("opaque {0}")] Opaque(&'static str), #[error(transparent)] - #[from] - TransparentVariant(TransparentInner) + TransparentVariant(#[from] TransparentInner) } #[test] diff --git a/tests/ui/from/struct_multiple_fields.stderr b/tests/ui/from/struct_multiple_fields.stderr index c529748..b8b63ce 100644 --- a/tests/ui/from/struct_multiple_fields.stderr +++ b/tests/ui/from/struct_multiple_fields.stderr @@ -1,5 +1,5 @@ -error: using #[from] in struct `BadStruct` requires exactly one field +error: deriving From requires no fields other than source and backtrace --> tests/ui/from/struct_multiple_fields.rs:6:5 | 6 | #[from] - | ^ + | ^^^^^^^ diff --git a/tests/ui/from/variant_multiple_fields.stderr b/tests/ui/from/variant_multiple_fields.stderr index 0f07dd2..e78cc01 100644 --- a/tests/ui/from/variant_multiple_fields.stderr +++ b/tests/ui/from/variant_multiple_fields.stderr @@ -1,5 +1,5 @@ -error: variant `Two` marked with #[from] must contain exactly one field +error: not expected here; the #[from] attribute belongs on a specific field --> tests/ui/from/variant_multiple_fields.rs:6:5 | 6 | #[from] - | ^ + | ^^^^^^^ diff --git a/tests/ui/transparent/enum_variant_multiple_fields.stderr b/tests/ui/transparent/enum_variant_multiple_fields.stderr index 2d0db83..d7b939b 100644 --- a/tests/ui/transparent/enum_variant_multiple_fields.stderr +++ b/tests/ui/transparent/enum_variant_multiple_fields.stderr @@ -1,8 +1,9 @@ -error: using #[error(transparent)] in variant `Variant` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/enum_variant_multiple_fields.rs:5:5 | -5 | #[error(transparent)] - | ^ +5 | / #[error(transparent)] +6 | | Variant(String, String) + | |___________________________^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/enum_variant_multiple_fields.rs:7:2 diff --git a/tests/ui/transparent/enum_variant_no_fields.stderr b/tests/ui/transparent/enum_variant_no_fields.stderr index fc44590..06a21e0 100644 --- a/tests/ui/transparent/enum_variant_no_fields.stderr +++ b/tests/ui/transparent/enum_variant_no_fields.stderr @@ -1,8 +1,9 @@ -error: using #[error(transparent)] in variant `Variant` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/enum_variant_no_fields.rs:5:5 | -5 | #[error(transparent)] - | ^ +5 | / #[error(transparent)] +6 | | Variant + | |___________^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/enum_variant_no_fields.rs:7:2 diff --git a/tests/ui/transparent/struct_multiple_fields.stderr b/tests/ui/transparent/struct_multiple_fields.stderr index 32a7491..d55036a 100644 --- a/tests/ui/transparent/struct_multiple_fields.stderr +++ b/tests/ui/transparent/struct_multiple_fields.stderr @@ -1,8 +1,8 @@ -error: using #[error(transparent)] in struct `TransparentMany` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/struct_multiple_fields.rs:4:1 | 4 | #[error(transparent)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/struct_multiple_fields.rs:8:2 diff --git a/tests/ui/transparent/struct_no_fields.stderr b/tests/ui/transparent/struct_no_fields.stderr index be3f853..d395ffa 100644 --- a/tests/ui/transparent/struct_no_fields.stderr +++ b/tests/ui/transparent/struct_no_fields.stderr @@ -1,8 +1,8 @@ -error: using #[error(transparent)] in struct `TransparentUnit` requires exactly one field +error: #[error(transparent)] requires exactly one field --> tests/ui/transparent/struct_no_fields.rs:4:1 | 4 | #[error(transparent)] - | ^ + | ^^^^^^^^^^^^^^^^^^^^^ error[E0601]: `main` function not found in crate `$CRATE` --> tests/ui/transparent/struct_no_fields.rs:5:24