Skip to content

Commit

Permalink
Add support for debug attributs on structs and enum variants (#279)
Browse files Browse the repository at this point in the history
Adds support `debug` attributes on structs an enum variants like this:
```rs
#[derive(Debug)]
#[debug("{name}={value:?}")]
struct Arg {
  name: String,
  value: String
}
```

Before the `debug` attribute could only be used on fields of a struct or variant.

Resolves #277

---------

Co-authored-by: tyranron <[email protected]>
  • Loading branch information
ModProg and tyranron authored Aug 5, 2023
1 parent ad301a4 commit 6a4772a
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 164 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
([#206](https://github.com/JelteF/derive_more/pull/206))
- Add `TryUnwrap` derive similar to the `Unwrap` derive. This one returns a `Result` and does not panic.
([#206](https://github.com/JelteF/derive_more/pull/206))
- Add support for container format in `Debug` derive with the same syntax as `Display` derives.
([#279](https://github.com/JelteF/derive_more/pull/279))

### Changed

Expand Down
16 changes: 12 additions & 4 deletions impl/doc/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
This derive macro is a clever superset of `Debug` from standard library. Additional features include:
- not imposing redundant trait bounds;
- `#[debug(skip)]` attribute to skip formatting struct field or enum variant;
- `#[debug("...", args...)]` to specify custom formatting for a particular struct or enum variant field;
- `#[debug("...", args...)]` to specify custom formatting either for the whole struct or enum variant, or its particular field;
- `#[debug(bounds(...))]` to impose additional custom trait bounds.




## The format of the format

You supply a format by placing an attribute on particular struct or enum variant field (not enum variant itself):
You supply a format by placing an attribute on a struct or enum variant, or its particular field:
`#[debug("...", args...)]`. The format is exactly like in [`format!()`] or any other [`format_args!()`]-based macros.

The variables available in the arguments is `self` and each member of the variant, with members of tuple structs being
named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc.
The variables available in the arguments is `self` and each member of the struct or enum variant, with members of tuple
structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc.



Expand Down Expand Up @@ -102,6 +102,10 @@ struct MyInt(i32);
#[derive(Debug)]
struct MyIntHex(#[debug("{_0:x}")] i32);

#[derive(Debug)]
#[debug("{_0} = {_1}")]
struct StructFormat(&'static str, u8);

#[derive(Debug)]
enum E {
Skipped {
Expand All @@ -114,13 +118,17 @@ enum E {
i: i8,
},
Path(#[debug("{}", _0.display())] PathBuf),
#[debug("{_0}")]
EnumFormat(bool)
}

assert_eq!(format!("{:?}", MyInt(-2)), "MyInt(-2)");
assert_eq!(format!("{:?}", MyIntHex(-255)), "MyIntHex(ffffff01)");
assert_eq!(format!("{:?}", StructFormat("answer", 42)), "answer = 42");
assert_eq!(format!("{:?}", E::Skipped { x: 10, y: 20 }), "Skipped { x: 10, .. }");
assert_eq!(format!("{:?}", E::Binary { i: -2 }), "Binary { i: 11111110 }");
assert_eq!(format!("{:?}", E::Path("abc".into())), "Path(abc)");
assert_eq!(format!("{:?}", E::EnumFormat(true)), "true");
```

[`format!()`]: https://doc.rust-lang.org/stable/std/macro.format.html
Expand Down
138 changes: 79 additions & 59 deletions impl/src/fmt/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{
parse::{Error, Parse, ParseStream, Result},
parse::{Parse, ParseStream},
parse_quote,
spanned::Spanned as _,
Ident,
};

use super::{BoundsAttribute, FmtAttribute};
use super::{ContainerAttributes, FmtAttribute};

/// Expands a [`fmt::Debug`] derive macro.
///
/// [`fmt::Debug`]: std::fmt::Debug
pub fn expand(input: &syn::DeriveInput, _: &str) -> Result<TokenStream> {
let attrs = ContainerAttributes::parse_attrs(&input.attrs)?;
pub fn expand(input: &syn::DeriveInput, _: &str) -> syn::Result<TokenStream> {
let attrs = ContainerAttributes::parse_attrs(&input.attrs, "Debug")?;
let ident = &input.ident;

let (bounds, body) = match &input.data {
syn::Data::Struct(s) => expand_struct(attrs, ident, s),
syn::Data::Enum(e) => expand_enum(attrs, e),
syn::Data::Union(_) => {
return Err(Error::new(
return Err(syn::Error::new(
input.span(),
"`Debug` cannot be derived for unions",
));
Expand Down Expand Up @@ -61,12 +61,13 @@ fn expand_struct(
attrs: ContainerAttributes,
ident: &Ident,
s: &syn::DataStruct,
) -> Result<(Vec<syn::WherePredicate>, TokenStream)> {
) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
let s = Expansion {
attr: &attrs,
fields: &s.fields,
ident,
};
s.validate_attrs()?;
let bounds = s.generate_bounds()?;
let body = s.generate_body()?;

Expand All @@ -91,19 +92,43 @@ fn expand_struct(
///
/// [`fmt::Debug`]: std::fmt::Debug
fn expand_enum(
attrs: ContainerAttributes,
mut attrs: ContainerAttributes,
e: &syn::DataEnum,
) -> Result<(Vec<syn::WherePredicate>, TokenStream)> {
) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
if let Some(enum_fmt) = attrs.fmt.as_ref() {
return Err(syn::Error::new_spanned(
enum_fmt,
"`#[debug(\"...\", ...)]` attribute is not allowed on enum, place it on its variants \
instead",
));
}

let (bounds, match_arms) = e.variants.iter().try_fold(
(Vec::new(), TokenStream::new()),
|(mut bounds, mut arms), variant| {
let ident = &variant.ident;

attrs.fmt = variant
.attrs
.iter()
.filter(|attr| attr.path().is_ident("debug"))
.try_fold(None, |mut attrs, attr| {
let attr = attr.parse_args::<FmtAttribute>()?;
attrs.replace(attr).map_or(Ok(()), |dup| {
Err(syn::Error::new(
dup.span(),
"multiple `#[debug(\"...\", ...)]` attributes aren't allowed",
))
})?;
Ok::<_, syn::Error>(attrs)
})?;

let v = Expansion {
attr: &attrs,
fields: &variant.fields,
ident,
};
v.validate_attrs()?;
let arm_body = v.generate_body()?;
bounds.extend(v.generate_bounds()?);

Expand All @@ -123,7 +148,7 @@ fn expand_enum(

arms.extend([quote! { #matcher => { #arm_body }, }]);

Ok::<_, Error>((bounds, arms))
Ok::<_, syn::Error>((bounds, arms))
},
)?;

Expand All @@ -135,42 +160,6 @@ fn expand_enum(
Ok((bounds, body))
}

/// Representation of a [`fmt::Debug`] derive macro container attribute.
///
/// ```rust,ignore
/// #[debug(bound(<bounds>))]
/// ```
///
/// [`fmt::Debug`]: std::fmt::Debug
#[derive(Debug, Default)]
struct ContainerAttributes {
/// Additional trait bounds.
bounds: BoundsAttribute,
}

impl ContainerAttributes {
/// Parses [`ContainerAttributes`] from the provided [`syn::Attribute`]s.
fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> Result<Self> {
attrs
.as_ref()
.iter()
.filter(|attr| attr.path().is_ident("debug"))
.try_fold(ContainerAttributes::default(), |mut attrs, attr| {
let attr = attr.parse_args::<ContainerAttributes>()?;
attrs.bounds.0.extend(attr.bounds.0);
Ok(attrs)
})
}
}

impl Parse for ContainerAttributes {
fn parse(input: ParseStream) -> Result<Self> {
BoundsAttribute::check_legacy_fmt(input)?;

input.parse().map(|bounds| ContainerAttributes { bounds })
}
}

/// Representation of a [`fmt::Debug`] derive macro field attribute.
///
/// ```rust,ignore
Expand All @@ -190,16 +179,16 @@ enum FieldAttribute {
}

impl FieldAttribute {
/// Parses [`ContainerAttributes`] from the provided [`syn::Attribute`]s.
fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> Result<Option<Self>> {
/// Parses a [`FieldAttribute`] from the provided [`syn::Attribute`]s.
fn parse_attrs(attrs: impl AsRef<[syn::Attribute]>) -> syn::Result<Option<Self>> {
Ok(attrs
.as_ref()
.iter()
.filter(|attr| attr.path().is_ident("debug"))
.try_fold(None, |mut attrs, attr| {
let field_attr = attr.parse_args::<FieldAttribute>()?;
if let Some((path, _)) = attrs.replace((attr.path(), field_attr)) {
Err(Error::new(
Err(syn::Error::new(
path.span(),
"only single `#[debug(...)]` attribute is allowed here",
))
Expand All @@ -212,7 +201,7 @@ impl FieldAttribute {
}

impl Parse for FieldAttribute {
fn parse(input: ParseStream) -> Result<Self> {
fn parse(input: ParseStream) -> syn::Result<Self> {
FmtAttribute::check_legacy_fmt(input)?;

if input.peek(syn::LitStr) {
Expand All @@ -222,7 +211,7 @@ impl Parse for FieldAttribute {
if ["skip", "ignore"].into_iter().any(|i| p.is_ident(i)) {
Ok(p)
} else {
Err(Error::new(
Err(syn::Error::new(
p.span(),
"unknown attribute, expected `skip` or `ignore`",
))
Expand All @@ -249,10 +238,34 @@ struct Expansion<'a> {
}

impl<'a> Expansion<'a> {
/// Validates attributes of this [`Expansion`] to be consistent.
fn validate_attrs(&self) -> syn::Result<()> {
if self.attr.fmt.is_some() {
for field_attr in self
.fields
.iter()
.map(|f| FieldAttribute::parse_attrs(&f.attrs))
{
if let Some(FieldAttribute::Fmt(fmt)) = field_attr? {
return Err(syn::Error::new_spanned(
fmt,
"`#[debug(...)]` attributes are not allowed on fields when \
`#[debug(\"...\", ...)]` is specified on struct or variant",
));
}
}
}
Ok(())
}

/// Generates [`Debug::fmt()`] implementation for a struct or an enum variant.
///
/// [`Debug::fmt()`]: std::fmt::Debug::fmt()
fn generate_body(&self) -> Result<TokenStream> {
fn generate_body(&self) -> syn::Result<TokenStream> {
if let Some(fmt_attr) = &self.attr.fmt {
return Ok(quote! { ::core::write!(__derive_more_f, #fmt_attr) });
};

match self.fields {
syn::Fields::Unit => {
let ident = self.ident.to_string();
Expand All @@ -278,7 +291,7 @@ impl<'a> Expansion<'a> {
|out, (i, field)| match FieldAttribute::parse_attrs(&field.attrs)? {
Some(FieldAttribute::Skip) => {
exhaustive = false;
Ok::<_, Error>(out)
Ok::<_, syn::Error>(out)
}
Some(FieldAttribute::Fmt(fmt)) => Ok(quote! {
::derive_more::__private::DebugTuple::field(
Expand Down Expand Up @@ -318,7 +331,7 @@ impl<'a> Expansion<'a> {
match FieldAttribute::parse_attrs(&field.attrs)? {
Some(FieldAttribute::Skip) => {
exhaustive = false;
Ok::<_, Error>(out)
Ok::<_, syn::Error>(out)
}
Some(FieldAttribute::Fmt(fmt)) => Ok(quote! {
::core::fmt::DebugStruct::field(
Expand All @@ -342,10 +355,17 @@ impl<'a> Expansion<'a> {
}

/// Generates trait bounds for a struct or an enum variant.
fn generate_bounds(&self) -> Result<Vec<syn::WherePredicate>> {
self.fields.iter().try_fold(
self.attr.bounds.0.clone().into_iter().collect::<Vec<_>>(),
|mut out, field| {
fn generate_bounds(&self) -> syn::Result<Vec<syn::WherePredicate>> {
let mut out = self.attr.bounds.0.clone().into_iter().collect::<Vec<_>>();

if let Some(fmt) = self.attr.fmt.as_ref() {
out.extend(fmt.bounded_types(self.fields).map(|(ty, trait_name)| {
let trait_name = format_ident!("{trait_name}");
parse_quote! { #ty: ::core::fmt::#trait_name }
}));
Ok(out)
} else {
self.fields.iter().try_fold(out, |mut out, field| {
let ty = &field.ty;
match FieldAttribute::parse_attrs(&field.attrs)? {
Some(FieldAttribute::Fmt(attr)) => {
Expand All @@ -360,7 +380,7 @@ impl<'a> Expansion<'a> {
None => out.extend([parse_quote! { #ty: ::core::fmt::Debug }]),
}
Ok(out)
},
)
})
}
}
}
Loading

0 comments on commit 6a4772a

Please sign in to comment.