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

Delegation for fmt traits (#321) #322

Merged
merged 28 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
81162c8
Cover direct delegation with tests
tyranron Dec 15, 2023
5b28215
Cover interpolated delegation with tests for `Display`
tyranron Dec 15, 2023
1966973
Cover interpolated `Display` delegation with tests for multi-field case
tyranron Dec 18, 2023
fcf399c
Cover interpolated `Debug` delegation for `Display`
tyranron Dec 18, 2023
f561527
Cover delegation suppression in `Display` tests
tyranron Dec 18, 2023
003bbbd
Bootstrap `Display delegation`
tyranron Dec 18, 2023
2b6c9f8
Add tests for untriggered delegation
tyranron Dec 18, 2023
eef4de0
Fix `Pointer` tests for other platforms
tyranron Dec 18, 2023
49d6f20
Enable delegation for `Debug`
tyranron Dec 18, 2023
6f62ba5
Cover `Debug` delegation with tests
tyranron Dec 18, 2023
955ccbc
Mention delegation in docs
tyranron Dec 18, 2023
0af03e6
Mention in CHANGELOG
tyranron Dec 18, 2023
040897e
Fix docs
tyranron Dec 18, 2023
8e93ade
Fix docs
tyranron Dec 18, 2023
bc87f9b
Parse `sign` in `format_spec`
tyranron Dec 19, 2023
9897613
Add tests for other formatting modifiers
tyranron Dec 19, 2023
ee7d27d
Parse `#` (alternate) in `format_spec`
tyranron Dec 19, 2023
9a47940
Parse `0` (zero padding) in `format_spec`
tyranron Dec 20, 2023
565ba9d
Parse `[fill]align` in `format_spec`
tyranron Dec 20, 2023
4450c1d
Consider all possbile modifiers for delegation
tyranron Dec 20, 2023
14f964b
Cover delegation for units in tests
tyranron Dec 20, 2023
2c3915a
Fix tests
tyranron Dec 20, 2023
6f8fc57
Rework delegation detection
tyranron Dec 20, 2023
1a30168
Revert changes to `bounded_types` method
tyranron Dec 20, 2023
0fee2bc
Switch terminology to "transparency"
tyranron Dec 20, 2023
916684b
Make `rustfmt` happy
tyranron Dec 20, 2023
0b266c9
Fix doc tests
tyranron Dec 20, 2023
96c5f67
Fix typo in CHANGELOG
tyranron Dec 20, 2023
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
42 changes: 38 additions & 4 deletions impl/doc/debug.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ The variables available in the arguments is `self` and each member of the struct
structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc.




### Generic data types

When deriving `Debug` for a generic struct/enum, all generic type arguments _used_ during formatting
Expand Down Expand Up @@ -53,8 +51,6 @@ The following where clauses would be generated:
- `&'a T1: Pointer`




### Custom trait bounds

Sometimes you may want to specify additional trait bounds on your generic type parameters, so that they could be used
Expand Down Expand Up @@ -88,6 +84,42 @@ trait MyTrait { fn my_function(&self) -> i32; }
```


### Delegation

If the top-level `#[debug("...", args...)]` attribute (the one for a whole struct or variant) is specified
and can be trivially substituted with a delegation call to the inner type, then all the additional
[formatting parameters][1] do work as expected:
```rust
# use derive_more::Debug;
#
#[derive(Debug)]
#[debug("{_0:o}")] // the same as calling `Octal::fmt()`
struct MyOctalInt(i32);

// so, additional formatting parameters do work transparently
assert_eq!(format!("{:03?}", MyInt(9)), "011");

#[derive(Debug)]
#[debug("{_0:02b}")] // cannot be trivially substituted with `Binary::fmt()`
struct MyBinaryInt(i32);

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07?}", MyBinaryInt(2)), "10");
```

If, for some reason, delegation in trivial cases is not desired, it may be suppressed explicitly:
```rust
# use derive_more::Debug;
#
#[derive(Debug)]
#[debug("{}", format_args!("{_0:o}"))] // `format_args!()` opaques the inner type
struct MyOctalInt(i32);

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07}", MyInt(9)), "11");
```




## Example usage
Expand Down Expand Up @@ -133,3 +165,5 @@ assert_eq!(format!("{:?}", E::EnumFormat(true)), "true");

[`format!()`]: https://doc.rust-lang.org/stable/std/macro.format.html
[`format_args!()`]: https://doc.rust-lang.org/stable/std/macro.format_args.html

[1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters
53 changes: 52 additions & 1 deletion impl/doc/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ the supplied format, or an automatically inferred one.
You specify the format on each variant by writing e.g. `#[display("my val: {}", some_val * 2)]`.
For enums, you can either specify it on each variant, or on the enum as a whole.

For variants that don't have a format specified, it will simply defer to the format of the
For variants that don't have a format specified, it will simply delegate to the format of the
inner variable. If there is no such variable, or there is more than 1, an error is generated.
tyranron marked this conversation as resolved.
Show resolved Hide resolved


Expand Down Expand Up @@ -97,6 +97,52 @@ struct MyStruct<T, U, V> {
```


### Delegation

If the `#[display("...", args...)]` attribute is omitted, the implementation simply delegates to the format
of the inner type, so all the additional [formatting parameters][1] do work as expected:
```rust
# use derive_more::Display;
#
#[derive(Display)]
struct MyInt(i32);

assert_eq!(format!("{:03}", MyInt(7)), "007");
```

If the `#[display("...", args...)]` attribute is specified and can be trivially substituted with a delegation
call to the inner type, then delegation will work too:
```rust
# use derive_more::Display;
#
#[derive(Display)]
#[display("{_0:o}")] // the same as calling `Octal::fmt()`
struct MyOctalInt(i32);

// so, additional formatting parameters do work transparently
assert_eq!(format!("{:03}", MyInt(9)), "011");

#[derive(Display)]
#[display("{_0:02b}")] // cannot be trivially substituted with `Binary::fmt()`
struct MyBinaryInt(i32);

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07}", MyBinaryInt(2)), "10");
```

If, for some reason, delegation in trivial cases is not desired, it may be suppressed explicitly:
```rust
# use derive_more::Display;
#
#[derive(Display)]
#[display("{}", format_args!("{_0:o}"))] // `format_args!()` opaques the inner type
struct MyOctalInt(i32);

// so, additional formatting parameters have no effect
assert_eq!(format!("{:07}", MyInt(9)), "11");
```


Comment on lines +156 to +157
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were all these newlines intentional? (feel free to keep if they were)



## Example usage
Expand Down Expand Up @@ -176,3 +222,8 @@ assert_eq!(UnitStruct {}.to_string(), "UnitStruct");
assert_eq!(PositiveOrNegative { x: 1 }.to_string(), "Positive");
assert_eq!(PositiveOrNegative { x: -1 }.to_string(), "Negative");
```




Comment on lines +237 to +240
Copy link
Owner

@JelteF JelteF Dec 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were all these newlines intentional? (feel free to keep if they were)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JelteF yes.... 4 newlines before ## headers and 2 newlines before ### headers.

[1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters
46 changes: 35 additions & 11 deletions impl/src/fmt/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,30 @@ impl<'a> Expansion<'a> {
///
/// [`Debug::fmt()`]: std::fmt::Debug::fmt()
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) });
if let Some(fmt) = &self.attr.fmt {
return Ok(fmt
tyranron marked this conversation as resolved.
Show resolved Hide resolved
.delegatable()
.then(|| {
let mut bindings = fmt.bindings(self.fields);
let binding = bindings.next()?;
bindings.next().is_none().then_some(binding)
})
.flatten()
.map_or_else(
|| {
quote! {
::core::write!(__derive_more_f, #fmt)
}
},
|b| {
let ident = &b.ident;
let trait_ident = format_ident!("{}", b.trait_name);

quote! {
::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
}
},
));
};

match self.fields {
Expand Down Expand Up @@ -330,9 +352,11 @@ impl<'a> Expansion<'a> {
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 }
out.extend(fmt.bindings(self.fields).map(|b| {
let ty = b.ty;
let trait_ident = format_ident!("{}", b.trait_name);

parse_quote! { #ty: ::core::fmt::#trait_ident }
}));
Ok(out)
} else {
Expand All @@ -342,12 +366,12 @@ impl<'a> Expansion<'a> {
.map(Spanning::into_inner)
{
Some(FieldAttribute::Right(fmt_attr)) => {
out.extend(fmt_attr.bounded_types(self.fields).map(
|(ty, trait_name)| {
let trait_name = format_ident!("{trait_name}");
parse_quote! { #ty: ::core::fmt::#trait_name }
},
));
out.extend(fmt_attr.bindings(self.fields).map(|b| {
let ty = b.ty;
let trait_ident = format_ident!("{}", b.trait_name);

parse_quote! { #ty: ::core::fmt::#trait_ident }
}));
}
Some(FieldAttribute::Left(_skip)) => {}
None => out.extend([parse_quote! { #ty: ::core::fmt::Debug }]),
Expand Down
40 changes: 34 additions & 6 deletions impl/src/fmt/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,35 @@ impl<'a> Expansion<'a> {
/// [`Display::fmt()`]: fmt::Display::fmt()
fn generate_body(&self) -> syn::Result<TokenStream> {
match &self.attrs.fmt {
Some(fmt) => Ok(quote! { ::core::write!(__derive_more_f, #fmt) }),
Some(fmt) => Ok(fmt
.delegatable()
.then(|| {
tyranron marked this conversation as resolved.
Show resolved Hide resolved
let mut bindings = fmt.bindings(self.fields);
let binding = bindings.next()?;
bindings.next().is_none().then_some(binding)
})
.flatten()
.map_or_else(
|| {
quote! {
::core::write!(__derive_more_f, #fmt)
}
},
|b| {
let ident = &b.ident;
let trait_ident = format_ident!("{}", b.trait_name);

quote! {
::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
}
},
)),
None if self.fields.is_empty() => {
let ident_str = self.ident.to_string();
Ok(quote! { ::core::write!(__derive_more_f, #ident_str) })

Ok(quote! {
::core::write!(__derive_more_f, #ident_str)
})
}
None if self.fields.len() == 1 => {
let field = self
Expand All @@ -235,6 +260,7 @@ impl<'a> Expansion<'a> {
.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! {
::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
})
Expand Down Expand Up @@ -265,10 +291,12 @@ impl<'a> Expansion<'a> {
.unwrap_or_default();
};

fmt.bounded_types(self.fields)
.map(|(ty, trait_name)| {
let tr = format_ident!("{}", trait_name);
parse_quote! { #ty: ::core::fmt::#tr }
fmt.bindings(self.fields)
.map(|b| {
let ty = b.ty;
let trait_ident = format_ident!("{}", b.trait_name);

parse_quote! { #ty: ::core::fmt::#trait_ident }
})
.chain(self.attrs.bounds.0.clone())
.collect()
Expand Down
Loading