derive_more_impl/fmt/
display.rs

1//! Implementation of [`fmt::Display`]-like derive macros.
2
3#[cfg(doc)]
4use std::fmt;
5
6use proc_macro2::TokenStream;
7use quote::{format_ident, quote};
8use syn::{
9    ext::IdentExt as _,
10    parse::{Parse, ParseStream},
11    parse_quote,
12    spanned::Spanned as _,
13    token, LitStr,
14};
15
16use crate::utils::{
17    attr::{self, ParseMultiple as _},
18    Spanning,
19};
20
21use super::{trait_name_to_attribute_name, ContainsGenericsExt as _, FmtAttribute};
22
23/// Expands a [`fmt::Display`]-like derive macro.
24///
25/// Available macros:
26/// - [`Binary`](fmt::Binary)
27/// - [`Display`](fmt::Display)
28/// - [`LowerExp`](fmt::LowerExp)
29/// - [`LowerHex`](fmt::LowerHex)
30/// - [`Octal`](fmt::Octal)
31/// - [`Pointer`](fmt::Pointer)
32/// - [`UpperExp`](fmt::UpperExp)
33/// - [`UpperHex`](fmt::UpperHex)
34pub fn expand(input: &syn::DeriveInput, trait_name: &str) -> syn::Result<TokenStream> {
35    let trait_name = normalize_trait_name(trait_name);
36    let attr_name = format_ident!("{}", trait_name_to_attribute_name(trait_name));
37
38    let attrs = ContainerAttributes::parse_attrs(&input.attrs, &attr_name)?
39        .map(Spanning::into_inner)
40        .unwrap_or_default();
41    let trait_ident = format_ident!("{trait_name}");
42    let ident = &input.ident;
43
44    let type_params = input
45        .generics
46        .params
47        .iter()
48        .filter_map(|p| match p {
49            syn::GenericParam::Type(t) => Some(&t.ident),
50            syn::GenericParam::Const(..) | syn::GenericParam::Lifetime(..) => None,
51        })
52        .collect::<Vec<_>>();
53
54    let ctx: ExpansionCtx = (&attrs, &type_params, ident, &trait_ident, &attr_name);
55    let (bounds, body) = match &input.data {
56        syn::Data::Struct(s) => expand_struct(s, ctx),
57        syn::Data::Enum(e) => expand_enum(e, ctx),
58        syn::Data::Union(u) => expand_union(u, ctx),
59    }?;
60
61    let (impl_gens, ty_gens, where_clause) = {
62        let (impl_gens, ty_gens, where_clause) = input.generics.split_for_impl();
63        let mut where_clause = where_clause
64            .cloned()
65            .unwrap_or_else(|| parse_quote! { where });
66        where_clause.predicates.extend(bounds);
67        (impl_gens, ty_gens, where_clause)
68    };
69
70    Ok(quote! {
71        #[allow(deprecated)] // omit warnings on deprecated fields/variants
72        #[allow(unreachable_code)] // omit warnings for `!` and other unreachable types
73        #[automatically_derived]
74        impl #impl_gens derive_more::core::fmt::#trait_ident for #ident #ty_gens #where_clause {
75            fn fmt(
76                &self, __derive_more_f: &mut derive_more::core::fmt::Formatter<'_>
77            ) -> derive_more::core::fmt::Result {
78                #body
79            }
80        }
81    })
82}
83
84/// Representation of possible [`fmt::Display`]-like derive macro attributes placed on a container
85/// (struct or enum variant).
86///
87/// ```rust,ignore
88/// #[<attribute>("<fmt-literal>", <fmt-args>)]
89/// #[<attribute>(bound(<where-predicates>))]
90/// #[<attribute>(rename_all = "<casing>")]
91/// ```
92///
93/// `#[<attribute>("...")]` and `#[<attribute>(rename_all = "...")]` can be specified only once,
94/// while multiple `#[<attribute>(bound(...))]` are allowed.
95#[derive(Debug, Default)]
96struct ContainerAttributes {
97    /// [`attr::RenameAll`] for case conversion.
98    rename_all: Option<attr::RenameAll>,
99
100    /// Common [`ContainerAttributes`].
101    ///
102    /// [`ContainerAttributes`]: super::ContainerAttributes
103    common: super::ContainerAttributes,
104}
105
106impl Parse for ContainerAttributes {
107    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
108        mod ident {
109            use syn::custom_keyword;
110
111            custom_keyword!(bounds);
112            custom_keyword!(bound);
113            custom_keyword!(rename_all);
114        }
115
116        // We do check `FmtAttribute::check_legacy_fmt` eagerly here, because `.lookahead1()` won't
117        // check for the `fmt =` ident below, to omit including it into the `ahead.error()`.
118        FmtAttribute::check_legacy_fmt(input)?;
119
120        // We use `.lookahead1()` with all possible idents to form a nice error message including
121        // all the possible variants.
122        let ahead = input.lookahead1();
123        if ahead.peek(LitStr)
124            || ahead.peek(ident::bounds)
125            || ahead.peek(ident::bound)
126            || ahead.peek(token::Where)
127        {
128            Ok(Self {
129                common: input.parse()?,
130                ..Default::default()
131            })
132        } else if ahead.peek(ident::rename_all) {
133            Ok(Self {
134                rename_all: Some(input.parse()?),
135                ..Self::default()
136            })
137        } else {
138            Err(ahead.error())
139        }
140    }
141}
142
143impl attr::ParseMultiple for ContainerAttributes {
144    fn merge_attrs(
145        prev: Spanning<Self>,
146        new: Spanning<Self>,
147        name: &syn::Ident,
148    ) -> syn::Result<Spanning<Self>> {
149        let Spanning {
150            span: prev_span,
151            item: mut prev,
152        } = prev;
153        let Spanning {
154            span: new_span,
155            item: new,
156        } = new;
157
158        if new
159            .rename_all
160            .and_then(|n| prev.rename_all.replace(n))
161            .is_some()
162        {
163            return Err(syn::Error::new(
164                new_span,
165                format!("multiple `#[{name}(rename_all=\"...\")]` attributes aren't allowed"),
166            ));
167        }
168        prev.common = super::ContainerAttributes::merge_attrs(
169            Spanning::new(prev.common, prev_span),
170            Spanning::new(new.common, new_span),
171            name,
172        )?
173        .into_inner();
174
175        Ok(Spanning::new(
176            prev,
177            prev_span.join(new_span).unwrap_or(prev_span),
178        ))
179    }
180}
181
182/// Type alias for an expansion context:
183/// - [`ContainerAttributes`].
184/// - Type parameters. Slice of [`syn::Ident`].
185/// - Struct/enum/union [`syn::Ident`].
186/// - Derived trait [`syn::Ident`].
187/// - Attribute name [`syn::Ident`].
188///
189/// [`syn::Ident`]: struct@syn::Ident
190type ExpansionCtx<'a> = (
191    &'a ContainerAttributes,
192    &'a [&'a syn::Ident],
193    &'a syn::Ident,
194    &'a syn::Ident,
195    &'a syn::Ident,
196);
197
198/// Expands a [`fmt::Display`]-like derive macro for the provided struct.
199fn expand_struct(
200    s: &syn::DataStruct,
201    (attrs, type_params, ident, trait_ident, _): ExpansionCtx<'_>,
202) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
203    let s = Expansion {
204        shared_attr: None,
205        attrs,
206        fields: &s.fields,
207        type_params,
208        trait_ident,
209        ident,
210    };
211    let bounds = s.generate_bounds();
212    let body = s.generate_body()?;
213
214    let vars = s.fields.iter().enumerate().map(|(i, f)| {
215        let var = f.ident.clone().unwrap_or_else(|| format_ident!("_{i}"));
216        let member = f
217            .ident
218            .clone()
219            .map_or_else(|| syn::Member::Unnamed(i.into()), syn::Member::Named);
220        quote! {
221            let #var = &self.#member;
222        }
223    });
224
225    let body = quote! {
226        #( #vars )*
227        #body
228    };
229
230    Ok((bounds, body))
231}
232
233/// Expands a [`fmt`]-like derive macro for the provided enum.
234fn expand_enum(
235    e: &syn::DataEnum,
236    (container_attrs, type_params, _, trait_ident, attr_name): ExpansionCtx<'_>,
237) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
238    if let Some(shared_fmt) = &container_attrs.common.fmt {
239        if shared_fmt
240            .placeholders_by_arg("_variant")
241            .any(|p| p.has_modifiers || p.trait_name != "Display")
242        {
243            // TODO: This limitation can be lifted, by analyzing the `shared_fmt` deeper and using
244            //       `&dyn fmt::TraitName` for transparency instead of just `format_args!()` in the
245            //       expansion.
246            return Err(syn::Error::new(
247                shared_fmt.span(),
248                "shared format `_variant` placeholder cannot contain format specifiers",
249            ));
250        }
251    }
252
253    let (bounds, match_arms) = e.variants.iter().try_fold(
254        (Vec::new(), TokenStream::new()),
255        |(mut bounds, mut arms), variant| {
256            let mut attrs = ContainerAttributes::parse_attrs(&variant.attrs, attr_name)?
257                .map(Spanning::into_inner)
258                .unwrap_or_default();
259            let ident = &variant.ident;
260
261            if attrs.common.fmt.is_none()
262                && variant.fields.is_empty()
263                && attr_name != "display" {
264                let container_fmt = container_attrs.common.fmt.as_ref();
265                if container_fmt.is_none() {
266                    return Err(syn::Error::new(
267                        e.variants.span(),
268                        format!(
269                            "implicit formatting of unit enum variant is supported only for \
270                             `Display` macro, use `#[{attr_name}(\"...\")]` to explicitly specify \
271                             the formatting on every variant or the enum",
272                        ),
273                    ));
274                } else if container_fmt.is_some_and(|fmt| fmt.contains_arg("_variant")) {
275                    return Err(syn::Error::new_spanned(
276                        variant,
277                        format!(
278                            "implicit formatting of unit enum variant is supported only for \
279                             `Display` macro, use `#[{attr_name}(\"...\")]` to explicitly specify \
280                             the formatting on every variant when using `{{_variant}}`",
281                        ),
282                    ));
283                }
284            }
285
286            if let Some(rename_all) = container_attrs.rename_all {
287                attrs.rename_all.get_or_insert(rename_all);
288            }
289
290            let v = Expansion {
291                shared_attr: container_attrs.common.fmt.as_ref(),
292                attrs: &attrs,
293                fields: &variant.fields,
294                type_params,
295                trait_ident,
296                ident,
297            };
298            let arm_body = v.generate_body()?;
299            bounds.extend(v.generate_bounds());
300
301            let fields_idents =
302                variant.fields.iter().enumerate().map(|(i, f)| {
303                    f.ident.clone().unwrap_or_else(|| format_ident!("_{i}"))
304                });
305            let matcher = match variant.fields {
306                syn::Fields::Named(_) => {
307                    quote! { Self::#ident { #( #fields_idents ),* } }
308                }
309                syn::Fields::Unnamed(_) => {
310                    quote! { Self::#ident ( #( #fields_idents ),* ) }
311                }
312                syn::Fields::Unit => quote! { Self::#ident },
313            };
314
315            arms.extend([quote! { #matcher => { #arm_body }, }]);
316
317            Ok::<_, syn::Error>((bounds, arms))
318        },
319    )?;
320
321    let body = if match_arms.is_empty() {
322        quote! { match *self {} }
323    } else {
324        quote! { match self { #match_arms } }
325    };
326
327    Ok((bounds, body))
328}
329
330/// Expands a [`fmt::Display`]-like derive macro for the provided union.
331fn expand_union(
332    u: &syn::DataUnion,
333    (attrs, _, _, _, attr_name): ExpansionCtx<'_>,
334) -> syn::Result<(Vec<syn::WherePredicate>, TokenStream)> {
335    let fmt = &attrs.common.fmt.as_ref().ok_or_else(|| {
336        syn::Error::new(
337            u.fields.span(),
338            format!("unions must have `#[{attr_name}(\"...\", ...)]` attribute"),
339        )
340    })?;
341
342    Ok((
343        attrs.common.bounds.0.clone().into_iter().collect(),
344        quote! { derive_more::core::write!(__derive_more_f, #fmt) },
345    ))
346}
347
348/// Helper struct to generate [`Display::fmt()`] implementation body and trait
349/// bounds for a struct or an enum variant.
350///
351/// [`Display::fmt()`]: fmt::Display::fmt()
352#[derive(Debug)]
353struct Expansion<'a> {
354    /// [`FmtAttribute`] shared between all variants of an enum.
355    ///
356    /// [`None`] for a struct.
357    shared_attr: Option<&'a FmtAttribute>,
358
359    /// Derive macro [`ContainerAttributes`].
360    attrs: &'a ContainerAttributes,
361
362    /// Struct or enum [`syn::Ident`].
363    ///
364    /// [`syn::Ident`]: struct@syn::Ident
365    ident: &'a syn::Ident,
366
367    /// Struct or enum [`syn::Fields`].
368    fields: &'a syn::Fields,
369
370    /// Type parameters in this struct or enum.
371    type_params: &'a [&'a syn::Ident],
372
373    /// [`fmt`] trait [`syn::Ident`].
374    ///
375    /// [`syn::Ident`]: struct@syn::Ident
376    trait_ident: &'a syn::Ident,
377}
378
379impl Expansion<'_> {
380    /// Checks and indicates whether a top-level shared [`FmtAttribute`] is present in this
381    /// [`Expansion`], and whether it has wrapping logic (e.g. uses `_variant` placeholder).
382    fn shared_attr_info(&self) -> (bool, bool) {
383        let shared_attr_contains_variant = self
384            .shared_attr
385            .map_or(true, |attr| attr.contains_arg("_variant"));
386        // If `shared_attr` is a transparent call to `_variant`, then we consider it being absent.
387        let has_shared_attr = self.shared_attr.is_some_and(|attr| {
388            attr.transparent_call().map_or(true, |(_, called_trait)| {
389                &called_trait != self.trait_ident || !shared_attr_contains_variant
390            })
391        });
392        (
393            has_shared_attr,
394            has_shared_attr && shared_attr_contains_variant,
395        )
396    }
397
398    /// Generates [`Display::fmt()`] implementation for a struct or an enum variant.
399    ///
400    /// # Errors
401    ///
402    /// In case [`FmtAttribute`] is [`None`] and [`syn::Fields`] length is greater than 1.
403    ///
404    /// [`Display::fmt()`]: fmt::Display::fmt()
405    fn generate_body(&self) -> syn::Result<TokenStream> {
406        let mut body = TokenStream::new();
407
408        let (has_shared_attr, shared_attr_is_wrapping) = self.shared_attr_info();
409
410        let wrap_into_shared_attr = match &self.attrs.common.fmt {
411            Some(fmt) => {
412                body = if shared_attr_is_wrapping {
413                    let deref_args = fmt.additional_deref_args(self.fields);
414
415                    quote! { &derive_more::core::format_args!(#fmt, #(#deref_args),*) }
416                } else if let Some((expr, trait_ident)) =
417                    fmt.transparent_call_on_fields(self.fields)
418                {
419                    quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) }
420                } else {
421                    let deref_args = fmt.additional_deref_args(self.fields);
422
423                    quote! { derive_more::core::write!(__derive_more_f, #fmt, #(#deref_args),*) }
424                };
425                shared_attr_is_wrapping
426            }
427            None => {
428                if shared_attr_is_wrapping || !has_shared_attr {
429                    body = if self.fields.is_empty() {
430                        let mut ident_str = self.ident.unraw().to_string();
431                        if let Some(rename_all) = &self.attrs.rename_all {
432                            ident_str = rename_all.convert_case(&ident_str);
433                        }
434
435                        if shared_attr_is_wrapping {
436                            quote! { #ident_str }
437                        } else {
438                            quote! { __derive_more_f.write_str(#ident_str) }
439                        }
440                    } else if self.fields.len() == 1 {
441                        let field = self
442                            .fields
443                            .iter()
444                            .next()
445                            .unwrap_or_else(|| unreachable!("count() == 1"));
446                        let ident =
447                            field.ident.clone().unwrap_or_else(|| format_ident!("_0"));
448                        let trait_ident = self.trait_ident;
449
450                        if shared_attr_is_wrapping {
451                            let placeholder =
452                                trait_name_to_default_placeholder_literal(trait_ident);
453
454                            quote! { &derive_more::core::format_args!(#placeholder, #ident) }
455                        } else {
456                            quote! {
457                                derive_more::core::fmt::#trait_ident::fmt(#ident, __derive_more_f)
458                            }
459                        }
460                    } else {
461                        return Err(syn::Error::new(
462                            self.fields.span(),
463                            format!(
464                                "struct or enum variant with more than 1 field must have \
465                                 `#[{}(\"...\", ...)]` attribute",
466                                trait_name_to_attribute_name(self.trait_ident),
467                            ),
468                        ));
469                    };
470                }
471                has_shared_attr
472            }
473        };
474        if wrap_into_shared_attr {
475            if let Some(shared_fmt) = &self.shared_attr {
476                let deref_args = shared_fmt.additional_deref_args(self.fields);
477
478                let shared_body = if let Some((expr, trait_ident)) =
479                    shared_fmt.transparent_call_on_fields(self.fields)
480                {
481                    quote! { derive_more::core::fmt::#trait_ident::fmt(#expr, __derive_more_f) }
482                } else {
483                    quote! {
484                        derive_more::core::write!(__derive_more_f, #shared_fmt, #(#deref_args),*)
485                    }
486                };
487
488                body = if body.is_empty() {
489                    shared_body
490                } else {
491                    quote! { match #body { _variant => #shared_body } }
492                }
493            }
494        }
495
496        Ok(body)
497    }
498
499    /// Generates trait bounds for a struct or an enum variant.
500    fn generate_bounds(&self) -> Vec<syn::WherePredicate> {
501        let mut bounds = vec![];
502
503        let (has_shared_attr, shared_attr_is_wrapping) = self.shared_attr_info();
504
505        let mix_shared_attr_bounds = match &self.attrs.common.fmt {
506            Some(attr) => {
507                bounds.extend(
508                    attr.bounded_types(self.fields)
509                        .filter_map(|(ty, trait_name)| {
510                            if !ty.contains_generics(self.type_params) {
511                                return None;
512                            }
513                            let trait_ident = format_ident!("{trait_name}");
514
515                            Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident })
516                        })
517                        .chain(self.attrs.common.bounds.0.clone()),
518                );
519                shared_attr_is_wrapping
520            }
521            None => {
522                if shared_attr_is_wrapping || !has_shared_attr {
523                    bounds.extend(self.fields.iter().next().and_then(|f| {
524                        let ty = &f.ty;
525                        if !ty.contains_generics(self.type_params) {
526                            return None;
527                        }
528                        let trait_ident = &self.trait_ident;
529                        Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident })
530                    }));
531                }
532                has_shared_attr
533            }
534        };
535        if mix_shared_attr_bounds {
536            bounds.extend(
537                self.shared_attr
538                    .as_ref()
539                    .unwrap()
540                    .bounded_types(self.fields)
541                    .filter_map(|(ty, trait_name)| {
542                        if !ty.contains_generics(self.type_params) {
543                            return None;
544                        }
545                        let trait_ident = format_ident!("{trait_name}");
546
547                        Some(parse_quote! { #ty: derive_more::core::fmt::#trait_ident })
548                    }),
549            );
550        }
551
552        bounds
553    }
554}
555
556/// Matches the provided derive macro `name` to appropriate actual trait name.
557fn normalize_trait_name(name: &str) -> &'static str {
558    match name {
559        "Binary" => "Binary",
560        "Display" => "Display",
561        "LowerExp" => "LowerExp",
562        "LowerHex" => "LowerHex",
563        "Octal" => "Octal",
564        "Pointer" => "Pointer",
565        "UpperExp" => "UpperExp",
566        "UpperHex" => "UpperHex",
567        _ => unimplemented!(),
568    }
569}
570
571/// Matches the provided [`fmt`] trait `name` to its default formatting placeholder.
572fn trait_name_to_default_placeholder_literal(name: &syn::Ident) -> &'static str {
573    match () {
574        _ if name == "Binary" => "{:b}",
575        _ if name == "Debug" => "{:?}",
576        _ if name == "Display" => "{}",
577        _ if name == "LowerExp" => "{:e}",
578        _ if name == "LowerHex" => "{:x}",
579        _ if name == "Octal" => "{:o}",
580        _ if name == "Pointer" => "{:p}",
581        _ if name == "UpperExp" => "{:E}",
582        _ if name == "UpperHex" => "{:X}",
583        _ => unimplemented!(),
584    }
585}