synthez_core/codegen/
to_tokens.rs

1//! `#[derive(ToTokens)]` proc macro implementation.
2
3use proc_macro2::{Span, TokenStream};
4use quote::quote;
5use syn::{
6    parse::{Parse, ParseStream},
7    token,
8};
9
10use crate::{
11    parse::{
12        attrs::{dedup, field::TryMerge as _, kind},
13        err,
14        ext::ParseBuffer as _,
15    },
16    ParseAttrs,
17};
18
19/// Name of the derived trait.
20const TRAIT_NAME: &str = "ToTokens";
21
22/// Name of the helper attribute of this `proc_macro_derive`.
23const ATTR_NAME: &str = "to_tokens";
24
25/// Expands `#[derive(ToTokens)]` proc macro.
26///
27/// # Errors
28///
29/// - If the proc macro isn't applied to a struct or an enum.
30/// - If parsing `#[to_tokens]` helper attribute fails.
31pub fn derive(input: &syn::DeriveInput) -> syn::Result<TokenStream> {
32    if !matches!(&input.data, syn::Data::Enum(_) | syn::Data::Struct(_)) {
33        return Err(syn::Error::new_spanned(
34            input,
35            format!("only structs and enums can derive {TRAIT_NAME}"),
36        ));
37    }
38
39    let attrs = Attrs::parse_attrs(ATTR_NAME, input)?;
40
41    let ty = &input.ident;
42    let (impl_generics, ty_generics, where_clause) =
43        input.generics.split_for_impl();
44
45    let impls = attrs.append.iter().map(|method| {
46        quote! {
47            ::synthez::quote::ToTokens::to_tokens(&self.#method(), out);
48        }
49    });
50
51    Ok(quote! {
52        #[automatically_derived]
53        impl #impl_generics ::synthez::quote::ToTokens for #ty #ty_generics
54             #where_clause
55        {
56            fn to_tokens(
57                &self,
58                out: &mut ::synthez::proc_macro2::TokenStream,
59            ) {
60                #( #impls )*
61            }
62        }
63    })
64}
65
66/// Representation of a `#[to_tokens]` attribute used along with a
67/// `#[derive(ToTokens)]` proc macro on a top-level definition.
68#[derive(Debug, Default)]
69struct Attrs {
70    /// Methods to be called in the generated [`ToTokens`] implementation.
71    ///
72    /// [`ToTokens`]: quote::ToTokens
73    // #[parse(value)]
74    append: Vec<syn::Ident>,
75}
76
77impl Parse for Attrs {
78    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
79        let mut out = Self::default();
80        while !input.is_empty() {
81            let ident = input.fork().parse_any_ident()?;
82            match ident.to_string().as_str() {
83                "append" => {
84                    input.skip_any_ident()?;
85                    for v in input.parse_eq_or_wrapped_and_punctuated::<
86                        syn::Ident, token::Paren, token::Comma,
87                    >()? {
88                        out.append.try_merge::<kind::Value, dedup::Unique>(v)?;
89                    }
90                }
91                name => {
92                    return Err(err::unknown_attr_arg(&ident, name));
93                }
94            }
95            if input.try_parse::<token::Comma>()?.is_none() && !input.is_empty()
96            {
97                return Err(err::expected_followed_by_comma(&ident));
98            }
99        }
100        Ok(out)
101    }
102}
103
104impl ParseAttrs for Attrs {
105    fn try_merge(mut self, another: Self) -> syn::Result<Self> {
106        self.append
107            .try_merge_self::<kind::Value, dedup::Unique>(another.append)?;
108        Ok(self)
109    }
110
111    fn validate(&self, attr_name: &str, item_span: Span) -> syn::Result<()> {
112        if self.append.is_empty() {
113            return Err(syn::Error::new(
114                item_span,
115                format!(
116                    "`#[{attr_name}(append(<function>))]` attribute is \
117                     expected",
118                ),
119            ));
120        }
121        Ok(())
122    }
123}