Skip to content

Commit 732bb12

Browse files
authored
Delegation for fmt traits (#322, #321)
## Synopsis See #321 (comment): > You’re discarding formatting flags provided by the user in format string, e.g.: > > ```rust > #[derive(derive_more::Display)] > #[display(fmt = "{:?}", _0)] > struct Num(usize); > > impl std::fmt::Debug for Num { > fn fmt(&self, fmtr: &mut std::fmt::Formatter) -> std::fmt::Result { > self.0.fmt(fmtr) > } > } > > fn main() { > let num = Num(7); > println!("{num:03?}"); // prints ‘007’ as expected > println!("{num:03}"); // prints ‘7’ instead > } > ``` ## Solution See #321 (comment): > Theoretically, we can support this with the current syntax, because we can detect so-called _trivial_ cases and transform them into delegation (we do parse formatting string literal anyway). > > ```rust > #[derive(derive_more::Display)] > #[display("{_0:?}")] // <--- it's clear to be a trivial delegation case > struct Num(usize); > ``` > > would expand to > > ```rust > impl std::fmt::Display for Num { > fn fmt(&self, fmtr: &mut std::fmt::Formatter) -> std::fmt::Result { > let _0 = &self.0; > std::fmt::Debug::fmt(_0, fmtr) > } > } > ``` > > rather than > > ```rust > impl std::fmt::Display for Num { > fn fmt(&self, fmtr: &mut std::fmt::Formatter) -> std::fmt::Result { > let _0 = &self.0; > write!(fmtr, "{_0:?}") > } > } > ```
1 parent 1536f52 commit 732bb12

File tree

11 files changed

+2374
-113
lines changed

11 files changed

+2374
-113
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
2525
`#[display(fmt = "...", ("<expr>"),*)]`, and `#[display(bound(<bound>))]`
2626
instead of `#[display(bound = "<bound>")]`. So without the double quotes
2727
around the expressions and bounds.
28+
- The `Debug` and `Display` derives (and other `fmt`-like ones) now transparently
29+
delegate to the inner type when `#[display("...", (<expr>),*)]` attribute is
30+
trivially substitutable with a transparent call.
31+
([#322](https://github.com/JelteF/derive_more/pull/322))
2832
- The `DebugCustom` derive is renamed to just `Debug` (gated now under a separate
2933
`debug` feature), and its semantics were changed to be a superset of `std` variant
3034
of `Debug`.

impl/doc/debug.md

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ The variables available in the arguments is `self` and each member of the struct
1818
structs being named with a leading underscore and their index, i.e. `_0`, `_1`, `_2`, etc.
1919

2020

21-
22-
2321
### Generic data types
2422

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

5553

56-
57-
5854
### Custom trait bounds
5955

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

9086

87+
### Transparency
88+
89+
If the top-level `#[debug("...", args...)]` attribute (the one for a whole struct or variant) is specified
90+
and can be trivially substituted with a transparent delegation call to the inner type, then all the additional
91+
[formatting parameters][1] do work as expected:
92+
```rust
93+
# use derive_more::Debug;
94+
#
95+
#[derive(Debug)]
96+
#[debug("{_0:o}")] // the same as calling `Octal::fmt()`
97+
struct MyOctalInt(i32);
98+
99+
// so, additional formatting parameters do work transparently
100+
assert_eq!(format!("{:03?}", MyOctalInt(9)), "011");
101+
102+
#[derive(Debug)]
103+
#[debug("{_0:02b}")] // cannot be trivially substituted with `Binary::fmt()`,
104+
struct MyBinaryInt(i32); // because of specified formatting parameters
105+
106+
// so, additional formatting parameters have no effect
107+
assert_eq!(format!("{:07?}", MyBinaryInt(2)), "10");
108+
```
109+
110+
If, for some reason, transparency in trivial cases is not desired, it may be suppressed explicitly
111+
either with the [`format_args!()`] macro usage:
112+
```rust
113+
# use derive_more::Debug;
114+
#
115+
#[derive(Debug)]
116+
#[debug("{}", format_args!("{_0:o}"))] // `format_args!()` obscures the inner type
117+
struct MyOctalInt(i32);
118+
119+
// so, additional formatting parameters have no effect
120+
assert_eq!(format!("{:07?}", MyOctalInt(9)), "11");
121+
```
122+
Or by adding [formatting parameters][1] which cause no visual effects:
123+
```rust
124+
# use derive_more::Debug;
125+
#
126+
#[derive(Debug)]
127+
#[debug("{_0:^o}")] // `^` is centering, but in absence of additional width has no effect
128+
struct MyOctalInt(i32);
129+
130+
// and so, additional formatting parameters have no effect
131+
assert_eq!(format!("{:07?}", MyOctalInt(9)), "11");
132+
```
133+
134+
91135

92136

93137
## Example usage
@@ -133,3 +177,5 @@ assert_eq!(format!("{:?}", E::EnumFormat(true)), "true");
133177

134178
[`format!()`]: https://doc.rust-lang.org/stable/std/macro.format.html
135179
[`format_args!()`]: https://doc.rust-lang.org/stable/std/macro.format_args.html
180+
181+
[1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters

impl/doc/display.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,64 @@ struct MyStruct<T, U, V> {
9797
```
9898

9999

100+
### Transparency
101+
102+
If the `#[display("...", args...)]` attribute is omitted, the implementation transparently delegates to the format
103+
of the inner type, so all the additional [formatting parameters][1] do work as expected:
104+
```rust
105+
# use derive_more::Display;
106+
#
107+
#[derive(Display)]
108+
struct MyInt(i32);
109+
110+
assert_eq!(format!("{:03}", MyInt(7)), "007");
111+
```
112+
113+
If the `#[display("...", args...)]` attribute is specified and can be trivially substituted with a transparent
114+
delegation call to the inner type, then additional [formatting parameters][1] will work too:
115+
```rust
116+
# use derive_more::Display;
117+
#
118+
#[derive(Display)]
119+
#[display("{_0:o}")] // the same as calling `Octal::fmt()`
120+
struct MyOctalInt(i32);
121+
122+
// so, additional formatting parameters do work transparently
123+
assert_eq!(format!("{:03}", MyOctalInt(9)), "011");
124+
125+
#[derive(Display)]
126+
#[display("{_0:02b}")] // cannot be trivially substituted with `Binary::fmt()`,
127+
struct MyBinaryInt(i32); // because of specified formatting parameters
128+
129+
// so, additional formatting parameters have no effect
130+
assert_eq!(format!("{:07}", MyBinaryInt(2)), "10");
131+
```
132+
133+
If, for some reason, transparency in trivial cases is not desired, it may be suppressed explicitly
134+
either with the [`format_args!()`] macro usage:
135+
```rust
136+
# use derive_more::Display;
137+
#
138+
#[derive(Display)]
139+
#[display("{}", format_args!("{_0:o}"))] // `format_args!()` obscures the inner type
140+
struct MyOctalInt(i32);
141+
142+
// so, additional formatting parameters have no effect
143+
assert_eq!(format!("{:07}", MyOctalInt(9)), "11");
144+
```
145+
Or by adding [formatting parameters][1] which cause no visual effects:
146+
```rust
147+
# use derive_more::Display;
148+
#
149+
#[derive(Display)]
150+
#[display("{_0:^o}")] // `^` is centering, but in absence of additional width has no effect
151+
struct MyOctalInt(i32);
152+
153+
// and so, additional formatting parameters have no effect
154+
assert_eq!(format!("{:07}", MyOctalInt(9)), "11");
155+
```
156+
157+
100158

101159

102160
## Example usage
@@ -176,3 +234,10 @@ assert_eq!(UnitStruct {}.to_string(), "UnitStruct");
176234
assert_eq!(PositiveOrNegative { x: 1 }.to_string(), "Positive");
177235
assert_eq!(PositiveOrNegative { x: -1 }.to_string(), "Negative");
178236
```
237+
238+
239+
240+
241+
[`format_args!()`]: https://doc.rust-lang.org/stable/std/macro.format_args.html
242+
243+
[1]: https://doc.rust-lang.org/stable/std/fmt/index.html#formatting-parameters

impl/src/fmt/debug.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,12 @@ impl<'a> Expansion<'a> {
227227
///
228228
/// [`Debug::fmt()`]: std::fmt::Debug::fmt()
229229
fn generate_body(&self) -> syn::Result<TokenStream> {
230-
if let Some(fmt_attr) = &self.attr.fmt {
231-
return Ok(quote! { ::core::write!(__derive_more_f, #fmt_attr) });
230+
if let Some(fmt) = &self.attr.fmt {
231+
return Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
232+
quote! { ::core::fmt::#trait_ident::fmt(&(#expr), __derive_more_f) }
233+
} else {
234+
quote! { ::core::write!(__derive_more_f, #fmt) }
235+
});
232236
};
233237

234238
match self.fields {
@@ -331,8 +335,9 @@ impl<'a> Expansion<'a> {
331335

332336
if let Some(fmt) = self.attr.fmt.as_ref() {
333337
out.extend(fmt.bounded_types(self.fields).map(|(ty, trait_name)| {
334-
let trait_name = format_ident!("{trait_name}");
335-
parse_quote! { #ty: ::core::fmt::#trait_name }
338+
let trait_ident = format_ident!("{trait_name}");
339+
340+
parse_quote! { #ty: ::core::fmt::#trait_ident }
336341
}));
337342
Ok(out)
338343
} else {
@@ -344,8 +349,9 @@ impl<'a> Expansion<'a> {
344349
Some(FieldAttribute::Right(fmt_attr)) => {
345350
out.extend(fmt_attr.bounded_types(self.fields).map(
346351
|(ty, trait_name)| {
347-
let trait_name = format_ident!("{trait_name}");
348-
parse_quote! { #ty: ::core::fmt::#trait_name }
352+
let trait_ident = format_ident!("{trait_name}");
353+
354+
parse_quote! { #ty: ::core::fmt::#trait_ident }
349355
},
350356
));
351357
}

impl/src/fmt/display.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,19 @@ impl<'a> Expansion<'a> {
222222
/// [`Display::fmt()`]: fmt::Display::fmt()
223223
fn generate_body(&self) -> syn::Result<TokenStream> {
224224
match &self.attrs.fmt {
225-
Some(fmt) => Ok(quote! { ::core::write!(__derive_more_f, #fmt) }),
225+
Some(fmt) => {
226+
Ok(if let Some((expr, trait_ident)) = fmt.transparent_call() {
227+
quote! { ::core::fmt::#trait_ident::fmt(&(#expr), __derive_more_f) }
228+
} else {
229+
quote! { ::core::write!(__derive_more_f, #fmt) }
230+
})
231+
}
226232
None if self.fields.is_empty() => {
227233
let ident_str = self.ident.to_string();
228-
Ok(quote! { ::core::write!(__derive_more_f, #ident_str) })
234+
235+
Ok(quote! {
236+
::core::write!(__derive_more_f, #ident_str)
237+
})
229238
}
230239
None if self.fields.len() == 1 => {
231240
let field = self
@@ -235,6 +244,7 @@ impl<'a> Expansion<'a> {
235244
.unwrap_or_else(|| unreachable!("count() == 1"));
236245
let ident = field.ident.clone().unwrap_or_else(|| format_ident!("_0"));
237246
let trait_ident = self.trait_ident;
247+
238248
Ok(quote! {
239249
::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
240250
})
@@ -267,8 +277,9 @@ impl<'a> Expansion<'a> {
267277

268278
fmt.bounded_types(self.fields)
269279
.map(|(ty, trait_name)| {
270-
let tr = format_ident!("{}", trait_name);
271-
parse_quote! { #ty: ::core::fmt::#tr }
280+
let trait_ident = format_ident!("{trait_name}");
281+
282+
parse_quote! { #ty: ::core::fmt::#trait_ident }
272283
})
273284
.chain(self.attrs.bounds.0.clone())
274285
.collect()

impl/src/fmt/mod.rs

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ pub(crate) mod display;
99
mod parsing;
1010

1111
use proc_macro2::TokenStream;
12-
use quote::ToTokens;
12+
use quote::{format_ident, ToTokens};
1313
use syn::{
1414
parse::{Parse, ParseStream},
1515
punctuated::Punctuated,
1616
spanned::Spanned as _,
17-
token, Ident,
17+
token,
1818
};
1919

2020
use crate::{
@@ -133,7 +133,69 @@ impl ToTokens for FmtAttribute {
133133
}
134134

135135
impl FmtAttribute {
136-
/// Returns an [`Iterator`] over bounded [`syn::Type`]s and trait names.
136+
/// Checks whether this [`FmtAttribute`] can be replaced with a transparent delegation (calling
137+
/// a formatting trait directly instead of interpolation syntax).
138+
///
139+
/// If such transparent call is possible, the returns an [`Ident`] of the delegated trait and
140+
/// the [`Expr`] to pass into the call, otherwise [`None`].
141+
///
142+
/// [`Ident`]: struct@syn::Ident
143+
fn transparent_call(&self) -> Option<(Expr, syn::Ident)> {
144+
// `FmtAttribute` is transparent when:
145+
146+
// (1) There is exactly one formatting parameter.
147+
let lit = self.lit.value();
148+
let param =
149+
parsing::format(&lit).and_then(|(more, p)| more.is_empty().then_some(p))?;
150+
151+
// (2) And the formatting parameter doesn't contain any modifiers.
152+
if param
153+
.spec
154+
.map(|s| {
155+
s.align.is_some()
156+
|| s.sign.is_some()
157+
|| s.alternate.is_some()
158+
|| s.zero_padding.is_some()
159+
|| s.width.is_some()
160+
|| s.precision.is_some()
161+
|| !s.ty.is_trivial()
162+
})
163+
.unwrap_or_default()
164+
{
165+
return None;
166+
}
167+
168+
let expr = match param.arg {
169+
// (3) And either exactly one positional argument is specified.
170+
Some(parsing::Argument::Integer(_)) | None => (self.args.len() == 1)
171+
.then(|| self.args.first())
172+
.flatten()
173+
.map(|a| a.expr.clone()),
174+
175+
// (4) Or the formatting parameter's name refers to some outer binding.
176+
Some(parsing::Argument::Identifier(name)) if self.args.is_empty() => {
177+
Some(format_ident!("{name}").into())
178+
}
179+
180+
// (5) Or exactly one named argument is specified for the formatting parameter's name.
181+
Some(parsing::Argument::Identifier(name)) => (self.args.len() == 1)
182+
.then(|| self.args.first())
183+
.flatten()
184+
.filter(|a| a.alias.as_ref().map(|a| a.0 == name).unwrap_or_default())
185+
.map(|a| a.expr.clone()),
186+
}?;
187+
188+
let trait_name = param
189+
.spec
190+
.map(|s| s.ty)
191+
.unwrap_or(parsing::Type::Display)
192+
.trait_name();
193+
194+
Some((expr, format_ident!("{trait_name}")))
195+
}
196+
197+
/// Returns an [`Iterator`] over bounded [`syn::Type`]s (and correspondent trait names) by this
198+
/// [`FmtAttribute`].
137199
fn bounded_types<'a>(
138200
&'a self,
139201
fields: &'a syn::Fields,
@@ -221,7 +283,9 @@ impl FmtAttribute {
221283
#[derive(Debug)]
222284
struct FmtArgument {
223285
/// `identifier =` [`Ident`].
224-
alias: Option<(Ident, token::Eq)>,
286+
///
287+
/// [`Ident`]: struct@syn::Ident
288+
alias: Option<(syn::Ident, token::Eq)>,
225289

226290
/// `expression` [`Expr`].
227291
expr: Expr,
@@ -231,15 +295,15 @@ impl FmtArgument {
231295
/// Returns an `identifier` of the [named parameter][1].
232296
///
233297
/// [1]: https://doc.rust-lang.org/stable/std/fmt/index.html#named-parameters
234-
fn alias(&self) -> Option<&Ident> {
298+
fn alias(&self) -> Option<&syn::Ident> {
235299
self.alias.as_ref().map(|(ident, _)| ident)
236300
}
237301
}
238302

239303
impl Parse for FmtArgument {
240304
fn parse(input: ParseStream) -> syn::Result<Self> {
241305
Ok(Self {
242-
alias: (input.peek(Ident) && input.peek2(token::Eq))
306+
alias: (input.peek(syn::Ident) && input.peek2(token::Eq))
243307
.then(|| Ok::<_, syn::Error>((input.parse()?, input.parse()?)))
244308
.transpose()?,
245309
expr: input.parse()?,
@@ -283,7 +347,7 @@ impl<'a> From<parsing::Argument<'a>> for Parameter {
283347
}
284348

285349
/// Representation of a formatting placeholder.
286-
#[derive(Debug, PartialEq, Eq)]
350+
#[derive(Debug, Eq, PartialEq)]
287351
struct Placeholder {
288352
/// Formatting argument (either named or positional) to be used by this placeholder.
289353
arg: Parameter,
@@ -378,7 +442,7 @@ impl attr::ParseMultiple for ContainerAttributes {
378442
fn merge_attrs(
379443
prev: Spanning<Self>,
380444
new: Spanning<Self>,
381-
name: &Ident,
445+
name: &syn::Ident,
382446
) -> syn::Result<Spanning<Self>> {
383447
let Spanning {
384448
span: prev_span,

0 commit comments

Comments
 (0)