diff --git a/CHANGELOG.md b/CHANGELOG.md index c21a1b5..b990fe7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Added +- `#[error(transparent)]` support in the derive macro: validates wrapper shape, + delegates `Display`/`source` to the inner error, and works with `#[from]`. + ## [0.4.0] - 2025-09-15 ### Added - Optional `frontend` feature: diff --git a/README.md b/README.md index ff14d8f..ed1bf7d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,8 @@ masterror = { version = "0.4.0", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude. +- **Less boilerplate.** Built-in conversions, compact prelude, + derive macro support for transparent wrappers via `#[error(transparent)]`. - **Consistent workspace.** Same error surface across crates. diff --git a/README.template.md b/README.template.md index 9342f58..aa69330 100644 --- a/README.template.md +++ b/README.template.md @@ -44,7 +44,8 @@ masterror = { version = "{{CRATE_VERSION}}", default-features = false } - **Opt-in integrations.** Zero default features; you enable what you need. - **Clean wire contract.** `ErrorResponse { status, code, message, details?, retry?, www_authenticate? }`. - **One log at boundary.** Log once with `tracing`. -- **Less boilerplate.** Built-in conversions, compact prelude. +- **Less boilerplate.** Built-in conversions, compact prelude, + derive macro support for transparent wrappers via `#[error(transparent)]`. - **Consistent workspace.** Same error surface across crates. diff --git a/masterror-derive/src/lib.rs b/masterror-derive/src/lib.rs index 7a7c3e8..c270ebb 100644 --- a/masterror-derive/src/lib.rs +++ b/masterror-derive/src/lib.rs @@ -5,8 +5,9 @@ //! //! 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, and a configurable error source via -//! `#[source]` field attributes. +//! `#[from]` conversions for wrapper types, transparent wrappers via +//! `#[error(transparent)]`, and a configurable error source via `#[source]` +//! field attributes. use std::collections::BTreeSet; @@ -15,7 +16,8 @@ use proc_macro2::{Ident, Span, TokenStream as TokenStream2}; use quote::{format_ident, quote}; use syn::{ Attribute, Data, DataEnum, DataStruct, DeriveInput, Field, Fields, GenericArgument, Generics, - LitStr, Member, Meta, PathArguments, Type, spanned::Spanned + Ident as SynIdent, LitStr, Member, Meta, PathArguments, Type, parse::ParseStream, + spanned::Spanned }; /// Derive [`std::error::Error`] and [`core::fmt::Display`] for structs and @@ -57,7 +59,7 @@ fn derive_error_impl(input: DeriveInput) -> syn::Result { 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)?; + error_impl = build_struct_error(&ident, &generics, &fields, &display_attr)?; from_impls = build_struct_from_impl(&ident, &generics, &fields)?; } Data::Enum(data) => { @@ -133,6 +135,17 @@ impl FieldAttributes { } } +#[derive(Clone)] +enum DisplayAttribute { + Format(LitStr), + Transparent(TransparentAttr) +} + +#[derive(Clone, Copy)] +struct TransparentAttr { + span: Span +} + #[derive(Clone, Copy)] enum SourceKind { Direct { needs_deref: bool }, @@ -154,7 +167,7 @@ struct ParsedFields { struct VariantInfo { ident: Ident, fields: ParsedFields, - display: LitStr + display: DisplayAttribute } struct FromFieldInfo<'a> { @@ -269,7 +282,7 @@ fn parse_fields_internal(fields: &Fields) -> syn::Result { } } -fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { +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() { @@ -280,8 +293,8 @@ fn parse_display_attr(attrs: &[Attribute]) -> syn::Result { } match &attr.meta { Meta::List(_) => { - let lit: LitStr = attr.parse_args()?; - result = Some(lit); + let attr_value = parse_error_attribute(attr)?; + result = Some(attr_value); } _ => { return Err(syn::Error::new( @@ -295,6 +308,37 @@ fn parse_display_attr(attrs: &[Attribute]) -> syn::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 { @@ -392,6 +436,23 @@ fn find_from_field<'a>( 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 { @@ -434,47 +495,63 @@ fn build_struct_display( ident: &Ident, generics: &Generics, fields: &ParsedFields, - display: &LitStr + display: &DisplayAttribute ) -> syn::Result { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(display, fields.fields.len())?; - - let body = 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; - } - }); + 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 Self { #( ref #field_idents ),* } = *self; - #[allow(unused_variables)] - let _ = (#(&#field_idents),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) + let #binding = &self.#member; + let _: &(dyn ::std::error::Error + 'static) = #binding; + ::core::fmt::Display::fmt(#binding, formatter) } } - 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) + 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) + } + } } } }; @@ -491,49 +568,64 @@ fn build_struct_display( fn build_struct_error( ident: &Ident, generics: &Generics, - fields: &ParsedFields + fields: &ParsedFields, + display: &DisplayAttribute ) -> syn::Result { let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let source_expr = 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)) + 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 } } } - } 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)> { - #source_expr + #body } } }) @@ -594,54 +686,96 @@ fn build_enum_display( let mut arms = Vec::with_capacity(variants.len()); for variant in variants { let variant_ident = &variant.ident; - let RewriteResult { - literal, - positional_indices - } = rewrite_format_string(&variant.display, variant.fields.fields.len())?; - let arm = 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; + 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) + } + } } - }); - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #[allow(unused_variables)] - let _ = (#(&#bindings),*); - #(#positional_bindings)* - ::core::write!(formatter, #literal) + 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" + ) + )); } } } - 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) + 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) + } + } } } } @@ -669,99 +803,139 @@ fn build_enum_error( let mut arms = Vec::with_capacity(variants.len()); for variant in variants { let variant_ident = &variant.ident; - let arm = 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)) + 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) + } } } - } else { - quote! { ::core::option::Option::None } - }; - quote! { - Self::#variant_ident { #( #bindings ),* } => { - #source_expr + 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" + ) + )); } } } - 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)) + 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 } } - } 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 + } } } } diff --git a/tests/error_derive.rs b/tests/error_derive.rs index 3df72ab..d5265d4 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -15,6 +15,18 @@ struct NamedError { #[error("leaf failure")] struct LeafError; +#[derive(Debug, Error)] +#[error("{0}")] +struct TransparentInner(#[source] LeafError); + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentWrapper(TransparentInner); + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentFromWrapper(#[from] TransparentInner); + #[derive(Debug, Error)] #[error("{0} -> {1:?}")] struct TupleError(&'static str, u8); @@ -75,6 +87,15 @@ enum MixedFromError { } } +#[derive(Debug, Error)] +enum TransparentEnum { + #[error("opaque {0}")] + Opaque(&'static str), + #[error(transparent)] + #[from] + TransparentVariant(TransparentInner) +} + #[test] fn named_struct_display_and_source() { let err = NamedError { @@ -163,3 +184,38 @@ fn enum_from_variants_generate_impls() { "secondary failure" ); } + +#[test] +fn transparent_struct_delegates_display_and_source() { + let inner = TransparentInner(LeafError); + let inner_display = inner.to_string(); + let inner_source = StdError::source(&inner).map(|err| err.to_string()); + let wrapper = TransparentWrapper(inner); + assert_eq!(wrapper.to_string(), inner_display); + assert_eq!( + StdError::source(&wrapper).map(|err| err.to_string()), + inner_source + ); +} + +#[test] +fn transparent_struct_from_impl() { + let wrapper = TransparentFromWrapper::from(TransparentInner(LeafError)); + assert_eq!(wrapper.to_string(), "leaf failure"); + assert_eq!( + StdError::source(&wrapper).map(|err| err.to_string()), + Some(String::from("leaf failure")) + ); +} + +#[test] +fn transparent_enum_variant_from_impl() { + let _unused = TransparentEnum::Opaque("noop"); + let variant = TransparentEnum::from(TransparentInner(LeafError)); + assert!(matches!(variant, TransparentEnum::TransparentVariant(_))); + assert_eq!(variant.to_string(), "leaf failure"); + assert_eq!( + StdError::source(&variant).map(|err| err.to_string()), + Some(String::from("leaf failure")) + ); +} diff --git a/tests/error_derive_from_trybuild.rs b/tests/error_derive_from_trybuild.rs index a878025..377c5c2 100644 --- a/tests/error_derive_from_trybuild.rs +++ b/tests/error_derive_from_trybuild.rs @@ -5,3 +5,9 @@ fn from_attribute_compile_failures() { let t = TestCases::new(); t.compile_fail("tests/ui/from/*.rs"); } + +#[test] +fn transparent_attribute_compile_failures() { + let t = TestCases::new(); + t.compile_fail("tests/ui/transparent/*.rs"); +} diff --git a/tests/ui/transparent/enum_variant_multiple_fields.rs b/tests/ui/transparent/enum_variant_multiple_fields.rs new file mode 100644 index 0000000..5565776 --- /dev/null +++ b/tests/ui/transparent/enum_variant_multiple_fields.rs @@ -0,0 +1,7 @@ +use masterror::Error; + +#[derive(Debug, Error)] +enum TransparentEnumFail { + #[error(transparent)] + Variant(String, String) +} diff --git a/tests/ui/transparent/enum_variant_multiple_fields.stderr b/tests/ui/transparent/enum_variant_multiple_fields.stderr new file mode 100644 index 0000000..2d0db83 --- /dev/null +++ b/tests/ui/transparent/enum_variant_multiple_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in variant `Variant` requires exactly one field + --> tests/ui/transparent/enum_variant_multiple_fields.rs:5:5 + | +5 | #[error(transparent)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/transparent/enum_variant_multiple_fields.rs:7:2 + | +7 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/enum_variant_multiple_fields.rs` diff --git a/tests/ui/transparent/enum_variant_no_fields.rs b/tests/ui/transparent/enum_variant_no_fields.rs new file mode 100644 index 0000000..92f40e0 --- /dev/null +++ b/tests/ui/transparent/enum_variant_no_fields.rs @@ -0,0 +1,7 @@ +use masterror::Error; + +#[derive(Debug, Error)] +enum TransparentEnumUnit { + #[error(transparent)] + Variant +} diff --git a/tests/ui/transparent/enum_variant_no_fields.stderr b/tests/ui/transparent/enum_variant_no_fields.stderr new file mode 100644 index 0000000..fc44590 --- /dev/null +++ b/tests/ui/transparent/enum_variant_no_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in variant `Variant` requires exactly one field + --> tests/ui/transparent/enum_variant_no_fields.rs:5:5 + | +5 | #[error(transparent)] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/transparent/enum_variant_no_fields.rs:7:2 + | +7 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/enum_variant_no_fields.rs` diff --git a/tests/ui/transparent/struct_multiple_fields.rs b/tests/ui/transparent/struct_multiple_fields.rs new file mode 100644 index 0000000..ddde691 --- /dev/null +++ b/tests/ui/transparent/struct_multiple_fields.rs @@ -0,0 +1,8 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentMany { + first: String, + second: String +} diff --git a/tests/ui/transparent/struct_multiple_fields.stderr b/tests/ui/transparent/struct_multiple_fields.stderr new file mode 100644 index 0000000..32a7491 --- /dev/null +++ b/tests/ui/transparent/struct_multiple_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in struct `TransparentMany` 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 + | +8 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/struct_multiple_fields.rs` diff --git a/tests/ui/transparent/struct_no_fields.rs b/tests/ui/transparent/struct_no_fields.rs new file mode 100644 index 0000000..a747343 --- /dev/null +++ b/tests/ui/transparent/struct_no_fields.rs @@ -0,0 +1,5 @@ +use masterror::Error; + +#[derive(Debug, Error)] +#[error(transparent)] +struct TransparentUnit; diff --git a/tests/ui/transparent/struct_no_fields.stderr b/tests/ui/transparent/struct_no_fields.stderr new file mode 100644 index 0000000..be3f853 --- /dev/null +++ b/tests/ui/transparent/struct_no_fields.stderr @@ -0,0 +1,11 @@ +error: using #[error(transparent)] in struct `TransparentUnit` 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 + | +5 | struct TransparentUnit; + | ^ consider adding a `main` function to `$DIR/tests/ui/transparent/struct_no_fields.rs`