From d617d933f5a050e7e127a1cf9dcd46bf00415dc8 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Sun, 21 Sep 2025 23:13:57 -0600 Subject: [PATCH 01/20] Fix GitHub Actions: wrong Cargo.toml path --- .github/workflows/publish.yml | 2 +- .github/workflows/testToStable.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7538490..9d27bc8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,7 +39,7 @@ jobs: - name: Get crate version id: get_crate_info run: | - CRATE_VERSION=$(grep '^version = ' Cargo.toml | head -n 1 | cut -d '"' -f 2) + CRATE_VERSION=$(grep '^version = ' impl/Cargo.toml | head -n 1 | cut -d '"' -f 2) echo "Crate error-stack-macros2" echo "Crate Version: $CRATE_VERSION" echo "crate_version=$CRATE_VERSION" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/testToStable.yml b/.github/workflows/testToStable.yml index 243c95b..3a74593 100644 --- a/.github/workflows/testToStable.yml +++ b/.github/workflows/testToStable.yml @@ -44,7 +44,7 @@ jobs: - name: Get crate version id: get_crate_info run: | - CRATE_VERSION=$(grep '^version = ' Cargo.toml | head -n 1 | cut -d '"' -f 2) + CRATE_VERSION=$(grep '^version = ' impl/Cargo.toml | head -n 1 | cut -d '"' -f 2) echo "Crate error-stack-macros2" echo "Crate Version: $CRATE_VERSION" echo "crate_version=$CRATE_VERSION" >> "$GITHUB_OUTPUT" From f7df30e609b86c5f2daf04b16916717da8f544f4 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 22 Sep 2025 14:48:06 -0600 Subject: [PATCH 02/20] Update GitHub labels --- .github/labels.yml | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index 4f80b29..588ba8a 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -1,11 +1,19 @@ - name: ๐Ÿ’ฃ Breaking change - description: Unintended change that breaks existing behavior + description: Change that modifies existing behavior color: "f79f34" - name: ๐Ÿž Bug description: Not working as intended color: "d73a4a" +- name: โš™๏ธ Configuration + description: Changes to configuration files + color: "7057ff" + +- name: ๐Ÿ“ฆ Dependencies + description: PRs that update production dependencies + color: "0d7bb5" + - name: ๐Ÿ’ป Dev Dependencies description: PRs that update development dependencies color: "155e8c" @@ -74,6 +82,10 @@ description: Unit testing is needed color: "c23675" +- name: ๐Ÿ”„ New release + description: This PR into `stable` marks a new release + color: "f9c74f" + - name: ๐Ÿฐ Nice to have description: Unplanned behavior, but useful color: "c5def5" @@ -82,6 +94,10 @@ description: Development on this issue has been suspended color: "d4c5f9" +- name: ๐Ÿš€ Performance + description: Improvements to performance + color: "fbca04" + - name: โœ… Ready to merge description: This PR is ready to be merged color: "0ff40b" @@ -90,10 +106,6 @@ description: Preparing for a new release (version bump, release notes, etc.) color: "f9c74f" -- name: ๐Ÿ”„ New release - description: This PR into `stable` marks a new release - color: "f9c74f" - - name: ๐Ÿงช Tests description: Improvements or additions to unit tests color: "9f2d60" From 49f335fb95f27cf0d06dce1ab9f0b98937ff60dd Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 22 Sep 2025 14:48:53 -0600 Subject: [PATCH 03/20] Change Dependabot schedule to daily --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 53f8242..ba5e8d8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,4 +3,4 @@ updates: - package-ecosystem: "cargo" directory: "/" schedule: - interval: "weekly" + interval: daily From 1db9e75b0c71940d62935e7309b47241f15eb281 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:26:05 +0000 Subject: [PATCH 04/20] Bump regex from 1.11.2 to 1.11.3 Bumps [regex](https://github.com/rust-lang/regex) from 1.11.2 to 1.11.3. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.11.2...1.11.3) --- updated-dependencies: - dependency-name: regex dependency-version: 1.11.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- impl/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 36dc187..a9d4ae1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,9 +72,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" dependencies = [ "aho-corasick", "memchr", @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 563e35b..fd3d992 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -32,7 +32,7 @@ panic = "warn" [dependencies] proc-macro2 = "1.0.101" quote = "1.0.40" -regex = "1.11.2" +regex = "1.11.3" syn = { version = "2.0.106", features = [] } [dev-dependencies] From 02fafecf955b7df14716156effbe03f2728b42b1 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Fri, 26 Sep 2025 17:29:41 -0600 Subject: [PATCH 05/20] dependabot.yml: Use existing labels and assign to me (LuisFerLCC) --- .github/dependabot.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ba5e8d8..013bcd6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,26 @@ version: 2 + updates: + # Production dependencies (e.g., [dependencies], [build-dependencies]) - package-ecosystem: "cargo" directory: "/" schedule: interval: daily + allow: + - dependency-type: "production" + labels: + - "๐Ÿ“ฆ Dependencies" + assignees: + - LuisFerLCC + + # Development dependencies (e.g., [dev-dependencies]) + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: daily + allow: + - dependency-type: "development" + labels: + - "๐Ÿ’ป Dev Dependencies" + assignees: + - LuisFerLCC From 19bae9b829327d940c79c54b813f455b060f31d3 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Fri, 26 Sep 2025 17:35:01 -0600 Subject: [PATCH 06/20] dependabot.yml: Restricted dev dependencies to `impl` directory --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 013bcd6..7b506cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,7 +15,7 @@ updates: # Development dependencies (e.g., [dev-dependencies]) - package-ecosystem: "cargo" - directory: "/" + directory: "impl" schedule: interval: daily allow: From e554aaf3e47a2567f2ed25705159da0697b76b32 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Sat, 27 Sep 2025 23:53:34 -0600 Subject: [PATCH 07/20] Refactor `FormatData` (now `TypeData`) to eliminate unnecessary allocation and repeated operations + rename types --- impl/src/lib.rs | 2 - impl/src/types/fmt/input.rs | 24 +-- impl/src/types/fmt/mod.rs | 363 ++++++++++++++++++++++++------------ impl/src/types/mod.rs | 10 +- impl/src/util/mod.rs | 1 - impl/src/util/traits.rs | 27 --- 6 files changed, 261 insertions(+), 166 deletions(-) delete mode 100644 impl/src/util/mod.rs delete mode 100644 impl/src/util/traits.rs diff --git a/impl/src/lib.rs b/impl/src/lib.rs index ce2de47..ea5179e 100644 --- a/impl/src/lib.rs +++ b/impl/src/lib.rs @@ -114,8 +114,6 @@ use syn::parse_macro_input; mod types; use types::ErrorStackDeriveInput; -mod util; - /// Derive macro for the [`Error`] trait that implements the best practices for /// [`error-stack`]. /// diff --git a/impl/src/types/fmt/input.rs b/impl/src/types/fmt/input.rs index 911e1ac..279634f 100644 --- a/impl/src/types/fmt/input.rs +++ b/impl/src/types/fmt/input.rs @@ -75,19 +75,19 @@ impl ToTokens for StructFormatInput { } } -pub(crate) struct EnumVariantFormatInput { +pub(crate) struct VariantFormatInput { lit_str: LitStr, args: Punctuated, } #[cfg(test)] -impl Debug for EnumVariantFormatInput { +impl Debug for VariantFormatInput { fn fmt(&self, _: &mut Formatter<'_>) -> fmt::Result { Ok(()) } } -impl Parse for EnumVariantFormatInput { +impl Parse for VariantFormatInput { fn parse(input: ParseStream) -> syn::Result { let mut lit_str: LitStr = input.parse()?; @@ -127,7 +127,7 @@ impl Parse for EnumVariantFormatInput { } } -impl ToTokens for EnumVariantFormatInput { +impl ToTokens for VariantFormatInput { fn to_tokens(&self, tokens: &mut TokenStream2) { let Self { lit_str, args } = self; @@ -188,9 +188,9 @@ mod tests { #[test] fn enum_variant_format_input_requires_initial_lit_str() { - let empty_stream_res = syn::parse2::(quote! {}); + let empty_stream_res = syn::parse2::(quote! {}); let err = empty_stream_res.expect_err( - "empty stream was parsed successfully as EnumVariantFormatInput", + "empty stream was parsed successfully as VariantFormatInput", ); assert_eq!( err.to_string(), @@ -201,9 +201,9 @@ mod tests { #[test] fn enum_variant_format_input_requires_initial_arg_to_be_lit_str() { let empty_stream_res = - syn::parse2::(quote! { true }); + syn::parse2::(quote! { true }); let err = empty_stream_res.expect_err( - "stream `true` was parsed successfully as EnumVariantFormatInput", + "stream `true` was parsed successfully as VariantFormatInput", ); assert_eq!(err.to_string(), "expected string literal"); } @@ -211,9 +211,9 @@ mod tests { #[test] fn enum_variant_format_input_rejects_unexpected_token_after_lit_str() { let empty_stream_res = - syn::parse2::(quote! { "format string" 5 }); + syn::parse2::(quote! { "format string" 5 }); let err = empty_stream_res.expect_err( - "stream `\"format string\" 5` was parsed successfully as EnumVariantFormatInput", + "stream `\"format string\" 5` was parsed successfully as VariantFormatInput", ); assert_eq!(err.to_string(), "unexpected token after string literal"); } @@ -221,9 +221,9 @@ mod tests { #[test] fn enum_variant_format_input_parses_lit_str_with_trailing_comma() { let empty_stream_res = - syn::parse2::(quote! { "format string", }); + syn::parse2::(quote! { "format string", }); let format_input = empty_stream_res.expect( - "stream `\"format string\",` could not be parsed as EnumVariantFormatInput", + "stream `\"format string\",` could not be parsed as VariantFormatInput", ); assert_eq!(format_input.lit_str.value(), "format string"); } diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index 4b5c597..bb081b6 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -1,65 +1,54 @@ +use std::convert::Infallible; #[cfg(test)] use std::fmt::{self, Debug, Formatter}; -use proc_macro2::TokenStream as TokenStream2; +use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{ToTokens, quote}; use syn::{ - Attribute, Data, DeriveInput, Fields, LitStr, Meta, Variant, parse::Parse, - spanned::Spanned, + Attribute, Data, Fields, Ident, LitStr, Meta, Variant, parse::Parse, + punctuated::Punctuated, spanned::Spanned, token::Comma, }; pub(crate) mod input; -use input::{EnumVariantFormatInput, StructFormatInput}; +use input::{StructFormatInput, VariantFormatInput}; -use crate::util::traits::IteratorExt; - -pub(crate) enum FormatData { +pub(crate) enum TypeData { Struct { display_input: StructFormatInput, }, Enum { default_display_input: Option, - variant_display_inputs: Vec<(Variant, EnumVariantFormatInput)>, + variant_display_inputs: Vec, }, EmptyEnum, } -impl FormatData { - pub(crate) fn new(derive_input: &DeriveInput) -> syn::Result { - let ident = &derive_input.ident; - - match &derive_input.data { +impl TypeData { + pub(crate) fn new( + input_data: Data, + attrs: Vec, + ident_span: Span, + ) -> syn::Result { + match input_data { Data::Struct(_) => { - let display_attr = Self::get_display_attr(&derive_input.attrs) - .ok_or_else(|| syn::Error::new(ident.span(), "missing `display` attribute for struct with `#[derive(Error)]`"))?; + let display_attr = Self::get_display_attr(attrs) + .ok_or_else(|| syn::Error::new(ident_span, "missing `display` attribute for struct with `#[derive(Error)]`"))?; let display_input = Self::get_format_input(display_attr)?; Ok(Self::Struct { display_input }) } Data::Enum(data) => { - let variants = &data.variants; + let variants = data.variants; if variants.is_empty() { return Ok(Self::EmptyEnum); } - let default_display_attr = - Self::get_display_attr(&derive_input.attrs); - - let variant_display_attrs = variants.iter().map(|variant| { - (variant, Self::get_display_attr(&variant.attrs)) - }); - - let variant_display_inputs_res = variant_display_attrs - .clone() - .filter_map(|(variant, attr)| Some((variant, attr?))) - .map(|(variant, attr)| { - Self::get_format_input(attr) - .map(|input| (variant.clone(), input)) - }) - .collect_vec_and_combine_syn_errors(); + let default_display_attr = Self::get_display_attr(attrs); + let variant_display_inputs = + Self::collect_valid_variant_states(variants)?; if let Some(attr) = default_display_attr { let default_display_input = @@ -67,73 +56,85 @@ impl FormatData { return Ok(Self::Enum { default_display_input, - variant_display_inputs: variant_display_inputs_res?, + variant_display_inputs: variant_display_inputs + .into_iter() + .filter_map(|state| state.data()) + .collect(), }); + }; + + let (valid_variants, none_spans) = + Self::separate_existing_variant_states( + variant_display_inputs.into_iter(), + ); + + if valid_variants.is_empty() { + return Err(syn::Error::new( + ident_span, + "missing `display` attribute for enum with `#[derive(Error)]`\nadd a `display` attribute to at least the whole enum or to all of its variants", + )); } - match variant_display_inputs_res { - Ok(inputs) => { - if inputs.is_empty() { - return Err(syn::Error::new( - ident.span(), - "missing `display` attribute for enum with `#[derive(Error)]`\nadd a `display` attribute to at least the whole enum or to all of its variants", - )); - } - - let unformatted_variants_error = variant_display_attrs - .filter_map(|(variant, attr)| match attr { - Some(_) => None, - None => Some(syn::Error::new( - variant.span(), - "missing `display` attribute for variant in enum with `#[derive(Error)]`\nadd a `display` attribute either to the whole enum (as a default) or to the remaining variants" - )), - }) - .reduce(|mut acc, next| { - acc.combine(next); - acc - }); - - if let Some(err) = unformatted_variants_error { - return Err(err); - } - - Ok(Self::Enum { - default_display_input: None, - variant_display_inputs: inputs, - }) - } - - Err(err) => Err(err), + if !none_spans.is_empty() { + #[allow(clippy::unwrap_used)] + return Err(none_spans + .into_iter() + .map(|span| { + syn::Error::new( + span, + "missing `display` attribute for variant in enum with `#[derive(Error)]`\nadd a `display` attribute either to the whole enum (as a default) or to the remaining variants" + ) + }).reduce(|mut err, err2| { + err.combine(err2); + err + }).unwrap()); } + + Ok(Self::Enum { + default_display_input: None, + variant_display_inputs: valid_variants, + }) } _ => Err(syn::Error::new( - ident.span(), + ident_span, "`#[derive(Error)]` only supports structs and enums", )), } } - pub(crate) fn get_display_attr(attrs: &[Attribute]) -> Option<&Attribute> { - attrs.iter().find(|attr| attr.path().is_ident("display")) + fn get_display_attr(attrs: Vec) -> Option { + attrs + .into_iter() + .find(|attr| attr.path().is_ident("display")) } - pub(crate) fn get_format_input( - display_attr: &Attribute, - ) -> syn::Result + fn get_format_input(display_attr: Attribute) -> syn::Result where T: Parse, { - if let Meta::List(meta) = &display_attr.meta { - return syn::parse(meta.tokens.clone().into()).map_err(|err| { - if err.to_string() - == "unexpected end of input, expected string literal" - { - syn::Error::new(meta.span(), "unexpected empty `display` attribute, expected string literal") - } else { - err + if let Meta::List(meta) = display_attr.meta { + let meta_span = meta.span(); + + let parse_res = syn::parse::(meta.tokens.into()); + + match parse_res { + Ok(input) => return Ok(input), + Err(err) => { + return Err( + if err.to_string() + == "unexpected end of input, expected string literal" + { + syn::Error::new( + meta_span, + "unexpected empty `display` attribute, expected string literal", + ) + } else { + err + }, + ); } - }); + } } Err(syn::Error::new( @@ -141,16 +142,78 @@ impl FormatData { "expected `display` to be a list attribute: `#[display(\"template...\")]`", )) } + + fn collect_valid_variant_states( + variants: Punctuated, + ) -> Result, syn::Error> { + let mut variant_states_iter = variants.into_iter().map(|variant| { + let variant_span = variant.span(); + + use VariantState as VS; + match Self::get_display_attr(variant.attrs) { + None => VS::None(variant_span), + Some(attr) => match Self::get_format_input(attr) { + Ok(input) => VS::Valid(VariantData { + ident: variant.ident, + fields: variant.fields, + display_input: input, + }), + Err(err) => VS::Invalid(err), + }, + } + }); + + let mut vec = Vec::new(); + + while let Some(state) = variant_states_iter.next() { + use VariantState as VS; + match state { + VS::None(span) => vec.push(VS::None(span)), + VS::Valid(data) => vec.push(VS::Valid(data)), + VS::Invalid(mut err) => { + while let Some(VS::Invalid(err2)) = + variant_states_iter.next() + { + err.combine(err2); + } + + return Err(err); + } + } + } + + Ok(vec) + } + + fn separate_existing_variant_states( + states_iter: I, + ) -> (Vec, Vec) + where + I: Iterator, + { + let mut valid_variants = Vec::new(); + let mut none_spans = Vec::new(); + + for state in states_iter { + use VariantState as VS; + match state { + VS::Valid(data) => valid_variants.push(data), + VS::None(span) => none_spans.push(span), + } + } + + (valid_variants, none_spans) + } } #[cfg(test)] -impl Debug for FormatData { +impl Debug for TypeData { fn fmt(&self, _: &mut Formatter<'_>) -> fmt::Result { Ok(()) } } -impl ToTokens for FormatData { +impl ToTokens for TypeData { fn to_tokens(&self, tokens: &mut TokenStream2) { match self { Self::Struct { display_input } => { @@ -165,28 +228,8 @@ impl ToTokens for FormatData { } => { let branches = variant_display_inputs .iter() - .map(|(variant, format_input)| { - let ident = &variant.ident; - - let fields = &variant.fields; - let field_idents = fields - .iter() - .enumerate() - .map(|(i, field)| - field.ident.clone().unwrap_or_else(|| - syn::Ident::new( - &format!("_field{}", i), - field.span()))); - - let field_tokens = match &variant.fields { - Fields::Named(_) => quote! { { #(#field_idents),* } }, - Fields::Unnamed(_) => quote! { ( #(#field_idents),* ) }, - Fields::Unit => TokenStream2::new() - }; - - quote! { - Self::#ident #field_tokens => ::core::write!(f, #format_input) - } + .map(|variant| { + quote! { #variant } }) .chain(default_display_input.as_ref().map(|lit_str| { quote! { @@ -210,20 +253,73 @@ impl ToTokens for FormatData { } } +pub(crate) enum VariantState { + Valid(VariantData), + Invalid(E), + None(Span), +} + +impl VariantState { + fn data(self) -> Option { + match self { + Self::Valid(data) => Some(data), + _ => None, + } + } +} + +pub(crate) type ValidVariantState = VariantState; + +pub(crate) struct VariantData { + ident: Ident, + fields: Fields, + display_input: VariantFormatInput, +} + +impl ToTokens for VariantData { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ident = &self.ident; + let fields = &self.fields; + let display_input = &self.display_input; + + let field_idents = fields.iter().enumerate().map(|(i, field)| { + field.ident.clone().unwrap_or_else(|| { + syn::Ident::new(&format!("_field{}", i), field.span()) + }) + }); + + let field_tokens = match fields { + Fields::Named(_) => quote! { { #(#field_idents),* } }, + Fields::Unnamed(_) => quote! { ( #(#field_idents),* ) }, + Fields::Unit => TokenStream2::new(), + }; + + tokens.extend(quote! { + Self::#ident #field_tokens => ::core::write!(f, #display_input) + }) + } +} + #[cfg(test)] #[allow(clippy::expect_used)] mod tests { use super::*; use quote::quote; + use syn::DeriveInput; #[test] - fn struct_format_data_requires_display_attr() { + fn struct_data_requires_display_attr() { let derive_input = syn::parse2::(quote! { struct CustomType; }) .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream without display attr was parsed successfully as FormatData", + let err = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream without display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -232,13 +328,18 @@ mod tests { } #[test] - fn struct_format_data_requires_list_form_for_display_attr() { + fn struct_data_requires_list_form_for_display_attr() { let derive_input = syn::parse2::( quote! { #[display] struct CustomType; }, ) .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with path display attr was parsed successfully as FormatData", + let err = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream with path display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -247,12 +348,17 @@ mod tests { } #[test] - fn enum_format_data_requires_display_attr() { + fn enum_data_requires_display_attr() { let derive_input = syn::parse2::(quote! { enum CustomType { One, Two } }) .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream without display attr was parsed successfully as FormatData", + let err = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream without display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -261,13 +367,18 @@ mod tests { } #[test] - fn enum_format_data_requires_list_form_for_display_attr() { + fn enum_data_requires_list_form_for_display_attr() { let derive_input = syn::parse2::( quote! { #[display] enum CustomType { One, Two } }, ) .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with path display attr was parsed successfully as FormatData", + let err = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream with path display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -276,7 +387,7 @@ mod tests { } #[test] - fn enum_format_data_requires_list_form_for_display_attr_on_every_variant() { + fn enum_data_requires_list_form_for_display_attr_on_every_variant() { let derive_input = syn::parse2::(quote! { enum CustomType { #[display] @@ -286,8 +397,13 @@ mod tests { } }) .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with path display attr was parsed successfully as FormatData", + let err = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream with path display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -301,8 +417,13 @@ mod tests { quote! { union CustomType { f1: u32, f2: f32 } }, ) .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with union type was parsed successfully as FormatData", + let err = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream with union type was parsed successfully as TypeData", ); assert_eq!( err.to_string(), diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index d706385..8ad44b6 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -6,18 +6,22 @@ use syn::{ }; mod fmt; -use fmt::FormatData; +use fmt::TypeData; pub(crate) struct ErrorStackDeriveInput { ident: Ident, - display_data: FormatData, + display_data: TypeData, } impl Parse for ErrorStackDeriveInput { fn parse(input: ParseStream) -> syn::Result { let derive_input: DeriveInput = input.parse()?; - let display_data = FormatData::new(&derive_input)?; + let display_data = TypeData::new( + derive_input.data, + derive_input.attrs, + derive_input.ident.span(), + )?; let ident = derive_input.ident; diff --git a/impl/src/util/mod.rs b/impl/src/util/mod.rs deleted file mode 100644 index a03027a..0000000 --- a/impl/src/util/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod traits; diff --git a/impl/src/util/traits.rs b/impl/src/util/traits.rs deleted file mode 100644 index 6671094..0000000 --- a/impl/src/util/traits.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub(crate) trait IteratorExt: - Sized + Iterator> -{ - fn collect_vec_and_combine_syn_errors(mut self) -> syn::Result> { - let mut vec = Vec::new(); - - while let Some(res) = self.next() { - match res { - Ok(item) => { - vec.push(item); - } - - Err(mut err) => { - while let Some(Err(err2)) = self.next() { - err.combine(err2); - } - - return Err(err); - } - } - } - - Ok(vec) - } -} - -impl IteratorExt for I where I: Sized + Iterator> {} From a8cbe4e32e09da01cf4ef35225800b4228203dee Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Sun, 28 Sep 2025 00:27:00 -0600 Subject: [PATCH 08/20] Add calls to `std::mem::drop` to deallocate each resource as soon as possible --- impl/src/types/fmt/input.rs | 24 ++++++++++++++++---- impl/src/types/fmt/mod.rs | 45 +++++++++++++++++++++++++++++++------ impl/src/types/mod.rs | 3 +++ 3 files changed, 61 insertions(+), 11 deletions(-) diff --git a/impl/src/types/fmt/input.rs b/impl/src/types/fmt/input.rs index 279634f..34bb7d5 100644 --- a/impl/src/types/fmt/input.rs +++ b/impl/src/types/fmt/input.rs @@ -25,7 +25,7 @@ impl Debug for StructFormatInput { impl Parse for StructFormatInput { fn parse(input: ParseStream) -> syn::Result { - let mut lit_str: LitStr = input.parse()?; + let lit_str: LitStr = input.parse()?; let comma: Option = input.parse()?; if comma.is_none() && !input.is_empty() { @@ -40,9 +40,14 @@ impl Parse for StructFormatInput { let mut fmt_string = lit_str.value(); let mut args = Punctuated::new(); + let lit_str_span = lit_str.span(); + drop(lit_str); + while let Some(captures) = regex.captures(&fmt_string) { #[allow(clippy::unwrap_used)] let group = captures.get(1).unwrap(); + drop(captures); + let inline_arg_str = group.as_str(); let arg_tokens = if inline_arg_str.parse::().is_ok() { @@ -59,7 +64,10 @@ impl Parse for StructFormatInput { fmt_string.replace_range(group.range(), ""); } - lit_str = LitStr::new(&fmt_string, lit_str.span()); + drop(regex); + + let lit_str = LitStr::new(&fmt_string, lit_str_span); + drop(fmt_string); Ok(Self { lit_str, args }) } @@ -89,7 +97,7 @@ impl Debug for VariantFormatInput { impl Parse for VariantFormatInput { fn parse(input: ParseStream) -> syn::Result { - let mut lit_str: LitStr = input.parse()?; + let lit_str: LitStr = input.parse()?; let comma: Option = input.parse()?; if comma.is_none() && !input.is_empty() { @@ -104,9 +112,14 @@ impl Parse for VariantFormatInput { let mut fmt_string = lit_str.value(); let mut args = Punctuated::new(); + let lit_str_span = lit_str.span(); + drop(lit_str); + while let Some(captures) = regex.captures(&fmt_string) { #[allow(clippy::unwrap_used)] let group = captures.get(1).unwrap(); + drop(captures); + let inline_arg_str = group.as_str(); let ident_str = if inline_arg_str.parse::().is_ok() { @@ -121,7 +134,10 @@ impl Parse for VariantFormatInput { fmt_string.replace_range(group.range(), ""); } - lit_str = LitStr::new(&fmt_string, lit_str.span()); + drop(regex); + + let lit_str = LitStr::new(&fmt_string, lit_str_span); + drop(fmt_string); Ok(Self { lit_str, args }) } diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index bb081b6..a98ad1b 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -33,6 +33,8 @@ impl TypeData { ) -> syn::Result { match input_data { Data::Struct(_) => { + drop(input_data); + let display_attr = Self::get_display_attr(attrs) .ok_or_else(|| syn::Error::new(ident_span, "missing `display` attribute for struct with `#[derive(Error)]`"))?; let display_input = Self::get_format_input(display_attr)?; @@ -43,6 +45,7 @@ impl TypeData { Data::Enum(data) => { let variants = data.variants; if variants.is_empty() { + drop(variants); return Ok(Self::EmptyEnum); } @@ -63,12 +66,16 @@ impl TypeData { }); }; + drop(default_display_attr); + let (valid_variants, none_spans) = Self::separate_existing_variant_states( variant_display_inputs.into_iter(), ); if valid_variants.is_empty() { + drop(valid_variants); + drop(none_spans); return Err(syn::Error::new( ident_span, "missing `display` attribute for enum with `#[derive(Error)]`\nadd a `display` attribute to at least the whole enum or to all of its variants", @@ -76,6 +83,8 @@ impl TypeData { } if !none_spans.is_empty() { + drop(valid_variants); + #[allow(clippy::unwrap_used)] return Err(none_spans .into_iter() @@ -90,16 +99,23 @@ impl TypeData { }).unwrap()); } + drop(none_spans); + Ok(Self::Enum { default_display_input: None, variant_display_inputs: valid_variants, }) } - _ => Err(syn::Error::new( - ident_span, - "`#[derive(Error)]` only supports structs and enums", - )), + _ => { + drop(input_data); + drop(attrs); + + Err(syn::Error::new( + ident_span, + "`#[derive(Error)]` only supports structs and enums", + )) + } } } @@ -113,8 +129,11 @@ impl TypeData { where T: Parse, { + let attr_span = display_attr.span(); + if let Meta::List(meta) = display_attr.meta { let meta_span = meta.span(); + drop(meta.path); let parse_res = syn::parse::(meta.tokens.into()); @@ -125,6 +144,8 @@ impl TypeData { if err.to_string() == "unexpected end of input, expected string literal" { + drop(err); + syn::Error::new( meta_span, "unexpected empty `display` attribute, expected string literal", @@ -137,8 +158,10 @@ impl TypeData { } } + drop(display_attr); + Err(syn::Error::new( - display_attr.span(), + attr_span, "expected `display` to be a list attribute: `#[display(\"template...\")]`", )) } @@ -182,6 +205,8 @@ impl TypeData { } } + drop(variant_states_iter); + Ok(vec) } @@ -263,7 +288,10 @@ impl VariantState { fn data(self) -> Option { match self { Self::Valid(data) => Some(data), - _ => None, + _ => { + drop(self); + None + } } } } @@ -291,7 +319,10 @@ impl ToTokens for VariantData { let field_tokens = match fields { Fields::Named(_) => quote! { { #(#field_idents),* } }, Fields::Unnamed(_) => quote! { ( #(#field_idents),* ) }, - Fields::Unit => TokenStream2::new(), + Fields::Unit => { + drop(field_idents); + TokenStream2::new() + } }; tokens.extend(quote! { diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index 8ad44b6..95f2a11 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -17,6 +17,9 @@ impl Parse for ErrorStackDeriveInput { fn parse(input: ParseStream) -> syn::Result { let derive_input: DeriveInput = input.parse()?; + drop(derive_input.generics); + drop(derive_input.vis); + let display_data = TypeData::new( derive_input.data, derive_input.attrs, From e2a0b6b33eefccd56ce8b1890b29490aefb29852 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 15:42:32 -0600 Subject: [PATCH 09/20] Remove unnecessary `pub(crate)` --- impl/src/types/fmt/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index a98ad1b..3abfc5a 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -9,7 +9,7 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::Comma, }; -pub(crate) mod input; +mod input; use input::{StructFormatInput, VariantFormatInput}; pub(crate) enum TypeData { @@ -278,7 +278,7 @@ impl ToTokens for TypeData { } } -pub(crate) enum VariantState { +enum VariantState { Valid(VariantData), Invalid(E), None(Span), @@ -296,7 +296,7 @@ impl VariantState { } } -pub(crate) type ValidVariantState = VariantState; +type ValidVariantState = VariantState; pub(crate) struct VariantData { ident: Ident, From 4c63deb0380be9fb12e346b9f76b2fd67ce3930c Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 16:56:37 -0600 Subject: [PATCH 10/20] Store non-display attributes in `ErrorStackDeriveInput` and apply to output --- impl/src/types/fmt/mod.rs | 47 ++++++++++++++++++++------------------- impl/src/types/mod.rs | 12 ++++++++-- impl/src/types/util.rs | 10 +++++++++ 3 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 impl/src/types/util.rs diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index 3abfc5a..08c41a1 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -12,6 +12,8 @@ use syn::{ mod input; use input::{StructFormatInput, VariantFormatInput}; +use super::util; + pub(crate) enum TypeData { Struct { display_input: StructFormatInput, @@ -28,14 +30,16 @@ pub(crate) enum TypeData { impl TypeData { pub(crate) fn new( input_data: Data, - attrs: Vec, + attrs: &mut Vec, ident_span: Span, ) -> syn::Result { + let default_display_attr = util::take_display_attr(attrs); + match input_data { Data::Struct(_) => { drop(input_data); - let display_attr = Self::get_display_attr(attrs) + let display_attr = default_display_attr .ok_or_else(|| syn::Error::new(ident_span, "missing `display` attribute for struct with `#[derive(Error)]`"))?; let display_input = Self::get_format_input(display_attr)?; @@ -49,7 +53,6 @@ impl TypeData { return Ok(Self::EmptyEnum); } - let default_display_attr = Self::get_display_attr(attrs); let variant_display_inputs = Self::collect_valid_variant_states(variants)?; @@ -109,7 +112,7 @@ impl TypeData { _ => { drop(input_data); - drop(attrs); + drop(default_display_attr); Err(syn::Error::new( ident_span, @@ -119,12 +122,6 @@ impl TypeData { } } - fn get_display_attr(attrs: Vec) -> Option { - attrs - .into_iter() - .find(|attr| attr.path().is_ident("display")) - } - fn get_format_input(display_attr: Attribute) -> syn::Result where T: Parse, @@ -172,8 +169,12 @@ impl TypeData { let mut variant_states_iter = variants.into_iter().map(|variant| { let variant_span = variant.span(); + let mut attrs = variant.attrs; + let display_attr = util::take_display_attr(&mut attrs); + drop(attrs); + use VariantState as VS; - match Self::get_display_attr(variant.attrs) { + match display_attr { None => VS::None(variant_span), Some(attr) => match Self::get_format_input(attr) { Ok(input) => VS::Valid(VariantData { @@ -341,12 +342,12 @@ mod tests { #[test] fn struct_data_requires_display_attr() { - let derive_input = + let mut derive_input = syn::parse2::(quote! { struct CustomType; }) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, - derive_input.attrs, + &mut derive_input.attrs, derive_input.ident.span(), ) .expect_err( @@ -360,13 +361,13 @@ mod tests { #[test] fn struct_data_requires_list_form_for_display_attr() { - let derive_input = syn::parse2::( + let mut derive_input = syn::parse2::( quote! { #[display] struct CustomType; }, ) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, - derive_input.attrs, + &mut derive_input.attrs, derive_input.ident.span(), ) .expect_err( @@ -380,12 +381,12 @@ mod tests { #[test] fn enum_data_requires_display_attr() { - let derive_input = + let mut derive_input = syn::parse2::(quote! { enum CustomType { One, Two } }) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, - derive_input.attrs, + &mut derive_input.attrs, derive_input.ident.span(), ) .expect_err( @@ -399,13 +400,13 @@ mod tests { #[test] fn enum_data_requires_list_form_for_display_attr() { - let derive_input = syn::parse2::( + let mut derive_input = syn::parse2::( quote! { #[display] enum CustomType { One, Two } }, ) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, - derive_input.attrs, + &mut derive_input.attrs, derive_input.ident.span(), ) .expect_err( @@ -419,7 +420,7 @@ mod tests { #[test] fn enum_data_requires_list_form_for_display_attr_on_every_variant() { - let derive_input = syn::parse2::(quote! { + let mut derive_input = syn::parse2::(quote! { enum CustomType { #[display] One, @@ -430,7 +431,7 @@ mod tests { .expect("malformed test stream"); let err = TypeData::new( derive_input.data, - derive_input.attrs, + &mut derive_input.attrs, derive_input.ident.span(), ) .expect_err( @@ -444,13 +445,13 @@ mod tests { #[test] fn union_type_is_rejected() { - let derive_input = syn::parse2::( + let mut derive_input = syn::parse2::( quote! { union CustomType { f1: u32, f2: f32 } }, ) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, - derive_input.attrs, + &mut derive_input.attrs, derive_input.ident.span(), ) .expect_err( diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index 95f2a11..99e91bc 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -1,14 +1,17 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; use syn::{ - DeriveInput, Ident, + Attribute, DeriveInput, Ident, parse::{Parse, ParseStream}, }; mod fmt; use fmt::TypeData; +mod util; + pub(crate) struct ErrorStackDeriveInput { + other_attrs: Vec, ident: Ident, display_data: TypeData, } @@ -20,15 +23,18 @@ impl Parse for ErrorStackDeriveInput { drop(derive_input.generics); drop(derive_input.vis); + let mut attrs = derive_input.attrs; + let display_data = TypeData::new( derive_input.data, - derive_input.attrs, + &mut attrs, derive_input.ident.span(), )?; let ident = derive_input.ident; Ok(Self { + other_attrs: attrs, ident, display_data, }) @@ -38,11 +44,13 @@ impl Parse for ErrorStackDeriveInput { impl ToTokens for ErrorStackDeriveInput { fn to_tokens(&self, tokens: &mut TokenStream2) { let Self { + other_attrs, ident, display_data, } = self; tokens.extend(quote! { + #(#other_attrs)* impl ::core::fmt::Display for #ident { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { #display_data diff --git a/impl/src/types/util.rs b/impl/src/types/util.rs new file mode 100644 index 0000000..cf1a791 --- /dev/null +++ b/impl/src/types/util.rs @@ -0,0 +1,10 @@ +use syn::Attribute; + +pub(crate) fn take_display_attr( + attrs: &mut Vec, +) -> Option { + let index = attrs + .iter_mut() + .position(|attr| attr.path().is_ident("display"))?; + Some(attrs.remove(index)) +} From bc54cc31406e94c2c7a323b7026806f1e40a7c89 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 16:59:21 -0600 Subject: [PATCH 11/20] Use `syn::parse2` --- impl/src/types/fmt/input.rs | 2 +- impl/src/types/fmt/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/impl/src/types/fmt/input.rs b/impl/src/types/fmt/input.rs index 34bb7d5..5a28e4b 100644 --- a/impl/src/types/fmt/input.rs +++ b/impl/src/types/fmt/input.rs @@ -58,7 +58,7 @@ impl Parse for StructFormatInput { quote! { &self.#ident } }; - let arg_expr = syn::parse(arg_tokens.into())?; + let arg_expr = syn::parse2(arg_tokens)?; args.push(arg_expr); fmt_string.replace_range(group.range(), ""); diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index 08c41a1..ebe9a4b 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -132,7 +132,7 @@ impl TypeData { let meta_span = meta.span(); drop(meta.path); - let parse_res = syn::parse::(meta.tokens.into()); + let parse_res = syn::parse2::(meta.tokens); match parse_res { Ok(input) => return Ok(input), From b5882b3dba193aabc8a298734a5c9e7d33d91e1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:09:26 +0000 Subject: [PATCH 12/20] Bump quote from 1.0.40 to 1.0.41 Bumps [quote](https://github.com/dtolnay/quote) from 1.0.40 to 1.0.41. - [Release notes](https://github.com/dtolnay/quote/releases) - [Commits](https://github.com/dtolnay/quote/compare/1.0.40...1.0.41) --- updated-dependencies: - dependency-name: quote dependency-version: 1.0.41 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- impl/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9d4ae1..21109a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] diff --git a/impl/Cargo.toml b/impl/Cargo.toml index fd3d992..5e3147a 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -31,7 +31,7 @@ panic = "warn" [dependencies] proc-macro2 = "1.0.101" -quote = "1.0.40" +quote = "1.0.41" regex = "1.11.3" syn = { version = "2.0.106", features = [] } From 6715621e790b17661860c4b901808fb7a6de3590 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 17:16:13 -0600 Subject: [PATCH 13/20] Remove tolerance for trailing comma --- impl/src/types/fmt/input.rs | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/impl/src/types/fmt/input.rs b/impl/src/types/fmt/input.rs index 5a28e4b..a8bc643 100644 --- a/impl/src/types/fmt/input.rs +++ b/impl/src/types/fmt/input.rs @@ -26,9 +26,7 @@ impl Debug for StructFormatInput { impl Parse for StructFormatInput { fn parse(input: ParseStream) -> syn::Result { let lit_str: LitStr = input.parse()?; - - let comma: Option = input.parse()?; - if comma.is_none() && !input.is_empty() { + if !input.is_empty() { return Err(syn::Error::new( input.span(), "unexpected token after string literal", @@ -98,9 +96,7 @@ impl Debug for VariantFormatInput { impl Parse for VariantFormatInput { fn parse(input: ParseStream) -> syn::Result { let lit_str: LitStr = input.parse()?; - - let comma: Option = input.parse()?; - if comma.is_none() && !input.is_empty() { + if !input.is_empty() { return Err(syn::Error::new( input.span(), "unexpected token after string literal", @@ -192,16 +188,6 @@ mod tests { assert_eq!(err.to_string(), "unexpected token after string literal"); } - #[test] - fn struct_format_input_parses_lit_str_with_trailing_comma() { - let empty_stream_res = - syn::parse2::(quote! { "format string", }); - let format_input = empty_stream_res.expect( - "stream `\"format string\",` could not be parsed as StructFormatInput", - ); - assert_eq!(format_input.lit_str.value(), "format string"); - } - #[test] fn enum_variant_format_input_requires_initial_lit_str() { let empty_stream_res = syn::parse2::(quote! {}); @@ -233,14 +219,4 @@ mod tests { ); assert_eq!(err.to_string(), "unexpected token after string literal"); } - - #[test] - fn enum_variant_format_input_parses_lit_str_with_trailing_comma() { - let empty_stream_res = - syn::parse2::(quote! { "format string", }); - let format_input = empty_stream_res.expect( - "stream `\"format string\",` could not be parsed as VariantFormatInput", - ); - assert_eq!(format_input.lit_str.value(), "format string"); - } } From b9906a42ceb5a9cf73e48e04af5de9c40b2c8290 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 17:52:20 -0600 Subject: [PATCH 14/20] Store non-display variant attributes and apply to output --- impl/src/types/fmt/mod.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index ebe9a4b..1b617ea 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -171,13 +171,13 @@ impl TypeData { let mut attrs = variant.attrs; let display_attr = util::take_display_attr(&mut attrs); - drop(attrs); use VariantState as VS; match display_attr { None => VS::None(variant_span), Some(attr) => match Self::get_format_input(attr) { Ok(input) => VS::Valid(VariantData { + other_attrs: attrs, ident: variant.ident, fields: variant.fields, display_input: input, @@ -300,6 +300,7 @@ impl VariantState { type ValidVariantState = VariantState; pub(crate) struct VariantData { + other_attrs: Vec, ident: Ident, fields: Fields, display_input: VariantFormatInput, @@ -307,13 +308,16 @@ pub(crate) struct VariantData { impl ToTokens for VariantData { fn to_tokens(&self, tokens: &mut TokenStream2) { - let ident = &self.ident; - let fields = &self.fields; - let display_input = &self.display_input; + let Self { + other_attrs, + ident, + fields, + display_input, + } = self; let field_idents = fields.iter().enumerate().map(|(i, field)| { field.ident.clone().unwrap_or_else(|| { - syn::Ident::new(&format!("_field{}", i), field.span()) + Ident::new(&format!("_field{}", i), field.span()) }) }); @@ -327,6 +331,7 @@ impl ToTokens for VariantData { }; tokens.extend(quote! { + #(#other_attrs)* Self::#ident #field_tokens => ::core::write!(f, #display_input) }) } From fe14cce5de1b10bb94be3065bd831e9c33170096 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 18:17:53 -0600 Subject: [PATCH 15/20] Optimizations (calls to `std::mem::drop` and remove unnecessary generics) --- impl/src/types/fmt/mod.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index 1b617ea..9286e11 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -73,7 +73,7 @@ impl TypeData { let (valid_variants, none_spans) = Self::separate_existing_variant_states( - variant_display_inputs.into_iter(), + variant_display_inputs, ); if valid_variants.is_empty() { @@ -168,13 +168,22 @@ impl TypeData { ) -> Result, syn::Error> { let mut variant_states_iter = variants.into_iter().map(|variant| { let variant_span = variant.span(); + drop(variant.discriminant); let mut attrs = variant.attrs; let display_attr = util::take_display_attr(&mut attrs); use VariantState as VS; match display_attr { - None => VS::None(variant_span), + None => { + drop(variant.fields); + drop(variant.ident); + drop(attrs); + drop(display_attr); + + VS::None(variant_span) + } + Some(attr) => match Self::get_format_input(attr) { Ok(input) => VS::Valid(VariantData { other_attrs: attrs, @@ -201,6 +210,7 @@ impl TypeData { err.combine(err2); } + drop(variant_states_iter); return Err(err); } } @@ -211,12 +221,9 @@ impl TypeData { Ok(vec) } - fn separate_existing_variant_states( - states_iter: I, - ) -> (Vec, Vec) - where - I: Iterator, - { + fn separate_existing_variant_states( + states_iter: Vec>, + ) -> (Vec, Vec) { let mut valid_variants = Vec::new(); let mut none_spans = Vec::new(); From e68f3a09abea4b4c412edcdfd9ad697ebc083dd1 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Mon, 29 Sep 2025 18:33:24 -0600 Subject: [PATCH 16/20] Move `TypeData` associated functions to `src::types::fmt::util` --- impl/src/types/fmt/mod.rs | 133 ++----------------------------------- impl/src/types/fmt/util.rs | 123 ++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 126 deletions(-) create mode 100644 impl/src/types/fmt/util.rs diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index 9286e11..ca116b2 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -4,15 +4,12 @@ use std::fmt::{self, Debug, Formatter}; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{ToTokens, quote}; -use syn::{ - Attribute, Data, Fields, Ident, LitStr, Meta, Variant, parse::Parse, - punctuated::Punctuated, spanned::Spanned, token::Comma, -}; +use syn::{Attribute, Data, Fields, Ident, LitStr, spanned::Spanned}; mod input; use input::{StructFormatInput, VariantFormatInput}; -use super::util; +mod util; pub(crate) enum TypeData { Struct { @@ -33,7 +30,7 @@ impl TypeData { attrs: &mut Vec, ident_span: Span, ) -> syn::Result { - let default_display_attr = util::take_display_attr(attrs); + let default_display_attr = super::util::take_display_attr(attrs); match input_data { Data::Struct(_) => { @@ -41,7 +38,7 @@ impl TypeData { let display_attr = default_display_attr .ok_or_else(|| syn::Error::new(ident_span, "missing `display` attribute for struct with `#[derive(Error)]`"))?; - let display_input = Self::get_format_input(display_attr)?; + let display_input = util::get_format_input(display_attr)?; Ok(Self::Struct { display_input }) } @@ -54,11 +51,11 @@ impl TypeData { } let variant_display_inputs = - Self::collect_valid_variant_states(variants)?; + util::collect_valid_variant_states(variants)?; if let Some(attr) = default_display_attr { let default_display_input = - Some(Self::get_format_input(attr)?); + Some(util::get_format_input(attr)?); return Ok(Self::Enum { default_display_input, @@ -72,7 +69,7 @@ impl TypeData { drop(default_display_attr); let (valid_variants, none_spans) = - Self::separate_existing_variant_states( + util::separate_existing_variant_states( variant_display_inputs, ); @@ -121,122 +118,6 @@ impl TypeData { } } } - - fn get_format_input(display_attr: Attribute) -> syn::Result - where - T: Parse, - { - let attr_span = display_attr.span(); - - if let Meta::List(meta) = display_attr.meta { - let meta_span = meta.span(); - drop(meta.path); - - let parse_res = syn::parse2::(meta.tokens); - - match parse_res { - Ok(input) => return Ok(input), - Err(err) => { - return Err( - if err.to_string() - == "unexpected end of input, expected string literal" - { - drop(err); - - syn::Error::new( - meta_span, - "unexpected empty `display` attribute, expected string literal", - ) - } else { - err - }, - ); - } - } - } - - drop(display_attr); - - Err(syn::Error::new( - attr_span, - "expected `display` to be a list attribute: `#[display(\"template...\")]`", - )) - } - - fn collect_valid_variant_states( - variants: Punctuated, - ) -> Result, syn::Error> { - let mut variant_states_iter = variants.into_iter().map(|variant| { - let variant_span = variant.span(); - drop(variant.discriminant); - - let mut attrs = variant.attrs; - let display_attr = util::take_display_attr(&mut attrs); - - use VariantState as VS; - match display_attr { - None => { - drop(variant.fields); - drop(variant.ident); - drop(attrs); - drop(display_attr); - - VS::None(variant_span) - } - - Some(attr) => match Self::get_format_input(attr) { - Ok(input) => VS::Valid(VariantData { - other_attrs: attrs, - ident: variant.ident, - fields: variant.fields, - display_input: input, - }), - Err(err) => VS::Invalid(err), - }, - } - }); - - let mut vec = Vec::new(); - - while let Some(state) = variant_states_iter.next() { - use VariantState as VS; - match state { - VS::None(span) => vec.push(VS::None(span)), - VS::Valid(data) => vec.push(VS::Valid(data)), - VS::Invalid(mut err) => { - while let Some(VS::Invalid(err2)) = - variant_states_iter.next() - { - err.combine(err2); - } - - drop(variant_states_iter); - return Err(err); - } - } - } - - drop(variant_states_iter); - - Ok(vec) - } - - fn separate_existing_variant_states( - states_iter: Vec>, - ) -> (Vec, Vec) { - let mut valid_variants = Vec::new(); - let mut none_spans = Vec::new(); - - for state in states_iter { - use VariantState as VS; - match state { - VS::Valid(data) => valid_variants.push(data), - VS::None(span) => none_spans.push(span), - } - } - - (valid_variants, none_spans) - } } #[cfg(test)] diff --git a/impl/src/types/fmt/util.rs b/impl/src/types/fmt/util.rs new file mode 100644 index 0000000..0757e3a --- /dev/null +++ b/impl/src/types/fmt/util.rs @@ -0,0 +1,123 @@ +use std::convert::Infallible; + +use proc_macro2::Span; +use syn::{ + Attribute, Meta, Variant, parse::Parse, punctuated::Punctuated, + spanned::Spanned, token::Comma, +}; + +use super::{ValidVariantState, VariantData, VariantState}; + +pub(crate) fn get_format_input(display_attr: Attribute) -> syn::Result +where + T: Parse, +{ + let attr_span = display_attr.span(); + + if let Meta::List(meta) = display_attr.meta { + let meta_span = meta.span(); + drop(meta.path); + + let parse_res = syn::parse2::(meta.tokens); + + match parse_res { + Ok(input) => return Ok(input), + Err(err) => { + return Err( + if err.to_string() + == "unexpected end of input, expected string literal" + { + drop(err); + + syn::Error::new( + meta_span, + "unexpected empty `display` attribute, expected string literal", + ) + } else { + err + }, + ); + } + } + } + + drop(display_attr); + + Err(syn::Error::new( + attr_span, + "expected `display` to be a list attribute: `#[display(\"template...\")]`", + )) +} + +pub(crate) fn collect_valid_variant_states( + variants: Punctuated, +) -> Result, syn::Error> { + let mut variant_states_iter = variants.into_iter().map(|variant| { + let variant_span = variant.span(); + drop(variant.discriminant); + + let mut attrs = variant.attrs; + let display_attr = crate::types::util::take_display_attr(&mut attrs); + + use VariantState as VS; + match display_attr { + None => { + drop(variant.fields); + drop(variant.ident); + drop(attrs); + drop(display_attr); + + VS::None(variant_span) + } + + Some(attr) => match get_format_input(attr) { + Ok(input) => VS::Valid(VariantData { + other_attrs: attrs, + ident: variant.ident, + fields: variant.fields, + display_input: input, + }), + Err(err) => VS::Invalid(err), + }, + } + }); + + let mut vec = Vec::new(); + + while let Some(state) = variant_states_iter.next() { + use VariantState as VS; + match state { + VS::None(span) => vec.push(VS::None(span)), + VS::Valid(data) => vec.push(VS::Valid(data)), + VS::Invalid(mut err) => { + while let Some(VS::Invalid(err2)) = variant_states_iter.next() { + err.combine(err2); + } + + drop(variant_states_iter); + return Err(err); + } + } + } + + drop(variant_states_iter); + + Ok(vec) +} + +pub(crate) fn separate_existing_variant_states( + states_iter: Vec>, +) -> (Vec, Vec) { + let mut valid_variants = Vec::new(); + let mut none_spans = Vec::new(); + + for state in states_iter { + use VariantState as VS; + match state { + VS::Valid(data) => valid_variants.push(data), + VS::None(span) => none_spans.push(span), + } + } + + (valid_variants, none_spans) +} From d240a71912d3181c45369e673e7febac912791c7 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Tue, 30 Sep 2025 15:55:56 -0600 Subject: [PATCH 17/20] Add support for generics --- impl/src/types/mod.rs | 30 +++++++++-- impl/src/types/util.rs | 113 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 138 insertions(+), 5 deletions(-) diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index 99e91bc..d9b11f2 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; use syn::{ - Attribute, DeriveInput, Ident, + Attribute, DeriveInput, Generics, Ident, parse::{Parse, ParseStream}, }; @@ -9,10 +9,12 @@ mod fmt; use fmt::TypeData; mod util; +use util::ReducedGenerics; pub(crate) struct ErrorStackDeriveInput { other_attrs: Vec, ident: Ident, + generics: Generics, display_data: TypeData, } @@ -20,7 +22,6 @@ impl Parse for ErrorStackDeriveInput { fn parse(input: ParseStream) -> syn::Result { let derive_input: DeriveInput = input.parse()?; - drop(derive_input.generics); drop(derive_input.vis); let mut attrs = derive_input.attrs; @@ -33,9 +34,16 @@ impl Parse for ErrorStackDeriveInput { let ident = derive_input.ident; + let mut generics = derive_input.generics; + generics + .params + .iter_mut() + .for_each(util::remove_generic_default); + Ok(Self { other_attrs: attrs, ident, + generics, display_data, }) } @@ -46,18 +54,32 @@ impl ToTokens for ErrorStackDeriveInput { let Self { other_attrs, ident, + generics, display_data, } = self; + let mut error_trait_generics = generics.clone(); + error_trait_generics + .params + .iter_mut() + .for_each(util::add_debug_trait_bound); + + let type_generics: ReducedGenerics = generics + .params + .iter() + .cloned() + .map(util::generic_reduced_to_ident) + .collect(); + tokens.extend(quote! { #(#other_attrs)* - impl ::core::fmt::Display for #ident { + impl #generics ::core::fmt::Display for #ident #type_generics { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { #display_data } } - impl ::core::error::Error for #ident {} + impl #error_trait_generics ::core::error::Error for #ident #type_generics {} }); } } diff --git a/impl/src/types/util.rs b/impl/src/types/util.rs index cf1a791..4303054 100644 --- a/impl/src/types/util.rs +++ b/impl/src/types/util.rs @@ -1,4 +1,50 @@ -use syn::Attribute; +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, quote}; +use syn::{ + Attribute, GenericParam, Ident, Lifetime, Path, TraitBound, + TraitBoundModifier, TypeParamBound, + punctuated::Punctuated, + spanned::Spanned, + token::{Colon, Comma}, +}; + +pub(crate) enum ReducedGenericParam { + ConstOrType(Ident), + Lifetime(Lifetime), +} + +impl ToTokens for ReducedGenericParam { + fn to_tokens(&self, tokens: &mut TokenStream2) { + use ReducedGenericParam as RGP; + match self { + RGP::ConstOrType(ident) => tokens.extend(quote! { #ident }), + RGP::Lifetime(lifetime) => tokens.extend(quote! { #lifetime }), + } + } +} + +pub(crate) struct ReducedGenerics { + params: Punctuated, +} + +impl FromIterator for ReducedGenerics { + fn from_iter>(iter: T) -> Self { + Self { + params: iter.into_iter().collect(), + } + } +} + +impl ToTokens for ReducedGenerics { + fn to_tokens(&self, tokens: &mut TokenStream2) { + if self.params.is_empty() { + return; + } + + let params = self.params.iter(); + tokens.extend(quote! { < #(#params),* > }); + } +} pub(crate) fn take_display_attr( attrs: &mut Vec, @@ -8,3 +54,68 @@ pub(crate) fn take_display_attr( .position(|attr| attr.path().is_ident("display"))?; Some(attrs.remove(index)) } + +pub(crate) fn remove_generic_default(param: &mut GenericParam) { + use GenericParam as GP; + match param { + GP::Const(const_p) => { + const_p.eq_token = None; + const_p.default = None; + } + + GP::Type(type_p) => { + type_p.eq_token = None; + type_p.default = None; + } + + _ => {} + } +} + +pub(crate) fn generic_reduced_to_ident( + param: GenericParam, +) -> ReducedGenericParam { + use GenericParam as GP; + use ReducedGenericParam as RGP; + match param { + GP::Const(const_p) => { + drop(const_p.attrs); + drop(const_p.default); + drop(const_p.ty); + + RGP::ConstOrType(const_p.ident) + } + + GP::Type(type_p) => { + drop(type_p.attrs); + drop(type_p.bounds); + drop(type_p.default); + + RGP::ConstOrType(type_p.ident) + } + + GP::Lifetime(lifetime_p) => { + drop(lifetime_p.attrs); + drop(lifetime_p.bounds); + + RGP::Lifetime(lifetime_p.lifetime) + } + } +} + +pub(crate) fn add_debug_trait_bound(param: &mut GenericParam) { + use GenericParam as GP; + if let GP::Type(type_p) = param { + #[allow(clippy::unwrap_used)] + let trait_path: Path = + syn::parse2(quote! { ::core::fmt::Debug }).unwrap(); + + type_p.colon_token = Some(Colon(type_p.span())); + type_p.bounds.push(TypeParamBound::Trait(TraitBound { + paren_token: None, + modifier: TraitBoundModifier::None, + lifetimes: None, + path: trait_path, + })); + } +} From 017136f0f96401fd2fce6c203b18b96bb8202c86 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Tue, 30 Sep 2025 15:58:57 -0600 Subject: [PATCH 18/20] Add support for `where` clauses --- impl/src/types/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index d9b11f2..cd9f7a4 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -58,6 +58,8 @@ impl ToTokens for ErrorStackDeriveInput { display_data, } = self; + let where_clause = &generics.where_clause; + let mut error_trait_generics = generics.clone(); error_trait_generics .params @@ -73,13 +75,18 @@ impl ToTokens for ErrorStackDeriveInput { tokens.extend(quote! { #(#other_attrs)* - impl #generics ::core::fmt::Display for #ident #type_generics { + impl #generics ::core::fmt::Display for #ident #type_generics + #where_clause + { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { #display_data } } - impl #error_trait_generics ::core::error::Error for #ident #type_generics {} + impl #error_trait_generics ::core::error::Error for #ident #type_generics + #where_clause + { + } }); } } From a050561005f66e95fad34e73953569526df3c453 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Tue, 30 Sep 2025 20:25:42 -0600 Subject: [PATCH 19/20] Add tests for generics and other attributes --- impl/src/types/fmt/mod.rs | 55 ++++++++++++++++++++---------- impl/src/types/mod.rs | 60 +++++++++++++++++++++++++++++++++ tests/src/structs.rs | 71 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+), 17 deletions(-) diff --git a/impl/src/types/fmt/mod.rs b/impl/src/types/fmt/mod.rs index ca116b2..cc4be8a 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -228,6 +228,8 @@ impl ToTokens for VariantData { #[cfg(test)] #[allow(clippy::expect_used)] mod tests { + use crate::ErrorStackDeriveInput; + use super::*; use quote::quote; @@ -235,8 +237,8 @@ mod tests { #[test] fn struct_data_requires_display_attr() { - let mut derive_input = - syn::parse2::(quote! { struct CustomType; }) + let mut derive_input: DeriveInput = + syn::parse2(quote! { struct CustomType; }) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, @@ -254,10 +256,9 @@ mod tests { #[test] fn struct_data_requires_list_form_for_display_attr() { - let mut derive_input = syn::parse2::( - quote! { #[display] struct CustomType; }, - ) - .expect("malformed test stream"); + let mut derive_input: DeriveInput = + syn::parse2(quote! { #[display] struct CustomType; }) + .expect("malformed test stream"); let err = TypeData::new( derive_input.data, &mut derive_input.attrs, @@ -274,8 +275,8 @@ mod tests { #[test] fn enum_data_requires_display_attr() { - let mut derive_input = - syn::parse2::(quote! { enum CustomType { One, Two } }) + let mut derive_input: DeriveInput = + syn::parse2(quote! { enum CustomType { One, Two } }) .expect("malformed test stream"); let err = TypeData::new( derive_input.data, @@ -293,10 +294,9 @@ mod tests { #[test] fn enum_data_requires_list_form_for_display_attr() { - let mut derive_input = syn::parse2::( - quote! { #[display] enum CustomType { One, Two } }, - ) - .expect("malformed test stream"); + let mut derive_input: DeriveInput = + syn::parse2(quote! { #[display] enum CustomType { One, Two } }) + .expect("malformed test stream"); let err = TypeData::new( derive_input.data, &mut derive_input.attrs, @@ -313,7 +313,7 @@ mod tests { #[test] fn enum_data_requires_list_form_for_display_attr_on_every_variant() { - let mut derive_input = syn::parse2::(quote! { + let mut derive_input: DeriveInput = syn::parse2(quote! { enum CustomType { #[display] One, @@ -338,10 +338,9 @@ mod tests { #[test] fn union_type_is_rejected() { - let mut derive_input = syn::parse2::( - quote! { union CustomType { f1: u32, f2: f32 } }, - ) - .expect("malformed test stream"); + let mut derive_input: DeriveInput = + syn::parse2(quote! { union CustomType { f1: u32, f2: f32 } }) + .expect("malformed test stream"); let err = TypeData::new( derive_input.data, &mut derive_input.attrs, @@ -355,4 +354,26 @@ mod tests { "`#[derive(Error)]` only supports structs and enums" ); } + + #[test] + fn variant_works_with_other_attrs() { + let derive_input: ErrorStackDeriveInput = syn::parse2(quote! { + #[display("custom type")] + enum CustomType { + #[cfg(true)] + One { inner: u8 }, + + #[cfg(true)] + #[display("custom type two {0}.{1}.{2}.{3}")] + Two(u8, u8, u8, u8), + } + }) + .expect("malformed test stream"); + + let output = quote! { #derive_input }; + assert_eq!( + output.to_string(), + "# [allow (single_use_lifetimes)] impl :: core :: fmt :: Display for CustomType { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { match & self { # [cfg (true)] Self :: Two (_field0 , _field1 , _field2 , _field3) => :: core :: write ! (f , \"custom type two {}.{}.{}.{}\" , _field0 , _field1 , _field2 , _field3) , _ => :: core :: write ! (f , \"custom type\") } } } # [allow (single_use_lifetimes)] impl :: core :: error :: Error for CustomType { }" + ); + } } diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index cd9f7a4..e23ea1e 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -75,6 +75,7 @@ impl ToTokens for ErrorStackDeriveInput { tokens.extend(quote! { #(#other_attrs)* + #[allow(single_use_lifetimes)] impl #generics ::core::fmt::Display for #ident #type_generics #where_clause { @@ -83,6 +84,8 @@ impl ToTokens for ErrorStackDeriveInput { } } + #(#other_attrs)* + #[allow(single_use_lifetimes)] impl #error_trait_generics ::core::error::Error for #ident #type_generics #where_clause { @@ -90,3 +93,60 @@ impl ToTokens for ErrorStackDeriveInput { }); } } + +#[cfg(test)] +#[allow(clippy::expect_used)] +mod tests { + use quote::quote; + + use crate::ErrorStackDeriveInput; + + #[test] + fn input_works_with_other_attrs() { + let input: ErrorStackDeriveInput = syn::parse2(quote! { + #[test_attribute] + #[display("custom type")] + #[test_attribute_2] + struct CustomType; + }) + .expect("malformed test stream"); + + let output = quote! { #input }; + assert_eq!( + output.to_string(), + "# [test_attribute] # [test_attribute_2] # [allow (single_use_lifetimes)] impl :: core :: fmt :: Display for CustomType { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { :: core :: write ! (f , \"custom type\" ,) } } # [test_attribute] # [test_attribute_2] # [allow (single_use_lifetimes)] impl :: core :: error :: Error for CustomType { }" + ); + } + + #[test] + fn generics_work_with_attrs() { + let derive_input: ErrorStackDeriveInput = syn::parse2(quote! { + #[display("custom type")] + struct CustomType<#[cfg(true)] T> { + _data: PhantomData + } + }) + .expect("malformed test stream"); + + let output = quote! { #derive_input }; + assert_eq!( + output.to_string(), + "# [allow (single_use_lifetimes)] impl < # [cfg (true)] T > :: core :: fmt :: Display for CustomType < T > { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { :: core :: write ! (f , \"custom type\" ,) } } # [allow (single_use_lifetimes)] impl < # [cfg (true)] T : :: core :: fmt :: Debug > :: core :: error :: Error for CustomType < T > { }" + ); + } + + #[test] + fn output_impl_has_attr_allow_single_use_lifetimes() { + let input: ErrorStackDeriveInput = syn::parse2(quote! { + #[display("custom type")] + struct CustomType; + }) + .expect("malformed test stream"); + + let output = quote! { #input }; + assert_eq!( + output.to_string(), + "# [allow (single_use_lifetimes)] impl :: core :: fmt :: Display for CustomType { fn fmt (& self , f : & mut :: core :: fmt :: Formatter < '_ >) -> :: core :: fmt :: Result { :: core :: write ! (f , \"custom type\" ,) } } # [allow (single_use_lifetimes)] impl :: core :: error :: Error for CustomType { }" + ); + } +} diff --git a/tests/src/structs.rs b/tests/src/structs.rs index 22092fe..8e0d1ca 100644 --- a/tests/src/structs.rs +++ b/tests/src/structs.rs @@ -1,5 +1,7 @@ #[cfg(test)] mod tests { + use std::fmt::{Debug, Display}; + use error_stack_macros2::Error; #[test] @@ -73,6 +75,75 @@ mod tests { ); } + #[test] + fn named_field_struct_works_with_type_parameters() { + #[derive(Debug, Error)] + #[display("T = {t}, U = {u:?}")] + struct NamedFieldStructType> { + t: T, + u: U, + } + + let test_val = NamedFieldStructType { + t: String::from("string"), + u: vec![192, 168, 0, 254], + }; + assert_eq!(test_val.to_string(), "T = string, U = [192, 168, 0, 254]"); + } + + #[test] + fn named_field_struct_works_with_lifetime_parameters() { + #[derive(Debug, Error)] + #[display( + "string_a = {string_a}, string_b = {string_b}, slice = {slice:?}" + )] + struct NamedFieldStructType<'a, 'b: 'a> { + string_a: &'a str, + string_b: &'b str, + slice: &'b [u8], + } + + let test_val = NamedFieldStructType { + string_a: "string a", + string_b: "string b", + slice: &[192, 168, 0, 254], + }; + assert_eq!( + test_val.to_string(), + "string_a = string a, string_b = string b, slice = [192, 168, 0, 254]" + ); + } + + #[test] + fn named_field_struct_works_with_const_parameters() { + #[derive(Debug, Error)] + #[display("inner = {inner}")] + struct NamedFieldStructType { + inner: u8, + } + + let test_val = NamedFieldStructType::<8> { inner: 8 }; + assert_eq!(test_val.to_string(), "inner = 8"); + } + + #[test] + fn named_field_struct_works_with_where_clause() { + const STRING: &str = "t ref"; + + #[derive(Debug, Error)] + #[display("t_ref = {t_ref:?}")] + struct NamedFieldStructType<'a, T> + where + 'a: 'static, + T: Debug, + { + t_ref: &'a T, + } + + let test_val = NamedFieldStructType { t_ref: &STRING }; + assert_eq!(test_val.to_string(), "t_ref = \"t ref\""); + } + #[test] #[allow(dead_code)] fn tuple_struct_works_without_interpolation() { From a58146bef5c6e210d2cb0c6d96a3de5050586684 Mon Sep 17 00:00:00 2001 From: LuisFerLCC Date: Tue, 30 Sep 2025 21:58:27 -0600 Subject: [PATCH 20/20] Prepare release 0.2.0 --- .github/CONTRIBUTING.md | 3 +- .github/PULL_REQUEST_TEMPLATE.md | 1 + Cargo.lock | 4 +-- RELEASE.md | 60 ++++++++++++-------------------- SECURITY.md | 3 +- impl/Cargo.toml | 2 +- tests/Cargo.toml | 2 +- 7 files changed, 32 insertions(+), 43 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8681ce8..d12d8ae 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -8,9 +8,10 @@ If you wish to contribute to the `error-stack-macros2` codebase, feel free to fo 1. Refer to the [documentation](https://docs.rs/error-stack-macros2) to make sure the error is actually a bug and not a mistake of your own or intended behavior. 1. Make sure the issue hasn't already been reported or suggested. +1. Before starting to make your changes, please create an issue (if there isn't one yet) in order to discuss your proposal. 1. Fork and clone the repository. 1. Make your changes (add or modify tests and documentation comments as necessary to cover your changes). 1. Run `cargo test` (or VSCode task _Cargo: Test_) to run the tests. You can also run `cargo build` (_Cargo: Create development build_) to test the macro in a local Cargo project, or run `cargo doc` (_Cargo: Generate documentation_) to build the documentation. 1. Run `cargo fmt` and `cargo clippy` (or VSCode tasks _RustFMT: Format_ and _Cargo Clippy: Lint_) and make sure there are no warnings or errors. -1. Commit and push your changes. +1. Commit and push your changes. **NOTE: This repository requires all commits to be [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). Please make sure to sign all of your commits. Commits made through the GitHub web app are signed by default.** 1. [Submit a pull request](https://github.com/LuisFerLCC/error-stack-macros2/compare). diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c69439d..4d5638c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,6 +6,7 @@ - Make sure to read the [contribution guidelines](https://github.com/LuisFerLCC/error-stack-macros2/blob/master/.github/CONTRIBUTING.md) before you go any further. - Is there an issue related to your changes? If so, link to it here. If not, create an issue before submitting your pull request in order to discuss. +- **Please make sure that all of your commits are [signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification).** ## (How) do your changes work? diff --git a/Cargo.lock b/Cargo.lock index 21109a1..6c79eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,7 +29,7 @@ dependencies = [ [[package]] name = "error-stack-macros2" -version = "0.1.0" +version = "0.2.0" dependencies = [ "error-stack", "proc-macro2", @@ -40,7 +40,7 @@ dependencies = [ [[package]] name = "error-stack-macros2-tests" -version = "0.1.0" +version = "0.2.0" dependencies = [ "error-stack", "error-stack-macros2", diff --git a/RELEASE.md b/RELEASE.md index c4e0e49..a9d4cf4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,55 +1,41 @@ -# `error-stack-macros2` v0.1.0 +# `error-stack-macros2` v0.2.0 -The very first development version of `error-stack-macros2` is finally here! +We have a new development version of `error-stack-macros2`! -## Features +## Fixes -This version (0.1.0) offers a derive macro for the [`Error`](https://doc.rust-lang.org/stable/core/error/trait.Error.html) trait which encourages the best practices for defining [`error-stack`](https://crates.io/crates/error-stack) context types. +This version (0.2.0) adds support for generics and external attributes to the [`impl_error_stack`](https://docs.rs/error-stack-macros2/latest/error_stack_macros2/derive.Error.html) macro. -Here's an example. This code: +This means that types like this: ```rust -use std::{ - error::Error, - fmt::{self, Display, Formatter}, -}; - -#[derive(Debug)] -pub enum CreditCardError { - InvalidInput(String), - Other, -} - -impl Display for CreditCardError { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let msg = match self { - Self::InvalidInput(_) => "credit card not found", - Self::Other => "failed to retrieve credit card", - }; +use error_stack_macros2::Error; - f.write_str(msg) - } +#[derive(Debug, Error)] +#[display("failed to retrieve credit card")] +enum CreditCardError +where + T: Display +{ + InvalidInput(T), + Other } -impl Error for CreditCardError {} +#[derive(Debug, Error)] +#[display("invalid card string")] +#[allow(non_camel_case_types)] +struct parseCardError; ``` -...can now be reduced to this code: +...can now compile properly. -```rust -use error_stack_macros2::Error; +## Performance -#[derive(Debug, Error)] -pub enum CreditCardError { - #[display("credit card not found")] - InvalidInput(String), +The entire source code has been refactored to eliminate unnecessary allocations, cloning, and double iterator consumptions. This should make compile times faster and reduce memory usage. - #[display("failed to retrieve credit card")] - Other, -} -``` +## Dependencies -This new release also means that we will now be listening to feedback and accepting new features (macros, obviously). We are also now committed to maintaining this macro going forward and keeping our dependencies up to date. +As promised, all dependencies have been updated to their latest versions, which in this case means performance improvements and bug fixes. ## Previous release notes diff --git a/SECURITY.md b/SECURITY.md index 7033048..1a06230 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,8 @@ | Version | Supported | | ------- | --------- | -| 0.1.0 | โœ… | +| 0.2.0 | โœ… | +| < 0.2.0 | โŽ | ## Report a vulnerability diff --git a/impl/Cargo.toml b/impl/Cargo.toml index 5e3147a..edb5fb9 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "error-stack-macros2" -version = "0.1.0" +version = "0.2.0" authors = ["LuisFerLCC"] edition = "2024" rust-version = "1.90.0" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 852700a..a7a79c6 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "error-stack-macros2-tests" -version = "0.1.0" +version = "0.2.0" authors = ["LuisFerLCC"] edition = "2024" rust-version = "1.90.0"