Skip to content

Commit

Permalink
Support _variant in outer level enum formatting for Display
Browse files Browse the repository at this point in the history
  • Loading branch information
JelteF committed Jul 2, 2024
1 parent 79e7fec commit da56134
Show file tree
Hide file tree
Showing 4 changed files with 298 additions and 24 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
118 changes: 95 additions & 23 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -80,6 +80,7 @@ fn expand_struct(
(attrs, ident, trait_ident, _): ExpansionCtx<'_>,
) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
let s = Expansion {
shared_format: None,
attrs,
fields: &s.fields,
trait_ident,
Expand Down Expand Up @@ -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<syn::WherePredicate>, 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| {
Expand All @@ -138,6 +135,7 @@ fn expand_enum(
}

let v = Expansion {
shared_format: shared_attrs.fmt.as_ref(),
attrs: &attrs,
fields: &variant.fields,
trait_ident,
Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -226,6 +227,48 @@ impl<'a> Expansion<'a> {
/// [`Display::fmt()`]: fmt::Display::fmt()
/// [`FmtAttribute`]: super::FmtAttribute
fn generate_body(&self) -> syn::Result<TokenStream> {
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`.
fn generate_body_impl(&self) -> syn::Result<TokenStream> {
match &self.attrs.fmt {
Some(fmt) => {
Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
Expand Down Expand Up @@ -267,27 +310,56 @@ impl<'a> Expansion<'a> {

/// Generates trait bounds for a struct or an enum variant.
fn generate_bounds(&self) -> Vec<syn::WherePredicate> {
let mut bounds: Vec<syn::WherePredicate> =
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
}
}

Expand Down
77 changes: 76 additions & 1 deletion impl/src/fmt/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,25 @@ pub(crate) fn format_string(input: &str) -> Option<FormatString<'_>> {
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<FormatPart> {
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
Expand All @@ -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
Expand Down Expand Up @@ -1261,7 +1291,7 @@ mod tests {
}

#[test]
fn full() {
fn full_format() {
assert_eq!(
format_string("prefix{{{0:#?}postfix{par:-^par$.a$}}}"),
Some(FormatString {
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit da56134

Please sign in to comment.