Skip to content

Commit

Permalink
Support _variant in outer level enum formatting for Display (#377, #…
Browse files Browse the repository at this point in the history
…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 <tyranron@gmail.com>
  • Loading branch information
JelteF and tyranron authored Jul 25, 2024
1 parent 162535e commit 8a172f2
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 97 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
2 changes: 0 additions & 2 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 19 additions & 3 deletions impl/doc/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -175,6 +181,7 @@ struct Point2D {
}

#[derive(Display)]
#[display("Enum E: {_variant}")]
enum E {
Uint(u32),
#[display("I am B {:b}", i)]
Expand All @@ -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 {
Expand Down Expand Up @@ -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");
Expand Down
207 changes: 150 additions & 57 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
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_attr: None,
attrs,
fields: &s.fields,
trait_ident,
Expand Down Expand Up @@ -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<syn::WherePredicate>, 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(
Expand All @@ -138,6 +150,7 @@ fn expand_enum(
}

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

Expand All @@ -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<TokenStream> {
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<syn::WherePredicate> {
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
}
}

Expand All @@ -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!(),
}
}
Loading

0 comments on commit 8a172f2

Please sign in to comment.