From 734ed2171e2a86ccebc7cbde224b29d6e3f64699 Mon Sep 17 00:00:00 2001 From: Jelte Fennema-Nio Date: Tue, 2 Jul 2024 09:59:20 +0200 Subject: [PATCH] Support _variant in outer level enum formatting for Display --- CHANGELOG.md | 2 + impl/src/fmt/display.rs | 120 ++++++++++++++++++++++++++++++-------- impl/src/fmt/parsing.rs | 77 ++++++++++++++++++++++++- tests/display.rs | 125 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 300 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9805322..0f8f37ce 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/impl/src/fmt/display.rs b/impl/src/fmt/display.rs index e41dcbd1..061cccc7 100644 --- a/impl/src/fmt/display.rs +++ b/impl/src/fmt/display.rs @@ -9,7 +9,7 @@ use syn::{parse_quote, spanned::Spanned as _}; use crate::utils::{attr::ParseMultiple as _, Spanning}; -use super::{trait_name_to_attribute_name, ContainerAttributes}; +use super::{parsing, 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_format: None, attrs, fields: &s.fields, trait_ident, @@ -110,12 +111,8 @@ 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<'_>, + (shared_attrs, _, trait_ident, attr_name): ExpansionCtx<'_>, ) -> syn::Result<(Vec, TokenStream)> { - if attrs.fmt.is_some() { - todo!("https://github.com/JelteF/derive_more/issues/142"); - } - let (bounds, match_arms) = e.variants.iter().try_fold( (Vec::new(), TokenStream::new()), |(mut bounds, mut arms), variant| { @@ -138,6 +135,7 @@ fn expand_enum( } let v = Expansion { + shared_format: shared_attrs.fmt.as_ref(), attrs: &attrs, fields: &variant.fields, trait_ident, @@ -198,6 +196,9 @@ fn expand_union( /// [`Display::fmt()`]: fmt::Display::fmt() #[derive(Debug)] struct Expansion<'a> { + /// Format shared between all variants of an enum. + shared_format: Option<&'a FmtAttribute>, + /// Derive macro [`ContainerAttributes`]. attrs: &'a ContainerAttributes, @@ -226,6 +227,50 @@ impl<'a> Expansion<'a> { /// [`Display::fmt()`]: fmt::Display::fmt() /// [`FmtAttribute`]: super::FmtAttribute fn generate_body(&self) -> syn::Result { + if self.shared_format.is_none() { + return self.generate_body_impl(); + } + let shared_format = self.shared_format.as_ref().unwrap(); + let mut tokens = TokenStream::new(); + let mut maybe_body = None; + let mut current_format = String::new(); + for part in parsing::format_parts(&shared_format.lit.value()) { + match part { + parsing::FormatPart::Text(s) => { + current_format.push_str(s); + } + parsing::FormatPart::Format { raw, format } => { + if format.arg == Some(parsing::Argument::Identifier("_variant")) { + if !current_format.is_empty() { + tokens.extend(quote! { derive_more::core::write!(__derive_more_f, #current_format)?; }); + current_format.clear(); + } + if maybe_body.is_none() { + maybe_body = Some(self.generate_body_impl()?); + } + let body = maybe_body.as_ref().unwrap(); + tokens.extend(quote! { #body?; }); + } else { + current_format.push_str(raw); + } + } + }; + } + if !current_format.is_empty() { + tokens.extend( + quote! { derive_more::core::write!(__derive_more_f, #current_format) }, + ) + } else { + tokens.extend(quote! { Ok(()) }); + } + Ok(tokens) + } + + /// Generates [`Display::fmt()`] implementation for a struct or an enum variant + /// without considering `shared_format`. + /// + /// [`Display::fmt()`]: fmt::Display::fmt() + fn generate_body_impl(&self) -> syn::Result { match &self.attrs.fmt { Some(fmt) => { Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() { @@ -267,27 +312,56 @@ impl<'a> Expansion<'a> { /// Generates trait bounds for a struct or an enum variant. fn generate_bounds(&self) -> Vec { + let mut bounds: Vec = + if let Some(shared_format) = self.shared_format { + let shared_bounds = shared_format + .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(); + // If it doesn't contain _variant we don't need to add any other bounds + if !parsing::format_string(&shared_format.lit.value()) + .unwrap() + .formats + .iter() + .any(|f| f.arg == Some(parsing::Argument::Identifier("_variant"))) + { + return shared_bounds; + } + shared_bounds + } else { + Vec::new() + }; + let Some(fmt) = &self.attrs.fmt else { - return 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(); + 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(), + ); + return bounds; }; - fmt.bounded_types(self.fields) - .map(|(ty, trait_name)| { - let trait_ident = format_ident!("{trait_name}"); + 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()) - .collect() + parse_quote! { #ty: derive_more::core::fmt::#trait_ident } + }) + .chain(self.attrs.bounds.0.clone()), + ); + bounds } } diff --git a/impl/src/fmt/parsing.rs b/impl/src/fmt/parsing.rs index 8a5d7527..1098e917 100644 --- a/impl/src/fmt/parsing.rs +++ b/impl/src/fmt/parsing.rs @@ -207,6 +207,25 @@ pub(crate) fn format_string(input: &str) -> Option> { input.is_empty().then_some(FormatString { formats }) } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) enum FormatPart<'a> { + Format { raw: &'a str, format: Format<'a> }, + Text(&'a str), +} + +pub(crate) fn format_parts(mut input: &str) -> Vec { + iter::repeat(()) + .scan(&mut input, |input, _| { + let (curr, format) = alt(&mut [ + &mut format_part, + &mut map(text, |(i, x)| (i, FormatPart::Text(x))), + ])(input)?; + **input = curr; + Some(format) + }) + .collect() +} + /// Parses a `maybe_format` as defined in the [grammar spec][0]. /// /// # Grammar @@ -233,6 +252,17 @@ fn maybe_format(input: &str) -> Option<(LeftToParse<'_>, MaybeFormat<'_>)> { ])(input) } +fn format_part(input: &str) -> Option<(LeftToParse<'_>, FormatPart<'_>)> { + alt(&mut [ + &mut map(str("{{"), |i| (i, FormatPart::Text("{{"))), + &mut map(str("}}"), |i| (i, FormatPart::Text("}}"))), + &mut map(format, |(i, format)| { + let raw = &input[..input.len() - i.len()]; + (i, FormatPart::Format { raw, format }) + }), + ])(input) +} + /// Parses a `format` as defined in the [grammar spec][0]. /// /// # Grammar @@ -1261,7 +1291,7 @@ mod tests { } #[test] - fn full() { + fn full_format() { assert_eq!( format_string("prefix{{{0:#?}postfix{par:-^par$.a$}}}"), Some(FormatString { @@ -1297,6 +1327,51 @@ mod tests { ); } + #[test] + fn full_parts() { + assert_eq!( + format_parts("prefix{{{0:#?}postfix{par:-^par$.a$}}}"), + vec![ + FormatPart::Text("prefix"), + FormatPart::Text("{{"), + FormatPart::Format { + raw: "{0:#?}", + format: Format { + arg: Some(Argument::Integer(0)), + spec: Some(FormatSpec { + align: None, + sign: None, + alternate: Some(Alternate), + zero_padding: None, + width: None, + precision: None, + ty: Type::Debug, + }), + } + }, + FormatPart::Text("postfix"), + FormatPart::Format { + raw: "{par:-^par$.a$}", + format: Format { + arg: Some(Argument::Identifier("par")), + spec: Some(FormatSpec { + align: Some((Some('-'), Align::Center)), + sign: None, + alternate: None, + zero_padding: None, + width: Some(Count::Parameter(Argument::Identifier("par"))), + precision: Some(Precision::Count(Count::Parameter( + Argument::Identifier("a"), + ))), + ty: Type::Display, + }), + } + }, + FormatPart::Text("}}"), + ], + ); + } + #[test] fn error() { assert_eq!(format_string("{"), None); diff --git a/tests/display.rs b/tests/display.rs index 1e7ddfc1..ebc0012e 100644 --- a/tests/display.rs +++ b/tests/display.rs @@ -1283,6 +1283,131 @@ 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 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",); + } + } + } } }