From 2fd58e2e27935ff431ae19431fdc5a9afb20714d Mon Sep 17 00:00:00 2001 From: RA <70325462+RAprogramm@users.noreply.github.com> Date: Fri, 19 Sep 2025 09:08:55 +0700 Subject: [PATCH] Allow #[from] to coexist with companion diagnostics --- masterror-derive/src/from_impl.rs | 129 +++++++++++++++---- masterror-derive/src/input.rs | 27 +++- tests/error_derive.rs | 48 +++++++ tests/ui/from/struct_multiple_fields.rs | 2 + tests/ui/from/variant_multiple_fields.rs | 11 +- tests/ui/from/variant_multiple_fields.stderr | 8 +- 6 files changed, 188 insertions(+), 37 deletions(-) diff --git a/masterror-derive/src/from_impl.rs b/masterror-derive/src/from_impl.rs index 87f2540..7dc873e 100644 --- a/masterror-derive/src/from_impl.rs +++ b/masterror-derive/src/from_impl.rs @@ -2,7 +2,9 @@ use proc_macro2::TokenStream; use quote::quote; use syn::Error; -use crate::input::{ErrorData, ErrorInput, Field, Fields, StructData, VariantData}; +use crate::input::{ + ErrorData, ErrorInput, Field, Fields, StructData, VariantData, is_option_type +}; pub fn expand(input: &ErrorInput) -> Result, Error> { let mut impls = Vec::new(); @@ -34,19 +36,7 @@ fn struct_from_impl( let ty = &field.ty; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let constructor = match &data.fields { - Fields::Named(_) => { - let field_ident = field.ident.clone().expect("named field"); - quote! { Self { #field_ident: value } } - } - Fields::Unnamed(_) => quote! { Self(value) }, - Fields::Unit => { - return Err(Error::new( - field.span, - "#[from] is not supported on unit structs" - )); - } - }; + let constructor = struct_constructor(&data.fields, field)?; Ok(quote! { impl #impl_generics core::convert::From<#ty> for #ident #ty_generics #where_clause { @@ -67,19 +57,7 @@ fn enum_from_impl( let variant_ident = &variant.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - let constructor = match &variant.fields { - Fields::Named(_) => { - let field_ident = field.ident.clone().expect("named field"); - quote! { Self::#variant_ident { #field_ident: value } } - } - Fields::Unnamed(_) => quote! { Self::#variant_ident(value) }, - Fields::Unit => { - return Err(Error::new( - field.span, - "#[from] is not supported on unit variants" - )); - } - }; + let constructor = variant_constructor(variant_ident, &variant.fields, field)?; Ok(quote! { impl #impl_generics core::convert::From<#ty> for #ident #ty_generics #where_clause { @@ -89,3 +67,100 @@ fn enum_from_impl( } }) } + +fn struct_constructor(fields: &Fields, from_field: &Field) -> Result { + match fields { + Fields::Named(named) => { + let mut initializers = Vec::new(); + for field in named { + let field_ident = field.ident.clone().expect("named field"); + let value = field_value_expr(field, from_field)?; + initializers.push(quote! { #field_ident: #value }); + } + Ok(quote! { Self { #(#initializers),* } }) + } + Fields::Unnamed(unnamed) => { + let mut values = Vec::new(); + for field in unnamed { + values.push(field_value_expr(field, from_field)?); + } + Ok(quote! { Self(#(#values),*) }) + } + Fields::Unit => Err(Error::new( + from_field.span, + "#[from] is not supported on unit structs" + )) + } +} + +fn variant_constructor( + variant_ident: &syn::Ident, + fields: &Fields, + from_field: &Field +) -> Result { + match fields { + Fields::Named(named) => { + let mut initializers = Vec::new(); + for field in named { + let field_ident = field.ident.clone().expect("named field"); + let value = field_value_expr(field, from_field)?; + initializers.push(quote! { #field_ident: #value }); + } + Ok(quote! { Self::#variant_ident { #(#initializers),* } }) + } + Fields::Unnamed(unnamed) => { + let mut values = Vec::new(); + for field in unnamed { + values.push(field_value_expr(field, from_field)?); + } + Ok(quote! { Self::#variant_ident(#(#values),*) }) + } + Fields::Unit => Err(Error::new( + from_field.span, + "#[from] is not supported on unit variants" + )) + } +} + +fn field_value_expr(field: &Field, from_field: &Field) -> Result { + if field.index == from_field.index { + return Ok(quote! { value }); + } + + if field.attrs.backtrace.is_some() { + return Ok(backtrace_initializer(field)); + } + + if field.attrs.source.is_some() && field.attrs.from.is_none() { + return source_initializer(field); + } + + Err(Error::new( + field.span, + "deriving From requires no fields other than source and backtrace" + )) +} + +fn source_initializer(field: &Field) -> Result { + if is_option_type(&field.ty) { + Ok(quote! { ::core::option::Option::None }) + } else { + Err(Error::new( + field.span, + "additional #[source] fields used with #[from] must be Option<_>" + )) + } +} + +fn backtrace_initializer(field: &Field) -> TokenStream { + let capture = quote! { ::std::backtrace::Backtrace::capture() }; + if is_option_type(&field.ty) { + quote! { + ::core::option::Option::Some(::core::convert::From::from(#capture)) + } + } else { + quote! { + ::core::convert::From::from(#capture) + } + } +} diff --git a/masterror-derive/src/input.rs b/masterror-derive/src/input.rs index a4b22fa..649f462 100644 --- a/masterror-derive/src/input.rs +++ b/masterror-derive/src/input.rs @@ -391,9 +391,30 @@ fn validate_from_usage(fields: &Fields, display: &DisplaySpec, errors: &mut Vec< return; } - if fields.len() > 1 - && let Some(attr) = &field.attrs.from - { + let mut has_unexpected_companions = false; + for companion in fields.iter() { + if companion.index == field.index { + continue; + } + + if companion.attrs.backtrace.is_some() { + continue; + } + + if let Some(attr) = &companion.attrs.source { + if companion.attrs.from.is_none() && !is_option_type(&companion.ty) { + errors.push(Error::new_spanned( + attr, + "additional #[source] fields used with #[from] must be Option<_>" + )); + } + continue; + } + + has_unexpected_companions = true; + } + + if has_unexpected_companions && let Some(attr) = &field.attrs.from { errors.push(Error::new_spanned( attr, "deriving From requires no fields other than source and backtrace" diff --git a/tests/error_derive.rs b/tests/error_derive.rs index 3fc6c2f..9ffb51e 100644 --- a/tests/error_derive.rs +++ b/tests/error_derive.rs @@ -1,3 +1,5 @@ +#![allow(unused_variables, non_shorthand_field_patterns)] + use std::error::Error as StdError; use masterror::Error; @@ -105,6 +107,26 @@ enum TransparentEnum { TransparentVariant(#[from] TransparentInner) } +#[derive(Debug, Error)] +#[error("{source:?}")] +struct StructFromWithBacktrace { + #[from] + source: LeafError, + #[backtrace] + trace: Option +} + +#[derive(Debug, Error)] +enum VariantFromWithBacktrace { + #[error("{source:?}")] + WithTrace { + #[from] + source: LeafError, + #[backtrace] + trace: Option + } +} + #[test] fn named_struct_display_and_source() { let err = NamedError { @@ -220,3 +242,29 @@ fn transparent_enum_variant_from_impl() { Some(String::from("leaf failure")) ); } + +#[test] +fn struct_from_with_backtrace_field_captures_trace() { + let err = StructFromWithBacktrace::from(LeafError); + assert!(err.trace.is_some()); + assert_eq!( + StdError::source(&err).map(|err| err.to_string()), + Some(String::from("leaf failure")) + ); +} + +#[test] +fn enum_from_with_backtrace_field_captures_trace() { + let err = VariantFromWithBacktrace::from(LeafError); + match &err { + VariantFromWithBacktrace::WithTrace { + trace, .. + } => { + assert!(trace.is_some()); + } + } + assert_eq!( + StdError::source(&err).map(|err| err.to_string()), + Some(String::from("leaf failure")) + ); +} diff --git a/tests/ui/from/struct_multiple_fields.rs b/tests/ui/from/struct_multiple_fields.rs index a545ce7..c8c2d2f 100644 --- a/tests/ui/from/struct_multiple_fields.rs +++ b/tests/ui/from/struct_multiple_fields.rs @@ -5,6 +5,8 @@ use masterror::Error; struct BadStruct { #[from] left: DummyError, + #[backtrace] + trace: Option, right: DummyError, } diff --git a/tests/ui/from/variant_multiple_fields.rs b/tests/ui/from/variant_multiple_fields.rs index 3d35295..8a05e32 100644 --- a/tests/ui/from/variant_multiple_fields.rs +++ b/tests/ui/from/variant_multiple_fields.rs @@ -2,9 +2,14 @@ use masterror::Error; #[derive(Debug, Error)] enum BadEnum { - #[error("{0} - {1}")] - #[from] - Two(#[source] DummyError, DummyError), + #[error("{source:?} - {extra:?}")] + WithExtra { + #[from] + source: DummyError, + #[backtrace] + trace: Option, + extra: DummyError + } } #[derive(Debug, Error)] diff --git a/tests/ui/from/variant_multiple_fields.stderr b/tests/ui/from/variant_multiple_fields.stderr index e78cc01..fc68934 100644 --- a/tests/ui/from/variant_multiple_fields.stderr +++ b/tests/ui/from/variant_multiple_fields.stderr @@ -1,5 +1,5 @@ -error: not expected here; the #[from] attribute belongs on a specific field - --> tests/ui/from/variant_multiple_fields.rs:6:5 +error: deriving From requires no fields other than source and backtrace + --> tests/ui/from/variant_multiple_fields.rs:7:9 | -6 | #[from] - | ^^^^^^^ +7 | #[from] + | ^^^^^^^