diff --git a/CHANGELOG.md b/CHANGELOG.md index 413d1968..576d050b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - `use derive_more::SomeTrait` now imports macro only. Importing macro with its trait along is possible now via `use derive_more::with_trait::SomeTrait`. ([#406](https://github.com/JelteF/derive_more/pull/406)) +- Top-level `#[display("...")]` attribute on an enum now has defaulting behavior + instead of replacing when no wrapping is possible (no `_variant` placeholder). + ([#395](https://github.com/JelteF/derive_more/pull/395)) ### Fixed @@ -24,6 +27,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Ambiguous associated item error when deriving `TryFrom`, `TryInto` or `FromStr` with an associated item called `Error` or `Err` respectively. ([#410](https://github.com/JelteF/derive_more/pull/410)) +- Top-level `#[display("...")]` attribute on an enum being incorrectly treated + as transparent or wrapping. + ([#395](https://github.com/JelteF/derive_more/pull/395)) ## 1.0.0 - 2024-08-07 diff --git a/impl/doc/display.md b/impl/doc/display.md index 0bdf3425..49bbe1a8 100644 --- a/impl/doc/display.md +++ b/impl/doc/display.md @@ -45,12 +45,6 @@ let a = &123; assert_eq!(format!("{}", RefInt{field: &a}), format!("{a:p} {:p}", a)); ``` -For enums you can also specify a shared format on the enum itself instead of -the variant. This format is used for each of the variants, and can be -customized per variant by including the special `{_variant}` placeholder in -this shared format, which is then replaced by the format string that's provided -on the variant. - ### Other formatting traits @@ -180,6 +174,57 @@ assert_eq!(format!("{:07}", MyOctalInt(9)), "11"); ``` +### Shared enum format + +Enums can have shared top-level `#[display("...", args...)]` attribute. Depending on its contents, +it can act either as a default format or a wrapping one. + +#### Wrapping enum format + +To act as a wrapping format, the shared top-level `#[display("...", args...)]` attribute should +contain at least one special `{_variant}` placeholder, which is then replaced by the format string +that's provided (or inferred) on the variant. +```rust +# use derive_more::Display; +# +#[derive(Display)] +#[display("Variant: {_variant} & {}", _variant)] +enum Enum { + #[display("A {_0}")] + A(i32), + B { field: i32 }, + #[display("c")] + C, +} + +assert_eq!(Enum::A(1).to_string(), "Variant: A 1 & A 1"); +assert_eq!(Enum::B { field: 2 }.to_string(), "Variant: 2 & 2"); +assert_eq!(Enum::C.to_string(), "Variant: c & c"); +``` + +#### Default enum format + +If the shared top-level `#[display("...", args...)]` attribute contains no `{_variant}` placeholders, +then it acts as the default one for the variants without its own format. +```rust +# use derive_more::Display; +# +#[derive(Display)] +#[display("Variant: {_0} & {}", _0)] // fields can be used too! +enum Enum { + #[display("A {_0}")] + A(i32), + B(u32), + #[display("c")] + C, +} + +assert_eq!(Enum::A(1).to_string(), "A 1"); +assert_eq!(Enum::B(2).to_string(), "Variant: 2 & 2"); +assert_eq!(Enum::C.to_string(), "c"); +``` + + ## Example usage diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index 2f6c52fd..10cf3200 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -255,97 +255,103 @@ struct Expansion<'a> { } impl Expansion<'_> { + /// Checks and indicates whether a top-level shared [`FmtAttribute`] is present in this + /// [`Expansion`], and whether it has wrapping logic (e.g. uses `_variant` placeholder). + fn shared_attr_info(&self) -> (bool, bool) { + let shared_attr_contains_variant = self + .shared_attr + .map_or(true, |attr| attr.contains_arg("_variant")); + // If `shared_attr` is a transparent call to `_variant`, then we consider it being absent. + let has_shared_attr = self.shared_attr.map_or(false, |attr| { + attr.transparent_call().map_or(true, |(_, called_trait)| { + &called_trait != self.trait_ident || !shared_attr_contains_variant + }) + }); + ( + has_shared_attr, + has_shared_attr && shared_attr_contains_variant, + ) + } + /// Generates [`Display::fmt()`] implementation for a struct or an enum variant. /// /// # Errors /// - /// In case [`FmtAttribute`] is [`None`] and [`syn::Fields`] length is - /// greater than 1. + /// In case [`FmtAttribute`] is [`None`] and [`syn::Fields`] length is greater than 1. /// /// [`Display::fmt()`]: fmt::Display::fmt() fn generate_body(&self) -> syn::Result { let mut body = TokenStream::new(); - // If `shared_attr` is a transparent call, then we consider it being absent. - let has_shared_attr = self - .shared_attr - .map_or(false, |a| a.transparent_call().is_none()); + let (has_shared_attr, shared_attr_is_wrapping) = self.shared_attr_info(); - if !has_shared_attr - || self - .shared_attr - .map_or(true, |a| a.contains_arg("_variant")) - { - body = match &self.attrs.fmt { - Some(fmt) => { - if has_shared_attr { - let deref_args = fmt.additional_deref_args(self.fields); - - quote! { &derive_more::core::format_args!(#fmt, #(#deref_args),*) } - } else if let Some((expr, trait_ident)) = fmt.transparent_call() { - let expr = - if self.fields.fmt_args_idents().any(|field| expr == field) - { - quote! { #expr } - } else { - quote! { &(#expr) } - }; - - quote! { - derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) - } - } else { - let deref_args = fmt.additional_deref_args(self.fields); + let wrap_into_shared_attr = match &self.attrs.fmt { + Some(fmt) => { + body = if shared_attr_is_wrapping { + let deref_args = fmt.additional_deref_args(self.fields); - quote! { - derive_more::core::write!(__derive_more_f, #fmt, #(#deref_args),*) - } - } - } - None if self.fields.is_empty() => { - let ident_str = self.ident.unraw().to_string(); + quote! { &derive_more::core::format_args!(#fmt, #(#deref_args),*) } + } else if let Some((expr, trait_ident)) = fmt.transparent_call() { + let expr = + if self.fields.fmt_args_idents().any(|field| expr == field) { + quote! { #expr } + } else { + quote! { &(#expr) } + }; - if has_shared_attr { - quote! { #ident_str } - } else { - quote! { __derive_more_f.write_str(#ident_str) } - } - } - None if self.fields.len() == 1 => { - let field = self - .fields - .iter() - .next() - .unwrap_or_else(|| unreachable!("count() == 1")); - let ident = - field.ident.clone().unwrap_or_else(|| format_ident!("_0")); - let trait_ident = self.trait_ident; - - if has_shared_attr { - let placeholder = - trait_name_to_default_placeholder_literal(trait_ident); - - quote! { &derive_more::core::format_args!(#placeholder, #ident) } - } else { - quote! { - derive_more::core::fmt::#trait_ident::fmt(#ident, __derive_more_f) + quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) } + } else { + let deref_args = fmt.additional_deref_args(self.fields); + + quote! { derive_more::core::write!(__derive_more_f, #fmt, #(#deref_args),*) } + }; + shared_attr_is_wrapping + } + None => { + if shared_attr_is_wrapping || !has_shared_attr { + body = if self.fields.is_empty() { + let ident_str = self.ident.unraw().to_string(); + + if shared_attr_is_wrapping { + quote! { #ident_str } + } else { + quote! { __derive_more_f.write_str(#ident_str) } } - } - } - _ => { - return Err(syn::Error::new( - self.fields.span(), - format!( - "struct or enum variant with more than 1 field must have \ - `#[{}(\"...\", ...)]` attribute", - trait_name_to_attribute_name(self.trait_ident), - ), - )) + } else if self.fields.len() == 1 { + let field = self + .fields + .iter() + .next() + .unwrap_or_else(|| unreachable!("count() == 1")); + let ident = + field.ident.clone().unwrap_or_else(|| format_ident!("_0")); + let trait_ident = self.trait_ident; + + if shared_attr_is_wrapping { + let placeholder = + trait_name_to_default_placeholder_literal(trait_ident); + + quote! { &derive_more::core::format_args!(#placeholder, #ident) } + } else { + quote! { + derive_more::core::fmt::#trait_ident::fmt(#ident, __derive_more_f) + } + } + } else { + return Err(syn::Error::new( + self.fields.span(), + format!( + "struct or enum variant with more than 1 field must have \ + `#[{}(\"...\", ...)]` attribute", + trait_name_to_attribute_name(self.trait_ident), + ), + )); + }; } - }; - } - - if has_shared_attr { + has_shared_attr + } + }; + if wrap_into_shared_attr { if let Some(shared_fmt) = &self.shared_attr { let deref_args = shared_fmt.additional_deref_args(self.fields); @@ -368,13 +374,12 @@ impl Expansion<'_> { fn generate_bounds(&self) -> Vec { let mut bounds = vec![]; - if self - .shared_attr - .map_or(true, |a| a.contains_arg("_variant")) - { - if let Some(fmt) = &self.attrs.fmt { + let (has_shared_attr, shared_attr_is_wrapping) = self.shared_attr_info(); + + let mix_shared_attr_bounds = match &self.attrs.fmt { + Some(attr) => { bounds.extend( - fmt.bounded_types(self.fields) + attr.bounded_types(self.fields) .filter_map(|(ty, trait_name)| { if !ty.contains_generics(self.type_params) { return None; @@ -385,29 +390,37 @@ impl Expansion<'_> { }) .chain(self.attrs.bounds.0.clone()), ); - } else { - bounds.extend(self.fields.iter().next().and_then(|f| { - let ty = &f.ty; - if !ty.contains_generics(self.type_params) { - return None; - } - let trait_ident = &self.trait_ident; - Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident }) - })); - }; - } - - if let Some(shared_fmt) = &self.shared_attr { - bounds.extend(shared_fmt.bounded_types(self.fields).filter_map( - |(ty, trait_name)| { - if !ty.contains_generics(self.type_params) { - return None; - } - let trait_ident = format_ident!("{trait_name}"); + shared_attr_is_wrapping + } + None => { + if shared_attr_is_wrapping || !has_shared_attr { + bounds.extend(self.fields.iter().next().and_then(|f| { + let ty = &f.ty; + if !ty.contains_generics(self.type_params) { + return None; + } + let trait_ident = &self.trait_ident; + Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident }) + })); + } + has_shared_attr + } + }; + if mix_shared_attr_bounds { + bounds.extend( + self.shared_attr + .as_ref() + .unwrap() + .bounded_types(self.fields) + .filter_map(|(ty, trait_name)| { + if !ty.contains_generics(self.type_params) { + return None; + } + let trait_ident = format_ident!("{trait_name}"); - Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident }) - }, - )); + Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident }) + }), + ); } bounds diff --git a/tests/display.rs b/tests/display.rs index fe899436..66298bdd 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -1384,6 +1384,8 @@ mod enums { field: i32, }, C, + #[display("D {_0} {}", _1)] + D(i8, u8), } #[test] @@ -1391,6 +1393,7 @@ mod enums { assert_eq!(Enum::A(1).to_string(), "Variant: A 1"); assert_eq!(Enum::B { field: 2 }.to_string(), "Variant: B 2"); assert_eq!(Enum::C.to_string(), "Variant: C"); + assert_eq!(Enum::D(1, 2).to_string(), "Variant: D 1 2"); } } @@ -1407,6 +1410,8 @@ mod enums { field: i32, }, C, + #[display("D {_0} {}", _1)] + D(i8, u8), #[display("{_0:b}")] TransparentBinary(i32), } @@ -1416,6 +1421,7 @@ mod enums { assert_eq!(Enum::A(1).to_string(), "A 1"); assert_eq!(Enum::B { field: 2 }.to_string(), "B 2"); assert_eq!(Enum::C.to_string(), "C"); + assert_eq!(Enum::D(1, 2).to_string(), "D 1 2"); assert_eq!( format!("{:08}", Enum::TransparentBinary(4)), "00000100", @@ -1436,6 +1442,8 @@ mod enums { field: i32, }, C, + #[display("D {_0} {}", _1)] + D(i8, u8), } #[test] @@ -1446,6 +1454,7 @@ mod enums { "B 2 Variant: B 2 B 2", ); assert_eq!(Enum::C.to_string(), "C Variant: C C"); + assert_eq!(Enum::D(1, 2).to_string(), "D 1 2 Variant: D 1 2 D 1 2",); } } @@ -1465,18 +1474,26 @@ mod enums { field: i32, }, C, - D(T), + #[display("D {_0} {}", _1)] + D(i8, u8), + E { + a: u8, + b: i8, + }, + X(T), } #[test] fn assert() { - assert_eq!(Enum::::A(1).to_string(), "Variant"); + assert_eq!(Enum::::A(1).to_string(), "A 1"); + assert_eq!(Enum::::B { field: 2 }.to_string(), "B 2",); + assert_eq!(Enum::::C.to_string(), "Variant"); + assert_eq!(Enum::::D(1, 2).to_string(), "D 1 2"); assert_eq!( - Enum::::B { field: 2 }.to_string(), - "Variant", + Enum::::E { a: 2, b: 1 }.to_string(), + "Variant" ); - assert_eq!(Enum::::C.to_string(), "Variant"); - assert_eq!(Enum::::D(NoDisplay).to_string(), "Variant"); + assert_eq!(Enum::::X(NoDisplay).to_string(), "Variant"); } } @@ -1488,14 +1505,83 @@ mod enums { enum Enum { A(i32), B(&'static str), - C(T), + #[display("D {_0} {}", _1)] + D(i8, u8), + E(u8, i8), + X(T), } #[test] fn assert() { assert_eq!(Enum::::A(1).to_string(), "Variant 1"); assert_eq!(Enum::::B("abc").to_string(), "Variant abc"); - assert_eq!(Enum::::C(9).to_string(), "Variant 9"); + assert_eq!(Enum::::D(1, 2).to_string(), "D 1 2"); + assert_eq!(Enum::::E(2, 1).to_string(), "Variant 2"); + assert_eq!(Enum::::X(9).to_string(), "Variant 9"); + } + } + + mod only_field { + use super::*; + + /// Make sure that top-level-specific bounds are not added if a field is not used. + struct NoDisplay; + + #[derive(Display)] + #[display("{_0}")] + enum Enum { + #[display("A")] + A(i32), + #[display("B")] + B(&'static str), + #[display("D {_0} {}", _1)] + D(i8, u8), + E(u8, i8), + #[display("X")] + X(X), + Y(Y), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "A"); + assert_eq!(Enum::::B("abc").to_string(), "B"); + assert_eq!(Enum::::D(1, 2).to_string(), "D 1 2"); + assert_eq!(Enum::::E(2, 1).to_string(), "2"); + assert_eq!(Enum::::X(NoDisplay).to_string(), "X"); + assert_eq!(Enum::::Y(9).to_string(), "9"); + } + } + + mod only_multiple_field { + use super::*; + + /// Make sure that top-level-specific bounds are not added if a field is not used. + struct NoDisplay; + + #[derive(Display)] + #[display("{_0} & {_1}")] + enum Enum { + #[display("A")] + A(i32), + #[display("B")] + B(&'static str), + #[display("D {_0} {}", _1)] + D(i8, u8), + E(u8, i8), + #[display("X")] + X(X), + Y(Y, i8), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "A"); + assert_eq!(Enum::::B("abc").to_string(), "B"); + assert_eq!(Enum::::D(1, 2).to_string(), "D 1 2"); + assert_eq!(Enum::::E(2, 1).to_string(), "2 & 1"); + assert_eq!(Enum::::X(NoDisplay).to_string(), "X"); + assert_eq!(Enum::::Y(9, 10).to_string(), "9 & 10"); } } @@ -1509,15 +1595,51 @@ mod enums { A(i32), #[display("B")] B(&'static str), - #[display("C")] - C(T), + #[display("D {_0} {}", _1)] + D(i8, u8), + #[display("X")] + X(T), + Y(T), } #[test] fn assert() { assert_eq!(Enum::::A(1).to_string(), "Variant A 1"); assert_eq!(Enum::::B("abc").to_string(), "Variant B abc"); - assert_eq!(Enum::::C(9).to_string(), "Variant C 9"); + assert_eq!(Enum::::D(1, 2).to_string(), "Variant D 1 2 1"); + assert_eq!(Enum::::X(9).to_string(), "Variant X 9"); + assert_eq!(Enum::::Y(9).to_string(), "Variant 9 9"); + } + } + + mod as_debug { + use super::*; + + #[derive(Debug, Display)] + #[display("{self:?}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + #[display("D {_0} {}", _1)] + D(i8, u8), + E { + a: u8, + b: i8, + }, + } + + #[test] + fn assert() { + assert_eq!(Enum::A(1).to_string(), "A 1"); + assert_eq!(Enum::B { field: 2 }.to_string(), "B 2"); + assert_eq!(Enum::C.to_string(), "C"); + assert_eq!(Enum::D(1, 2).to_string(), "D 1 2"); + assert_eq!(Enum::E { a: 2, b: 1 }.to_string(), "E { a: 2, b: 1 }"); } }