Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support _variant in outer level enum formatting for Display #377

Merged
merged 15 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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