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/.github/dependabot.yml b/.github/dependabot.yml index 53f8242..7b506cf 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: "weekly" + interval: daily + allow: + - dependency-type: "production" + labels: + - "๐Ÿ“ฆ Dependencies" + assignees: + - LuisFerLCC + + # Development dependencies (e.g., [dev-dependencies]) + - package-ecosystem: "cargo" + directory: "impl" + schedule: + interval: daily + allow: + - dependency-type: "development" + labels: + - "๐Ÿ’ป Dev Dependencies" + assignees: + - LuisFerLCC 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" 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" diff --git a/Cargo.lock b/Cargo.lock index 36dc187..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", @@ -63,18 +63,18 @@ 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", ] [[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/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 563e35b..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" @@ -31,8 +31,8 @@ panic = "warn" [dependencies] proc-macro2 = "1.0.101" -quote = "1.0.40" -regex = "1.11.2" +quote = "1.0.41" +regex = "1.11.3" syn = { version = "2.0.106", features = [] } [dev-dependencies] 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..a8bc643 100644 --- a/impl/src/types/fmt/input.rs +++ b/impl/src/types/fmt/input.rs @@ -25,10 +25,8 @@ impl Debug for StructFormatInput { impl Parse for StructFormatInput { fn parse(input: ParseStream) -> syn::Result { - let mut lit_str: LitStr = input.parse()?; - - let comma: Option = input.parse()?; - if comma.is_none() && !input.is_empty() { + let lit_str: LitStr = input.parse()?; + if !input.is_empty() { return Err(syn::Error::new( input.span(), "unexpected token after string literal", @@ -40,9 +38,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() { @@ -53,13 +56,16 @@ 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(), ""); } - 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 }) } @@ -75,24 +81,22 @@ 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()?; - - let comma: Option = input.parse()?; - if comma.is_none() && !input.is_empty() { + let lit_str: LitStr = input.parse()?; + if !input.is_empty() { return Err(syn::Error::new( input.span(), "unexpected token after string literal", @@ -104,9 +108,14 @@ impl Parse for EnumVariantFormatInput { 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,13 +130,16 @@ impl Parse for EnumVariantFormatInput { 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 }) } } -impl ToTokens for EnumVariantFormatInput { +impl ToTokens for VariantFormatInput { fn to_tokens(&self, tokens: &mut TokenStream2) { let Self { lit_str, args } = self; @@ -176,21 +188,11 @@ 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! {}); + 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 +203,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,20 +213,10 @@ 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"); } - - #[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 EnumVariantFormatInput", - ); - 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..cc4be8a 100644 --- a/impl/src/types/fmt/mod.rs +++ b/impl/src/types/fmt/mod.rs @@ -1,156 +1,133 @@ +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, -}; +use syn::{Attribute, Data, Fields, Ident, LitStr, spanned::Spanned}; -pub(crate) mod input; -use input::{EnumVariantFormatInput, StructFormatInput}; +mod input; +use input::{StructFormatInput, VariantFormatInput}; -use crate::util::traits::IteratorExt; +mod util; -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; +impl TypeData { + pub(crate) fn new( + input_data: Data, + attrs: &mut Vec, + ident_span: Span, + ) -> syn::Result { + let default_display_attr = super::util::take_display_attr(attrs); - match &derive_input.data { + 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_input = Self::get_format_input(display_attr)?; + drop(input_data); + + 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 = util::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() { + drop(variants); 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 variant_display_inputs = + 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, - variant_display_inputs: variant_display_inputs_res?, + variant_display_inputs: variant_display_inputs + .into_iter() + .filter_map(|state| state.data()) + .collect(), }); + }; + + drop(default_display_attr); + + let (valid_variants, none_spans) = + util::separate_existing_variant_states( + variant_display_inputs, + ); + + 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", + )); } - 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); - } + if !none_spans.is_empty() { + drop(valid_variants); + + #[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: inputs, - }) - } + drop(none_spans); - Err(err) => Err(err), - } + 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(default_display_attr); - pub(crate) fn get_display_attr(attrs: &[Attribute]) -> Option<&Attribute> { - attrs.iter().find(|attr| attr.path().is_ident("display")) - } - - pub(crate) 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 - } - }); + Err(syn::Error::new( + ident_span, + "`#[derive(Error)]` only supports structs and enums", + )) + } } - - Err(syn::Error::new( - display_attr.span(), - "expected `display` to be a list attribute: `#[display(\"template...\")]`", - )) } } #[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 +142,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 +167,86 @@ impl ToTokens for FormatData { } } +enum VariantState { + Valid(VariantData), + Invalid(E), + None(Span), +} + +impl VariantState { + fn data(self) -> Option { + match self { + Self::Valid(data) => Some(data), + _ => { + drop(self); + None + } + } + } +} + +type ValidVariantState = VariantState; + +pub(crate) struct VariantData { + other_attrs: Vec, + ident: Ident, + fields: Fields, + display_input: VariantFormatInput, +} + +impl ToTokens for VariantData { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let Self { + other_attrs, + ident, + fields, + display_input, + } = self; + + let field_idents = fields.iter().enumerate().map(|(i, field)| { + field.ident.clone().unwrap_or_else(|| { + Ident::new(&format!("_field{}", i), field.span()) + }) + }); + + let field_tokens = match fields { + Fields::Named(_) => quote! { { #(#field_idents),* } }, + Fields::Unnamed(_) => quote! { ( #(#field_idents),* ) }, + Fields::Unit => { + drop(field_idents); + TokenStream2::new() + } + }; + + tokens.extend(quote! { + #(#other_attrs)* + Self::#ident #field_tokens => ::core::write!(f, #display_input) + }) + } +} + #[cfg(test)] #[allow(clippy::expect_used)] mod tests { + use crate::ErrorStackDeriveInput; + use super::*; use quote::quote; + use syn::DeriveInput; #[test] - fn struct_format_data_requires_display_attr() { - let derive_input = - syn::parse2::(quote! { struct CustomType; }) + fn struct_data_requires_display_attr() { + let mut derive_input: DeriveInput = + 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, + &mut 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 +255,17 @@ mod tests { } #[test] - fn struct_format_data_requires_list_form_for_display_attr() { - let derive_input = syn::parse2::( - quote! { #[display] struct CustomType; }, + fn struct_data_requires_list_form_for_display_attr() { + 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, + derive_input.ident.span(), ) - .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with path display attr was parsed successfully as FormatData", + .expect_err( + "stream with path display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -247,12 +274,17 @@ mod tests { } #[test] - fn enum_format_data_requires_display_attr() { - let derive_input = - syn::parse2::(quote! { enum CustomType { One, Two } }) + fn enum_data_requires_display_attr() { + let mut derive_input: DeriveInput = + 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, + &mut 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 +293,17 @@ mod tests { } #[test] - fn enum_format_data_requires_list_form_for_display_attr() { - let derive_input = syn::parse2::( - quote! { #[display] enum CustomType { One, Two } }, + fn enum_data_requires_list_form_for_display_attr() { + 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, + derive_input.ident.span(), ) - .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with path display attr was parsed successfully as FormatData", + .expect_err( + "stream with path display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -276,8 +312,8 @@ mod tests { } #[test] - fn enum_format_data_requires_list_form_for_display_attr_on_every_variant() { - let derive_input = syn::parse2::(quote! { + fn enum_data_requires_list_form_for_display_attr_on_every_variant() { + let mut derive_input: DeriveInput = syn::parse2(quote! { enum CustomType { #[display] One, @@ -286,8 +322,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, + &mut derive_input.attrs, + derive_input.ident.span(), + ) + .expect_err( + "stream with path display attr was parsed successfully as TypeData", ); assert_eq!( err.to_string(), @@ -297,16 +338,42 @@ mod tests { #[test] fn union_type_is_rejected() { - let derive_input = syn::parse2::( - quote! { union CustomType { f1: u32, f2: f32 } }, + 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, + derive_input.ident.span(), ) - .expect("malformed test stream"); - let err = FormatData::new(&derive_input).expect_err( - "stream with union type was parsed successfully as FormatData", + .expect_err( + "stream with union type was parsed successfully as TypeData", ); assert_eq!( err.to_string(), "`#[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/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) +} diff --git a/impl/src/types/mod.rs b/impl/src/types/mod.rs index d706385..e23ea1e 100644 --- a/impl/src/types/mod.rs +++ b/impl/src/types/mod.rs @@ -1,28 +1,49 @@ use proc_macro2::TokenStream as TokenStream2; use quote::{ToTokens, quote}; use syn::{ - DeriveInput, Ident, + Attribute, DeriveInput, Generics, Ident, parse::{Parse, ParseStream}, }; mod fmt; -use fmt::FormatData; +use fmt::TypeData; + +mod util; +use util::ReducedGenerics; pub(crate) struct ErrorStackDeriveInput { + other_attrs: Vec, ident: Ident, - display_data: FormatData, + generics: Generics, + 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)?; + drop(derive_input.vis); + + let mut attrs = derive_input.attrs; + + let display_data = TypeData::new( + derive_input.data, + &mut attrs, + derive_input.ident.span(), + )?; 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, }) } @@ -31,18 +52,101 @@ impl Parse for ErrorStackDeriveInput { impl ToTokens for ErrorStackDeriveInput { fn to_tokens(&self, tokens: &mut TokenStream2) { let Self { + other_attrs, ident, + generics, display_data, } = self; + let where_clause = &generics.where_clause; + + 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! { - impl ::core::fmt::Display for #ident { + #(#other_attrs)* + #[allow(single_use_lifetimes)] + 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 ::core::error::Error for #ident {} + #(#other_attrs)* + #[allow(single_use_lifetimes)] + impl #error_trait_generics ::core::error::Error for #ident #type_generics + #where_clause + { + } }); } } + +#[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/impl/src/types/util.rs b/impl/src/types/util.rs new file mode 100644 index 0000000..4303054 --- /dev/null +++ b/impl/src/types/util.rs @@ -0,0 +1,121 @@ +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, +) -> Option { + let index = attrs + .iter_mut() + .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, + })); + } +} 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> {} 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" 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() {