From 3b4683fd2c6bab8763c87aefd07da346cf61a1ff Mon Sep 17 00:00:00 2001 From: Roland Fredenhagen Date: Sun, 2 Feb 2025 20:40:42 +0100 Subject: [PATCH 1/2] impl rename_all for #[derive(Display)] (#216) --- impl/Cargo.toml | 4 +- impl/src/fmt/display.rs | 35 ++++- impl/src/fmt/mod.rs | 145 +++++++++++++++++- .../debug/unknown_attribute.stderr | 2 +- tests/compile_fail/display/invalid_casing.rs | 7 + .../display/invalid_casing.stderr | 5 + .../display/rename_all_on_struct.rs | 11 ++ .../display/rename_all_on_struct.stderr | 11 ++ .../display/unknown_attribute.stderr | 2 +- tests/display.rs | 57 +++++++ 10 files changed, 260 insertions(+), 19 deletions(-) create mode 100644 tests/compile_fail/display/invalid_casing.rs create mode 100644 tests/compile_fail/display/invalid_casing.stderr create mode 100644 tests/compile_fail/display/rename_all_on_struct.rs create mode 100644 tests/compile_fail/display/rename_all_on_struct.stderr diff --git a/impl/Cargo.toml b/impl/Cargo.toml index b836827b..a1d309ab 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -53,10 +53,10 @@ add = [] add_assign = [] as_ref = ["syn/extra-traits", "syn/visit"] constructor = [] -debug = ["syn/extra-traits", "dep:unicode-xid"] +debug = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"] deref = [] deref_mut = [] -display = ["syn/extra-traits", "dep:unicode-xid"] +display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"] error = ["syn/extra-traits"] from = ["syn/extra-traits"] from_str = [] diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index 758d8586..3a9a9d0c 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -11,7 +11,7 @@ use crate::utils::{attr::ParseMultiple as _, Spanning}; use super::{ trait_name_to_attribute_name, ContainerAttributes, ContainsGenericsExt as _, - FmtAttribute, + FmtAttribute, RenameAllAttribute, }; /// Expands a [`fmt::Display`]-like derive macro. @@ -29,9 +29,13 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result syn::Result<(Vec, TokenStream)> { let s = Expansion { shared_attr: None, + rename_all: None, attrs, fields: &s.fields, type_params, @@ -148,14 +153,18 @@ fn expand_enum( let (bounds, match_arms) = e.variants.iter().try_fold( (Vec::new(), TokenStream::new()), |(mut bounds, mut arms), variant| { - let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)? - .map(Spanning::into_inner) - .unwrap_or_default(); + let attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?; + if let Some(attrs) = &attrs { + attrs.validate_for_struct(attr_name)?; + }; + let attrs = attrs.map(Spanning::into_inner).unwrap_or_default(); + let ident = &variant.ident; if attrs.fmt.is_none() && variant.fields.is_empty() && attr_name != "display" + && container_attrs.rename_all.is_none() { return Err(syn::Error::new( e.variants.span(), @@ -168,6 +177,7 @@ fn expand_enum( let v = Expansion { shared_attr: container_attrs.fmt.as_ref(), + rename_all: container_attrs.rename_all, attrs: &attrs, fields: &variant.fields, type_params, @@ -234,6 +244,11 @@ struct Expansion<'a> { /// [`None`] for a struct. shared_attr: Option<&'a FmtAttribute>, + /// [`RenameAllAttribute`] placed on enum. + /// + /// [`None`] for a struct. + rename_all: Option, + /// Derive macro [`ContainerAttributes`]. attrs: &'a ContainerAttributes, @@ -305,7 +320,11 @@ impl Expansion<'_> { None => { if shared_attr_is_wrapping || !has_shared_attr { body = if self.fields.is_empty() { - let ident_str = self.ident.unraw().to_string(); + let ident_str = if let Some(rename_all) = &self.rename_all { + rename_all.convert_case(self.ident) + } else { + self.ident.unraw().to_string() + }; if shared_attr_is_wrapping { quote! { #ident_str } diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs index 63041a06..14e2c26f 100644 --- a/impl/src/fmt/mod.rs +++ b/impl/src/fmt/mod.rs @@ -8,6 +8,9 @@ pub(crate) mod debug; pub(crate) mod display; mod parsing; +use std::fmt::Display; + +use convert_case::{Case, Casing as _}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; use syn::{ @@ -16,7 +19,7 @@ use syn::{ parse_quote, punctuated::Punctuated, spanned::Spanned as _, - token, + token, LitStr, Token, }; use crate::{ @@ -88,6 +91,62 @@ impl BoundsAttribute { } } +/// Representation of a `rename_all` macro attribute. +/// +/// ```rust,ignore +/// #[(rename_all = "...")] +/// ``` +/// +/// Possible Cases: +/// - `lowercase` +/// - `UPPERCASE` +/// - `PascalCase` +/// - `camelCase` +/// - `snake_case` +/// - `SCREAMING_SNAKE_CASE` +/// - `kebab-case` +/// - `SCREAMING-KEBAB-CASE` +#[derive(Debug, Clone, Copy)] +struct RenameAllAttribute(Case); + +impl RenameAllAttribute { + fn convert_case(&self, ident: &syn::Ident) -> String { + ident.unraw().to_string().to_case(self.0) + } +} + +impl Parse for RenameAllAttribute { + fn parse(input: ParseStream<'_>) -> syn::Result { + let _ = input.parse::().and_then(|p| { + if p.is_ident("rename_all") { + Ok(p) + } else { + Err(syn::Error::new( + p.span(), + "unknown attribute argument, expected `rename_all = \"...\"`", + )) + } + })?; + + input.parse::()?; + + let value: LitStr = input.parse()?; + + // TODO should we really do a case insensitive comparision here? + Ok(Self(match value.value().replace(['-', '_'], "").to_lowercase().as_str() { + "lowercase" => Case::Flat, + "uppercase" => Case::UpperFlat, + "pascalcase" => Case::Pascal, + "camelcase" => Case::Camel, + "snakecase" => Case::Snake, + "screamingsnakecase" => Case::UpperSnake, + "kebabcase" => Case::Kebab, + "screamingkebabcase" => Case::UpperKebab, + _ => return Err(syn::Error::new_spanned(value, "unexpected casing expected one of: \"lowercase\", \"UPPERCASE\", \"PascalCase\", \"camelCase\", \"snake_case\", \"SCREAMING_SNAKE_CASE\", \"kebab-case\", or \"SCREAMING-KEBAB-CASE\"")) + })) + } +} + /// Representation of a [`fmt`]-like attribute. /// /// ```rust,ignore @@ -516,6 +575,30 @@ struct ContainerAttributes { /// Addition trait bounds. bounds: BoundsAttribute, + + /// Rename unit enum variants following a similar behavior as [`serde`](https://serde.rs/container-attrs.html#rename_all). + rename_all: Option, +} + +impl Spanning { + fn validate_for_struct(&self, attr_name: impl Display) -> syn::Result<()> { + if self.rename_all.is_some() { + Err(syn::Error::new( + self.span, + format_args!("`#[{attr_name}(rename_all=\"...\")]` can not be specified on structs or variants"), + )) + } else { + Ok(()) + } + } +} + +mod kw { + use syn::custom_keyword; + + custom_keyword!(rename_all); + custom_keyword!(bounds); + custom_keyword!(bound); } impl Parse for ContainerAttributes { @@ -523,12 +606,50 @@ impl Parse for ContainerAttributes { // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow // any error of the `Either::Left` if the `Either::Right` succeeds. FmtAttribute::check_legacy_fmt(input)?; - >::parse(input).map(|v| match v { - Either::Left(fmt) => Self { + let lookahead = input.lookahead1(); + Ok(if lookahead.peek(LitStr) { + Self { + fmt: Some(input.parse()?), bounds: BoundsAttribute::default(), - fmt: Some(fmt), - }, - Either::Right(bounds) => Self { bounds, fmt: None }, + rename_all: None, + } + } else if lookahead.peek(kw::rename_all) + || lookahead.peek(kw::bounds) + || lookahead.peek(kw::bound) + || lookahead.peek(Token![where]) + { + let mut bounds = BoundsAttribute::default(); + let mut rename_all = None; + + while !input.is_empty() { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename_all) { + if rename_all.is_some() { + return Err( + input.error("`rename_all` can only be specified once") + ); + } else { + rename_all = Some(input.parse()?); + } + } else if lookahead.peek(kw::bounds) + || lookahead.peek(kw::bound) + || lookahead.peek(Token![where]) + { + bounds.0.extend(input.parse::()?.0) + } else { + return Err(lookahead.error()); + } + if !input.is_empty() { + input.parse::()?; + } + } + Self { + fmt: None, + bounds, + rename_all, + } + } else { + return Err(lookahead.error()); }) } } @@ -554,6 +675,16 @@ impl attr::ParseMultiple for ContainerAttributes { format!("multiple `#[{name}(\"...\", ...)]` attributes aren't allowed"), )); } + if new + .rename_all + .and_then(|n| prev.rename_all.replace(n)) + .is_some() + { + return Err(syn::Error::new( + new_span, + format!("multiple `#[{name}(rename_all=\"...\")]` attributes aren't allowed"), + )); + } prev.bounds.0.extend(new.bounds.0); Ok(Spanning::new( @@ -582,7 +713,7 @@ where } } -/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to travers its type parameters. +/// Extension of a [`syn::Type`] and a [`syn::Path`] allowing to traverse its type parameters. trait ContainsGenericsExt { /// Checks whether this definition contains any of the provided `type_params`. fn contains_generics(&self, type_params: &[&syn::Ident]) -> bool; diff --git a/tests/compile_fail/debug/unknown_attribute.stderr b/tests/compile_fail/debug/unknown_attribute.stderr index 1820df7b..c634447a 100644 --- a/tests/compile_fail/debug/unknown_attribute.stderr +++ b/tests/compile_fail/debug/unknown_attribute.stderr @@ -1,4 +1,4 @@ -error: unknown attribute argument, expected `bound(...)` +error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where` --> tests/compile_fail/debug/unknown_attribute.rs:2:9 | 2 | #[debug(unknown = "unknown")] diff --git a/tests/compile_fail/display/invalid_casing.rs b/tests/compile_fail/display/invalid_casing.rs new file mode 100644 index 00000000..461cc50d --- /dev/null +++ b/tests/compile_fail/display/invalid_casing.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display(rename_all = "Whatever")] +enum Enum { + UnitVariant, +} + +fn main() {} diff --git a/tests/compile_fail/display/invalid_casing.stderr b/tests/compile_fail/display/invalid_casing.stderr new file mode 100644 index 00000000..60f687a6 --- /dev/null +++ b/tests/compile_fail/display/invalid_casing.stderr @@ -0,0 +1,5 @@ +error: unexpected casing expected one of: "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE", "kebab-case", or "SCREAMING-KEBAB-CASE" + --> tests/compile_fail/display/invalid_casing.rs:2:24 + | +2 | #[display(rename_all = "Whatever")] + | ^^^^^^^^^^ diff --git a/tests/compile_fail/display/rename_all_on_struct.rs b/tests/compile_fail/display/rename_all_on_struct.rs new file mode 100644 index 00000000..9e2d9d1c --- /dev/null +++ b/tests/compile_fail/display/rename_all_on_struct.rs @@ -0,0 +1,11 @@ +#[derive(derive_more::Display)] +enum Enum { + #[display(rename_all = "lowercase")] + RenameAllOnVariant, +} + +#[derive(derive_more::Display)] +#[display(rename_all = "lowercase")] +struct Struct; + +fn main() {} diff --git a/tests/compile_fail/display/rename_all_on_struct.stderr b/tests/compile_fail/display/rename_all_on_struct.stderr new file mode 100644 index 00000000..d80364e9 --- /dev/null +++ b/tests/compile_fail/display/rename_all_on_struct.stderr @@ -0,0 +1,11 @@ +error: `#[display(rename_all="...")]` can not be specified on structs or variants + --> tests/compile_fail/display/rename_all_on_struct.rs:3:5 + | +3 | #[display(rename_all = "lowercase")] + | ^ + +error: `#[display(rename_all="...")]` can not be specified on structs or variants + --> tests/compile_fail/display/rename_all_on_struct.rs:8:1 + | +8 | #[display(rename_all = "lowercase")] + | ^ diff --git a/tests/compile_fail/display/unknown_attribute.stderr b/tests/compile_fail/display/unknown_attribute.stderr index be551fc0..d18706cb 100644 --- a/tests/compile_fail/display/unknown_attribute.stderr +++ b/tests/compile_fail/display/unknown_attribute.stderr @@ -1,4 +1,4 @@ -error: unknown attribute argument, expected `bound(...)` +error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where` --> tests/compile_fail/display/unknown_attribute.rs:3:11 | 3 | #[display(unknown = "unknown")] diff --git a/tests/display.rs b/tests/display.rs index 478e1d94..00c38b84 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -1797,6 +1797,63 @@ mod enums { } } } + + mod rename_all { + use super::*; + + macro_rules! casing_test { + ($name:ident, $casing:literal, $VariantOne:literal, $Two:literal) => { + #[test] + fn $name() { + #[derive( + Binary, Display, LowerExp, LowerHex, Octal, Pointer, UpperExp, + UpperHex + )] + #[binary(rename_all = $casing)] + #[display(rename_all = $casing)] + #[lower_exp(rename_all = $casing)] + #[lower_hex(rename_all = $casing)] + #[octal(rename_all = $casing)] + #[pointer(rename_all = $casing)] + #[upper_exp(rename_all = $casing)] + #[upper_hex(rename_all = $casing)] + enum Enum { + VariantOne, + Two, + } + + assert_eq!(Enum::VariantOne.to_string(), $VariantOne); + assert_eq!(Enum::Two.to_string(), $Two); + assert_eq!(format!("{:b}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:e}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:x}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:o}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:p}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:E}", Enum::VariantOne), $VariantOne); + assert_eq!(format!("{:X}", Enum::VariantOne), $VariantOne); + } + }; + } + + casing_test!(lower_case, "lowercase", "variantone", "two"); + casing_test!(upper_case, "UPPERCASE", "VARIANTONE", "TWO"); + casing_test!(pascal_case, "PascalCase", "VariantOne", "Two"); + casing_test!(camel_case, "camelCase", "variantOne", "two"); + casing_test!(snake_case, "snake_case", "variant_one", "two"); + casing_test!( + screaming_snake_case, + "SCREAMING_SNAKE_CASE", + "VARIANT_ONE", + "TWO" + ); + casing_test!(kebab_case, "kebab-case", "variant-one", "two"); + casing_test!( + screaming_kebab_case, + "SCREAMING-KEBAB-CASE", + "VARIANT-ONE", + "TWO" + ); + } } mod generic { From 2ebbf3ea05fb5a2013efc71a5dc66efc17f8ef29 Mon Sep 17 00:00:00 2001 From: Roland Fredenhagen Date: Mon, 3 Feb 2025 00:43:07 +0100 Subject: [PATCH 2/2] improve debug --- impl/Cargo.toml | 2 +- impl/src/fmt/debug.rs | 8 +- impl/src/fmt/display.rs | 4 +- impl/src/fmt/mod.rs | 87 ++++++++++++++----- .../debug/unknown_attribute.stderr | 2 +- 5 files changed, 73 insertions(+), 30 deletions(-) diff --git a/impl/Cargo.toml b/impl/Cargo.toml index a1d309ab..330dd8a8 100644 --- a/impl/Cargo.toml +++ b/impl/Cargo.toml @@ -53,7 +53,7 @@ add = [] add_assign = [] as_ref = ["syn/extra-traits", "syn/visit"] constructor = [] -debug = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"] +debug = ["syn/extra-traits", "dep:unicode-xid"] deref = [] deref_mut = [] display = ["syn/extra-traits", "dep:unicode-xid", "dep:convert_case"] diff --git a/impl/src/fmt/debug.rs b/impl/src/fmt/debug.rs index e2c8bc83..95cd0d55 100644 --- a/impl/src/fmt/debug.rs +++ b/impl/src/fmt/debug.rs @@ -2,6 +2,8 @@ //! //! [`fmt::Debug`]: std::fmt::Debug +use std::convert::Infallible; + use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; @@ -77,7 +79,7 @@ pub fn expand(input: &syn::DeriveInput, _: &str) -> syn::Result { /// /// [`fmt::Debug`]: std::fmt::Debug fn expand_struct( - attrs: ContainerAttributes, + attrs: ContainerAttributes, ident: &syn::Ident, s: &syn::DataStruct, type_params: &[&syn::Ident], @@ -115,7 +117,7 @@ fn expand_struct( /// /// [`fmt::Debug`]: std::fmt::Debug fn expand_enum( - mut attrs: ContainerAttributes, + mut attrs: ContainerAttributes, e: &syn::DataEnum, type_params: &[&syn::Ident], attr_name: &syn::Ident, @@ -207,7 +209,7 @@ type FieldAttribute = Either; /// [`Debug::fmt()`]: std::fmt::Debug::fmt() #[derive(Debug)] struct Expansion<'a> { - attr: &'a ContainerAttributes, + attr: &'a ContainerAttributes, /// Struct or enum [`Ident`](struct@syn::Ident). ident: &'a syn::Ident, diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index 3a9a9d0c..d6d023fd 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -87,7 +87,7 @@ pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result = ( - &'a ContainerAttributes, + &'a ContainerAttributes, &'a [&'a syn::Ident], &'a syn::Ident, &'a syn::Ident, @@ -250,7 +250,7 @@ struct Expansion<'a> { rename_all: Option, /// Derive macro [`ContainerAttributes`]. - attrs: &'a ContainerAttributes, + attrs: &'a ContainerAttributes, /// Struct or enum [`syn::Ident`]. /// diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs index 14e2c26f..596e2026 100644 --- a/impl/src/fmt/mod.rs +++ b/impl/src/fmt/mod.rs @@ -8,8 +8,7 @@ pub(crate) mod debug; pub(crate) mod display; mod parsing; -use std::fmt::Display; - +#[cfg(feature = "display")] use convert_case::{Case, Casing as _}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -19,7 +18,7 @@ use syn::{ parse_quote, punctuated::Punctuated, spanned::Spanned as _, - token, LitStr, Token, + token, }; use crate::{ @@ -106,15 +105,18 @@ impl BoundsAttribute { /// - `SCREAMING_SNAKE_CASE` /// - `kebab-case` /// - `SCREAMING-KEBAB-CASE` +#[cfg(feature = "display")] #[derive(Debug, Clone, Copy)] -struct RenameAllAttribute(Case); +struct RenameAllAttribute(#[allow(unused)] Case); +#[cfg(feature = "display")] impl RenameAllAttribute { fn convert_case(&self, ident: &syn::Ident) -> String { ident.unraw().to_string().to_case(self.0) } } +#[cfg(feature = "display")] impl Parse for RenameAllAttribute { fn parse(input: ParseStream<'_>) -> syn::Result { let _ = input.parse::().and_then(|p| { @@ -128,9 +130,9 @@ impl Parse for RenameAllAttribute { } })?; - input.parse::()?; + input.parse::()?; - let value: LitStr = input.parse()?; + let value: syn::LitStr = input.parse()?; // TODO should we really do a case insensitive comparision here? Ok(Self(match value.value().replace(['-', '_'], "").to_lowercase().as_str() { @@ -568,8 +570,8 @@ impl Placeholder { /// are allowed. /// /// [`fmt::Display`]: std::fmt::Display -#[derive(Debug, Default)] -struct ContainerAttributes { +#[derive(Debug)] +struct ContainerAttributes { /// Interpolation [`FmtAttribute`]. fmt: Option, @@ -577,11 +579,25 @@ struct ContainerAttributes { bounds: BoundsAttribute, /// Rename unit enum variants following a similar behavior as [`serde`](https://serde.rs/container-attrs.html#rename_all). - rename_all: Option, + rename_all: Option, } -impl Spanning { - fn validate_for_struct(&self, attr_name: impl Display) -> syn::Result<()> { +impl std::default::Default for ContainerAttributes { + fn default() -> Self { + Self { + fmt: None, + bounds: BoundsAttribute::default(), + rename_all: None, + } + } +} + +#[cfg(feature = "display")] +impl Spanning> { + fn validate_for_struct( + &self, + attr_name: impl std::fmt::Display, + ) -> syn::Result<()> { if self.rename_all.is_some() { Err(syn::Error::new( self.span, @@ -593,21 +609,43 @@ impl Spanning { } } -mod kw { - use syn::custom_keyword; - - custom_keyword!(rename_all); - custom_keyword!(bounds); - custom_keyword!(bound); +#[cfg(feature = "debug")] +impl Parse for ContainerAttributes { + fn parse(input: ParseStream<'_>) -> syn::Result { + // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow + // any error of the `Either::Left` if the `Either::Right` succeeds. + FmtAttribute::check_legacy_fmt(input)?; + >::parse(input).map(|v| match v { + Either::Left(fmt) => Self { + bounds: BoundsAttribute::default(), + fmt: Some(fmt), + rename_all: None, + }, + Either::Right(bounds) => Self { + bounds, + fmt: None, + rename_all: None, + }, + }) + } } -impl Parse for ContainerAttributes { +#[cfg(feature = "display")] +impl Parse for ContainerAttributes { fn parse(input: ParseStream<'_>) -> syn::Result { + mod kw { + use syn::custom_keyword; + + custom_keyword!(rename_all); + custom_keyword!(bounds); + custom_keyword!(bound); + } + // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `Either` will swallow // any error of the `Either::Left` if the `Either::Right` succeeds. FmtAttribute::check_legacy_fmt(input)?; let lookahead = input.lookahead1(); - Ok(if lookahead.peek(LitStr) { + Ok(if lookahead.peek(syn::LitStr) { Self { fmt: Some(input.parse()?), bounds: BoundsAttribute::default(), @@ -616,7 +654,7 @@ impl Parse for ContainerAttributes { } else if lookahead.peek(kw::rename_all) || lookahead.peek(kw::bounds) || lookahead.peek(kw::bound) - || lookahead.peek(Token![where]) + || lookahead.peek(syn::Token![where]) { let mut bounds = BoundsAttribute::default(); let mut rename_all = None; @@ -633,14 +671,14 @@ impl Parse for ContainerAttributes { } } else if lookahead.peek(kw::bounds) || lookahead.peek(kw::bound) - || lookahead.peek(Token![where]) + || lookahead.peek(syn::Token![where]) { bounds.0.extend(input.parse::()?.0) } else { return Err(lookahead.error()); } if !input.is_empty() { - input.parse::()?; + input.parse::()?; } } Self { @@ -654,7 +692,10 @@ impl Parse for ContainerAttributes { } } -impl attr::ParseMultiple for ContainerAttributes { +impl attr::ParseMultiple for ContainerAttributes +where + ContainerAttributes: Parse, +{ fn merge_attrs( prev: Spanning, new: Spanning, diff --git a/tests/compile_fail/debug/unknown_attribute.stderr b/tests/compile_fail/debug/unknown_attribute.stderr index c634447a..1820df7b 100644 --- a/tests/compile_fail/debug/unknown_attribute.stderr +++ b/tests/compile_fail/debug/unknown_attribute.stderr @@ -1,4 +1,4 @@ -error: expected one of: string literal, `rename_all`, `bounds`, `bound`, `where` +error: unknown attribute argument, expected `bound(...)` --> tests/compile_fail/debug/unknown_attribute.rs:2:9 | 2 | #[debug(unknown = "unknown")]