From 8a172f2672de58d05af27301d87b9ea81b1a70fc Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Thu, 25 Jul 2024 13:40:01 +0200 Subject: [PATCH] Support `_variant` in outer level enum formatting for Display (#377, #142, #239) Resolves #142, #239 ## Synopsis This adds back support for top-level format strings of the Display derive. It now includes the display of the variant whenever the `_variant` placeholder appears be found. It also supports using the field. ## Solution This does not include the same support for Debug, since it is considered much less useful there and the differences between the Debug implementation and Display implementation make it a non-trivial port. Only named arguments are supported in the format string, no positional ones. This made the implementation easier, maybe in a future PR positional support can be added. This bumps MSRV to 1.70.0, on earlier versions the derived code throws the following error: ```rust error: there is no argument named `_0` --> tests/display.rs:1373:26 | 1373 | #[derive(Display)] | ^^^^^^^ | = note: did you intend to capture a variable `_0` from the surrounding scope? = note: to avoid ambiguity, `format_args!` cannot capture variables when the format string is expanded from a macro = note: this error originates in the derive macro `Display` (in Nightly builds, run with -Z macro-backtrace for more info) ``` Co-authored-by: Kai Ren --- CHANGELOG.md | 2 + clippy.toml | 2 - impl/doc/display.md | 22 +- impl/src/fmt/display.rs | 207 +++++++++++++----- impl/src/fmt/mod.rs | 96 +++++--- .../shared_format_positional_placeholders.rs | 19 ++ ...ared_format_positional_placeholders.stderr | 34 +++ .../display/shared_format_unclosed_brace.rs | 7 + .../shared_format_unclosed_brace.stderr | 9 + .../display/shared_format_variant_spec.rs | 7 + .../display/shared_format_variant_spec.stderr | 5 + tests/display.rs | 160 ++++++++++++++ 12 files changed, 473 insertions(+), 97 deletions(-) create mode 100644 tests/compile_fail/display/shared_format_positional_placeholders.rs create mode 100644 tests/compile_fail/display/shared_format_positional_placeholders.stderr create mode 100644 tests/compile_fail/display/shared_format_unclosed_brace.rs create mode 100644 tests/compile_fail/display/shared_format_unclosed_brace.stderr create mode 100644 tests/compile_fail/display/shared_format_variant_spec.rs create mode 100644 tests/compile_fail/display/shared_format_variant_spec.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b0f832..73d17971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/). [#294](https://github.com/JelteF/derive_more/pull/294)) - The `as_mut` feature is removed, and the `AsMut` derive is now gated by the `as_ref` feature. ([#295](https://github.com/JelteF/derive_more/pull/295)) +- A top level `#[display("...")]` attribute on an enum now requires the usage + of `{_variant}` to include the variant instead of including it at `{}`. ([#377](https://github.com/JelteF/derive_more/pull/377)) ### Added diff --git a/clippy.toml b/clippy.toml index 0c697c31..2487d144 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,8 +2,6 @@ # See full lints list at: # https://rust-lang.github.io/rust-clippy/master/index.html -msrv = "1.65.0" - # Ensures consistent bracing for macro calls in the codebase. # Extends default settings: # https://github.com/rust-lang/rust-clippy/blob/master/clippy_lints/src/nonstandard_macro_braces.rs#L143-L184 diff --git a/impl/doc/display.md b/impl/doc/display.md index 5c242bdf..6a7139e6 100644 --- a/impl/doc/display.md +++ b/impl/doc/display.md @@ -26,6 +26,12 @@ The variables available in the arguments is `self` and each member of the varian with members of tuple structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc. +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 @@ -175,6 +181,7 @@ struct Point2D { } #[derive(Display)] +#[display("Enum E: {_variant}")] enum E { Uint(u32), #[display("I am B {:b}", i)] @@ -185,6 +192,13 @@ enum E { Path(PathBuf), } +#[derive(Display)] +#[display("Enum E2: {_0:?}")] +enum E2 { + Uint(u32), + String(&'static str, &'static str), +} + #[derive(Display)] #[display("Hello there!")] union U { @@ -223,9 +237,11 @@ impl PositiveOrNegative { assert_eq!(MyInt(-2).to_string(), "-2"); assert_eq!(Point2D { x: 3, y: 4 }.to_string(), "(3, 4)"); -assert_eq!(E::Uint(2).to_string(), "2"); -assert_eq!(E::Binary { i: -2 }.to_string(), "I am B 11111110"); -assert_eq!(E::Path("abc".into()).to_string(), "I am C abc"); +assert_eq!(E::Uint(2).to_string(), "Enum E: 2"); +assert_eq!(E::Binary { i: -2 }.to_string(), "Enum E: I am B 11111110"); +assert_eq!(E::Path("abc".into()).to_string(), "Enum E: I am C abc"); +assert_eq!(E2::Uint(2).to_string(), "Enum E2: 2"); +assert_eq!(E2::String("shown", "ignored").to_string(), "Enum E2: \"shown\""); assert_eq!(U { i: 2 }.to_string(), "Hello there!"); assert_eq!(format!("{:o}", S), "7"); assert_eq!(format!("{:X}", UH), "UpperHex"); diff --git a/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index e41dcbd1..9e86bd57 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -5,11 +5,11 @@ use std::fmt; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{parse_quote, spanned::Spanned as _}; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::utils::{attr::ParseMultiple as _, Spanning}; -use super::{trait_name_to_attribute_name, ContainerAttributes}; +use super::{trait_name_to_attribute_name, ContainerAttributes, FmtAttribute}; /// Expands a [`fmt::Display`]-like derive macro. /// @@ -80,6 +80,7 @@ fn expand_struct( (attrs, ident, trait_ident, _): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { let s = Expansion { + shared_attr: None, attrs, fields: &s.fields, trait_ident, @@ -110,10 +111,21 @@ fn expand_struct( /// Expands a [`fmt`]-like derive macro for the provided enum. fn expand_enum( e: &syn::DataEnum, - (attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, + (container_attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { - if attrs.fmt.is_some() { - todo!("https://github.com/JelteF/derive_more/issues/142"); + if let Some(shared_fmt) = &container_attrs.fmt { + if shared_fmt + .placeholders_by_arg("_variant") + .any(|p| p.has_modifiers || p.trait_name != "Display") + { + // TODO: This limitation can be lifted, by analyzing the `shared_fmt` deeper and using + // `&dyn fmt::TraitName` for transparency instead of just `format_args!()` in the + // expansion. + return Err(syn::Error::new( + shared_fmt.span(), + "shared format `_variant` placeholder cannot contain format specifiers", + )); + } } let (bounds, match_arms) = e.variants.iter().try_fold( @@ -138,6 +150,7 @@ fn expand_enum( } let v = Expansion { + shared_attr: container_attrs.fmt.as_ref(), attrs: &attrs, fields: &variant.fields, trait_ident, @@ -198,6 +211,11 @@ fn expand_union( /// [`Display::fmt()`]: fmt::Display::fmt() #[derive(Debug)] struct Expansion<'a> { + /// [`FmtAttribute`] shared between all variants of an enum. + /// + /// [`None`] for a struct. + shared_attr: Option<&'a FmtAttribute>, + /// Derive macro [`ContainerAttributes`]. attrs: &'a ContainerAttributes, @@ -224,70 +242,129 @@ impl<'a> Expansion<'a> { /// greater than 1. /// /// [`Display::fmt()`]: fmt::Display::fmt() - /// [`FmtAttribute`]: super::FmtAttribute fn generate_body(&self) -> syn::Result { - match &self.attrs.fmt { - Some(fmt) => { - Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() { - quote! { derive_more::core::fmt::#trait_ident::fmt(&(#expr), __derive_more_f) } - } else { - quote! { derive_more::core::write!(__derive_more_f, #fmt) } - }) - } - None if self.fields.is_empty() => { - let ident_str = self.ident.to_string(); + 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()); + + 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 { + quote! { &derive_more::core::format_args!(#fmt) } + } else if let Some((expr, trait_ident)) = fmt.transparent_call() { + quote! { + derive_more::core::fmt::#trait_ident::fmt(&(#expr), __derive_more_f) + } + } else { + quote! { derive_more::core::write!(__derive_more_f, #fmt) } + } + } + None if self.fields.is_empty() => { + let ident_str = self.ident.unraw().to_string(); + + 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) + } + } + } + _ => { + 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), + ), + )) + } + }; + } - Ok(quote! { - derive_more::core::write!(__derive_more_f, #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; - - Ok(quote! { - derive_more::core::fmt::#trait_ident::fmt(#ident, __derive_more_f) - }) + if has_shared_attr { + if let Some(shared_fmt) = &self.shared_attr { + let shared_body = quote! { + derive_more::core::write!(__derive_more_f, #shared_fmt) + }; + + body = if body.is_empty() { + shared_body + } else { + quote! { match #body { _variant => #shared_body } } + } } - _ => 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), - ), - )), } + + Ok(body) } /// Generates trait bounds for a struct or an enum variant. fn generate_bounds(&self) -> Vec { - let Some(fmt) = &self.attrs.fmt else { - return self - .fields - .iter() - .next() - .map(|f| { + let mut bounds = vec![]; + + if self + .shared_attr + .map_or(true, |a| a.contains_arg("_variant")) + { + if let Some(fmt) = &self.attrs.fmt { + bounds.extend( + fmt.bounded_types(self.fields) + .map(|(ty, trait_name)| { + let trait_ident = format_ident!("{trait_name}"); + + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }) + .chain(self.attrs.bounds.0.clone()), + ); + } else { + bounds.extend(self.fields.iter().next().map(|f| { let ty = &f.ty; let trait_ident = &self.trait_ident; - vec![parse_quote! { #ty: derive_more::core::fmt::#trait_ident }] - }) - .unwrap_or_default(); - }; + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + })) + }; + } - fmt.bounded_types(self.fields) - .map(|(ty, trait_name)| { - let trait_ident = format_ident!("{trait_name}"); + if let Some(shared_fmt) = &self.shared_attr { + bounds.extend(shared_fmt.bounded_types(self.fields).map( + |(ty, trait_name)| { + let trait_ident = format_ident!("{trait_name}"); - parse_quote! { #ty: derive_more::core::fmt::#trait_ident } - }) - .chain(self.attrs.bounds.0.clone()) - .collect() + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }, + )); + } + + bounds } } @@ -305,3 +382,19 @@ fn normalize_trait_name(name: &str) -> &'static str { _ => unimplemented!(), } } + +/// Matches the provided [`fmt`] trait `name` to its default formatting placeholder. +fn trait_name_to_default_placeholder_literal(name: &syn::Ident) -> &'static str { + match () { + _ if name == "Binary" => "{:b}", + _ if name == "Debug" => "{:?}", + _ if name == "Display" => "{}", + _ if name == "LowerExp" => "{:e}", + _ if name == "LowerHex" => "{:x}", + _ if name == "Octal" => "{:o}", + _ if name == "Pointer" => "{:p}", + _ if name == "UpperExp" => "{:E}", + _ if name == "UpperHex" => "{:X}", + _ => unimplemented!(), + } +} diff --git a/impl/src/fmt/mod.rs b/impl/src/fmt/mod.rs index 324e574c..bd559c35 100644 --- a/impl/src/fmt/mod.rs +++ b/impl/src/fmt/mod.rs @@ -235,6 +235,44 @@ impl FmtAttribute { }) } + #[cfg(feature = "display")] + /// Checks whether this [`FmtAttribute`] contains an argument with the provided `name` (either + /// in its direct [`FmtArgument`]s or inside [`Placeholder`]s). + fn contains_arg(&self, name: &str) -> bool { + self.placeholders_by_arg(name).next().is_some() + } + + #[cfg(feature = "display")] + /// Returns an [`Iterator`] over [`Placeholder`]s using an argument with the provided `name` + /// (either in its direct [`FmtArgument`]s of this [`FmtAttribute`] or inside the + /// [`Placeholder`] itself). + fn placeholders_by_arg<'a>( + &'a self, + name: &'a str, + ) -> impl Iterator + 'a { + let placeholders = Placeholder::parse_fmt_string(&self.lit.value()); + + placeholders.into_iter().filter(move |placeholder| { + match &placeholder.arg { + Parameter::Named(name) => self + .args + .iter() + .find_map(|a| (a.alias()? == name).then_some(&a.expr)) + .map_or(Some(name.clone()), |expr| { + expr.ident().map(ToString::to_string) + }), + Parameter::Positional(i) => self + .args + .iter() + .nth(*i) + .and_then(|a| a.expr.ident().filter(|_| a.alias.is_none())) + .map(ToString::to_string), + } + .as_deref() + == Some(name) + }) + } + /// Errors in case legacy syntax is encountered: `fmt = "...", (arg),*`. fn check_legacy_fmt(input: ParseStream<'_>) -> syn::Result<()> { let fork = input.fork(); @@ -278,8 +316,7 @@ impl FmtAttribute { } } -/// Representation of a [named parameter][1] (`identifier '=' expression`) in -/// in a [`FmtAttribute`]. +/// Representation of a [named parameter][1] (`identifier '=' expression`) in a [`FmtAttribute`]. /// /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters #[derive(Debug)] @@ -351,20 +388,13 @@ impl<'a> From> for Parameter { /// Representation of a formatting placeholder. #[derive(Debug, Eq, PartialEq)] struct Placeholder { - /// Formatting argument (either named or positional) to be used by this placeholder. + /// Formatting argument (either named or positional) to be used by this [`Placeholder`]. arg: Parameter, - /// [Width parameter][1], if present. - /// - /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#width - width: Option, + /// Indicator whether this [`Placeholder`] has any formatting modifiers. + has_modifiers: bool, - /// [Precision parameter][1], if present. - /// - /// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#precision - precision: Option, - - /// Name of [`std::fmt`] trait to be used for rendering this placeholder. + /// Name of [`std::fmt`] trait to be used for rendering this [`Placeholder`]. trait_name: &'static str, } @@ -389,16 +419,18 @@ impl Placeholder { Self { arg: position, - width: format.spec.and_then(|s| match s.width { - Some(parsing::Count::Parameter(arg)) => Some(arg.into()), - _ => None, - }), - precision: format.spec.and_then(|s| match s.precision { - Some(parsing::Precision::Count(parsing::Count::Parameter( - arg, - ))) => Some(arg.into()), - _ => None, - }), + has_modifiers: format + .spec + .map(|s| { + s.align.is_some() + || s.sign.is_some() + || s.alternate.is_some() + || s.zero_padding.is_some() + || s.width.is_some() + || s.precision.is_some() + || !s.ty.is_trivial() + }) + .unwrap_or_default(), trait_name: ty.trait_name(), } }) @@ -577,38 +609,32 @@ mod placeholder_parse_fmt_string_spec { vec![ Placeholder { arg: Parameter::Positional(0), - width: None, - precision: None, + has_modifiers: false, trait_name: "Display", }, Placeholder { arg: Parameter::Positional(1), - width: None, - precision: None, + has_modifiers: false, trait_name: "Debug", }, Placeholder { arg: Parameter::Positional(1), - width: Some(Parameter::Positional(0)), - precision: None, + has_modifiers: true, trait_name: "Display", }, Placeholder { arg: Parameter::Positional(2), - width: None, - precision: Some(Parameter::Positional(1)), + has_modifiers: true, trait_name: "LowerHex", }, Placeholder { arg: Parameter::Named("par".to_owned()), - width: None, - precision: None, + has_modifiers: true, trait_name: "Debug", }, Placeholder { arg: Parameter::Positional(2), - width: Some(Parameter::Named("width".to_owned())), - precision: None, + has_modifiers: true, trait_name: "Display", }, ], diff --git a/tests/compile_fail/display/shared_format_positional_placeholders.rs b/tests/compile_fail/display/shared_format_positional_placeholders.rs new file mode 100644 index 00000000..8d2a0ff2 --- /dev/null +++ b/tests/compile_fail/display/shared_format_positional_placeholders.rs @@ -0,0 +1,19 @@ +#[derive(derive_more::Display)] +#[display("Stuff({})")] +enum Foo { + A, +} + +#[derive(derive_more::Display)] +#[display("Stuff({0})")] +enum Foo2 { + A, +} + +#[derive(derive_more::Display)] +#[display("Stuff()", _0, _2)] +enum Foo3 { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_positional_placeholders.stderr b/tests/compile_fail/display/shared_format_positional_placeholders.stderr new file mode 100644 index 00000000..e33f7c38 --- /dev/null +++ b/tests/compile_fail/display/shared_format_positional_placeholders.stderr @@ -0,0 +1,34 @@ +error: 1 positional argument in format string, but no arguments were given + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:2:18 + | +2 | #[display("Stuff({})")] + | ^^ + +error: invalid reference to positional argument 0 (no arguments were given) + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:8:19 + | +8 | #[display("Stuff({0})")] + | ^ + | + = note: positional arguments are zero-based + +error: multiple unused formatting arguments + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:14:22 + | +14 | #[display("Stuff()", _0, _2)] + | --------- ^^ ^^ argument never used + | | | + | | argument never used + | multiple missing formatting specifiers + +error[E0425]: cannot find value `_0` in this scope + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:14:22 + | +14 | #[display("Stuff()", _0, _2)] + | ^^ not found in this scope + +error[E0425]: cannot find value `_2` in this scope + --> tests/compile_fail/display/shared_format_positional_placeholders.rs:14:26 + | +14 | #[display("Stuff()", _0, _2)] + | ^^ not found in this scope diff --git a/tests/compile_fail/display/shared_format_unclosed_brace.rs b/tests/compile_fail/display/shared_format_unclosed_brace.rs new file mode 100644 index 00000000..47134e25 --- /dev/null +++ b/tests/compile_fail/display/shared_format_unclosed_brace.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display("Stuff({)")] +enum Foo { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_unclosed_brace.stderr b/tests/compile_fail/display/shared_format_unclosed_brace.stderr new file mode 100644 index 00000000..4d4861f3 --- /dev/null +++ b/tests/compile_fail/display/shared_format_unclosed_brace.stderr @@ -0,0 +1,9 @@ +error: invalid format string: expected `'}'`, found `')'` + --> tests/compile_fail/display/shared_format_unclosed_brace.rs:2:19 + | +2 | #[display("Stuff({)")] + | -^ expected `'}'` in format string + | | + | because of this opening brace + | + = note: if you intended to print `{`, you can escape it using `{{` diff --git a/tests/compile_fail/display/shared_format_variant_spec.rs b/tests/compile_fail/display/shared_format_variant_spec.rs new file mode 100644 index 00000000..9ad647ad --- /dev/null +++ b/tests/compile_fail/display/shared_format_variant_spec.rs @@ -0,0 +1,7 @@ +#[derive(derive_more::Display)] +#[display("Stuff({_variant:?})")] +enum Foo { + A, +} + +fn main() {} diff --git a/tests/compile_fail/display/shared_format_variant_spec.stderr b/tests/compile_fail/display/shared_format_variant_spec.stderr new file mode 100644 index 00000000..2fb6028a --- /dev/null +++ b/tests/compile_fail/display/shared_format_variant_spec.stderr @@ -0,0 +1,5 @@ +error: shared format `_variant` placeholder cannot contain format specifiers + --> tests/compile_fail/display/shared_format_variant_spec.rs:2:11 + | +2 | #[display("Stuff({_variant:?})")] + | ^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/display.rs b/tests/display.rs index 7ae52f61..2e3f0620 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -20,6 +20,9 @@ mod structs { #[derive(Display)] struct Unit; + #[derive(Display)] + struct r#RawUnit; + #[derive(Display)] struct Tuple(); @@ -29,6 +32,7 @@ mod structs { #[test] fn assert() { assert_eq!(Unit.to_string(), "Unit"); + assert_eq!(r#RawUnit.to_string(), "RawUnit"); assert_eq!(Tuple().to_string(), "Tuple"); assert_eq!(Struct {}.to_string(), "Struct"); } @@ -696,6 +700,7 @@ mod enums { #[derive(Display)] enum Enum { Unit, + r#RawUnit, Unnamed(), Named {}, #[display("STR_UNIT")] @@ -709,6 +714,7 @@ mod enums { #[test] fn assert() { assert_eq!(Enum::Unit.to_string(), "Unit"); + assert_eq!(Enum::r#RawUnit.to_string(), "RawUnit"); assert_eq!(Enum::Unnamed().to_string(), "Unnamed"); assert_eq!(Enum::Named {}.to_string(), "Named"); assert_eq!(Enum::StrUnit.to_string(), "STR_UNIT"); @@ -1278,6 +1284,160 @@ mod enums { } } } + + mod shared_format { + use super::*; + + mod single { + use super::*; + + #[derive(Display)] + #[display("Variant: {_variant}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + } + + #[test] + fn assert() { + 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"); + } + } + + mod transparent { + use super::*; + + #[derive(Display)] + #[display("{_variant}")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + #[display("{_0:b}")] + TransparentBinary(i32), + } + + #[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!( + format!("{:08}", Enum::TransparentBinary(4)), + "00000100", + ); + } + } + + mod multiple { + use super::*; + + #[derive(Display)] + #[display("{_variant} Variant: {_variant} {}", _variant)] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + } + + #[test] + fn assert() { + assert_eq!(Enum::A(1).to_string(), "A 1 Variant: A 1 A 1"); + assert_eq!( + Enum::B { field: 2 }.to_string(), + "B 2 Variant: B 2 B 2", + ); + assert_eq!(Enum::C.to_string(), "C Variant: C C"); + } + } + + mod none { + use super::*; + + /// Make sure that variant-specific bounds are not added if `_variant` is not used. + struct NoDisplay; + + #[derive(Display)] + #[display("Variant")] + enum Enum { + #[display("A {_0}")] + A(i32), + #[display("B {}", field)] + B { + field: i32, + }, + C, + D(T), + } + + #[test] + fn assert() { + assert_eq!(Enum::::A(1).to_string(), "Variant"); + assert_eq!( + Enum::::B { field: 2 }.to_string(), + "Variant", + ); + assert_eq!(Enum::::C.to_string(), "Variant"); + assert_eq!(Enum::::D(NoDisplay).to_string(), "Variant"); + } + } + + mod use_field { + use super::*; + + #[derive(Display)] + #[display("Variant {_0}")] + enum Enum { + A(i32), + B(&'static str), + C(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"); + } + } + + mod use_field_and_variant { + use super::*; + + #[derive(Display)] + #[display("Variant {_variant} {}", _0)] + enum Enum { + #[display("A")] + A(i32), + #[display("B")] + B(&'static str), + #[display("C")] + C(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"); + } + } + } } }