sycamore_macro/
component.rs

1//! The `#[component]` attribute macro implementation.
2
3use proc_macro2::TokenStream;
4use quote::{format_ident, quote, ToTokens};
5use syn::parse::{Parse, ParseStream};
6use syn::punctuated::Punctuated;
7use syn::spanned::Spanned;
8use syn::{
9    parse_quote, Error, Expr, FnArg, Ident, Item, ItemFn, Pat, PatIdent, Result, ReturnType,
10    Signature, Token, Type, TypeTuple,
11};
12
13pub struct ComponentFn {
14    pub f: ItemFn,
15}
16
17impl Parse for ComponentFn {
18    fn parse(input: ParseStream) -> Result<Self> {
19        // Parse macro body.
20        let parsed: Item = input.parse()?;
21
22        match parsed {
23            Item::Fn(mut f) => {
24                let ItemFn { sig, .. } = &mut f;
25
26                if sig.constness.is_some() {
27                    return Err(syn::Error::new(
28                        sig.constness.span(),
29                        "const functions can't be components",
30                    ));
31                }
32
33                if sig.abi.is_some() {
34                    return Err(syn::Error::new(
35                        sig.abi.span(),
36                        "extern functions can't be components",
37                    ));
38                }
39
40                if let ReturnType::Default = sig.output {
41                    return Err(syn::Error::new(
42                        sig.paren_token.span.close(),
43                        "component must return `sycamore::view::View`",
44                    ));
45                };
46
47                let inputs = sig.inputs.clone().into_iter().collect::<Vec<_>>();
48
49                match &inputs[..] {
50                    [] => {}
51                    [input] => {
52                        if let FnArg::Receiver(_) = input {
53                            return Err(syn::Error::new(
54                                input.span(),
55                                "components can't accept a receiver",
56                            ));
57                        }
58
59                        if let FnArg::Typed(pat) = input {
60                            if let Type::Tuple(TypeTuple { elems, .. }) = &*pat.ty {
61                                if elems.is_empty() {
62                                    return Err(syn::Error::new(
63                                        pat.ty.span(),
64                                        "taking an unit tuple as props is useless",
65                                    ));
66                                }
67                            }
68                        }
69                    }
70                    [..] => {
71                        if inputs.len() > 1 {
72                            return Err(syn::Error::new(
73                                sig.inputs
74                                    .clone()
75                                    .into_iter()
76                                    .skip(2)
77                                    .collect::<Punctuated<_, Token![,]>>()
78                                    .span(),
79                                "component should not take more than 1 parameter",
80                            ));
81                        }
82                    }
83                };
84
85                Ok(Self { f })
86            }
87            item => Err(syn::Error::new_spanned(
88                item,
89                "the `component` attribute can only be applied to functions",
90            )),
91        }
92    }
93}
94
95struct AsyncCompInputs {
96    sync_input: Punctuated<FnArg, Token![,]>,
97    async_args: Vec<Expr>,
98}
99
100fn async_comp_inputs_from_sig_inputs(inputs: &Punctuated<FnArg, Token![,]>) -> AsyncCompInputs {
101    let mut sync_input = Punctuated::new();
102    let mut async_args = Vec::new();
103
104    fn pat_ident_arm(
105        sync_input: &mut Punctuated<FnArg, Token![,]>,
106        fn_arg: &FnArg,
107        id: &PatIdent,
108    ) -> Expr {
109        sync_input.push(fn_arg.clone());
110        let ident = &id.ident;
111        parse_quote! { #ident }
112    }
113
114    let mut inputs = inputs.iter();
115
116    let prop_arg = inputs.next();
117    let prop_arg = prop_arg.map(|prop_fn_arg| match prop_fn_arg {
118        FnArg::Typed(t) => match &*t.pat {
119            Pat::Ident(id) => pat_ident_arm(&mut sync_input, prop_fn_arg, id),
120            Pat::Struct(pat_struct) => {
121                // For the sync input we don't want a destructured pattern but just to take a
122                // `syn::PatType` (i.e. `props: MyPropsStruct`) then the inner async function
123                // signature can have the destructured pattern and it will work correctly
124                // as long as we provide our brand new ident that we used in the
125                // `syn::PatIdent`.
126                let ident = Ident::new("props", pat_struct.span());
127                // Props are taken by value so no refs or mutability required here
128                // The destructured pattern can add mutability (if required) even without this
129                // set.
130                let pat_ident = PatIdent {
131                    attrs: vec![],
132                    by_ref: None,
133                    mutability: None,
134                    ident,
135                    subpat: None,
136                };
137                let pat_type = syn::PatType {
138                    attrs: vec![],
139                    pat: Box::new(Pat::Ident(pat_ident)),
140                    colon_token: Default::default(),
141                    ty: t.ty.clone(),
142                };
143
144                let fn_arg = FnArg::Typed(pat_type);
145                sync_input.push(fn_arg);
146                parse_quote! { props }
147            }
148            _ => panic!("unexpected pattern!"),
149        },
150        FnArg::Receiver(_) => unreachable!(),
151    });
152
153    if let Some(arg) = prop_arg {
154        async_args.push(arg);
155    }
156
157    AsyncCompInputs {
158        async_args,
159        sync_input,
160    }
161}
162
163impl ToTokens for ComponentFn {
164    fn to_tokens(&self, tokens: &mut TokenStream) {
165        let ComponentFn { f } = self;
166        let ItemFn {
167            attrs,
168            vis,
169            sig,
170            block,
171        } = &f;
172
173        if sig.asyncness.is_some() {
174            // When the component function is async then we need to extract out some of the
175            // function signature (Syn::Signature) so that we can wrap the async function with
176            // a non-async component.
177            //
178            // In order to support the struct destructured pattern for props we alter the existing
179            // signature for the non-async component so that it is defined as a `Syn::PatType`
180            // (i.e. props: MyPropsStruct) with a new `Syn::Ident` "props". We then use this ident
181            // again as an argument to the inner async function which has the user defined
182            // destructured pattern which will work as expected.
183            //
184            // Note: this does not affect the signature of the function.
185            let inputs = &sig.inputs;
186            let AsyncCompInputs {
187                sync_input,
188                async_args: args,
189            } = async_comp_inputs_from_sig_inputs(inputs);
190
191            let non_async_sig = Signature {
192                asyncness: None,
193                inputs: sync_input,
194                ..sig.clone()
195            };
196            let inner_ident = format_ident!("{}_inner", sig.ident);
197            let inner_sig = Signature {
198                ident: inner_ident.clone(),
199                ..sig.clone()
200            };
201            tokens.extend(quote! {
202                // Create a new function that is not async so that it is just a standard component.
203                #(#attrs)*
204                #[::sycamore::component]
205                #vis #non_async_sig {
206                    // Define the original function as a nested function so that it cannot be
207                    // called from outside.
208                    #[allow(non_snake_case)]
209                    #inner_sig #block
210
211                    ::sycamore::rt::WrapAsync(move || #inner_ident(#(#args),*))
212                }
213            });
214        } else {
215            tokens.extend(quote! {
216                #[allow(non_snake_case)]
217                #f
218            })
219        }
220    }
221}
222
223/// Arguments to the `component` attribute proc-macro.
224pub struct ComponentArgs {
225    inline_props: Option<Ident>,
226}
227
228impl Parse for ComponentArgs {
229    fn parse(input: ParseStream) -> Result<Self> {
230        let inline_props: Option<Ident> = input.parse()?;
231        if let Some(inline_props) = &inline_props {
232            // Check if the ident is correct.
233            if *inline_props != "inline_props" {
234                return Err(Error::new(inline_props.span(), "expected `inline_props`"));
235            }
236        }
237        Ok(Self { inline_props })
238    }
239}
240
241pub fn component_impl(args: ComponentArgs, item: TokenStream) -> Result<TokenStream> {
242    if args.inline_props.is_some() {
243        let mut item_fn = syn::parse::<ItemFn>(item.into())?;
244        let inline_props = inline_props_impl(&mut item_fn)?;
245        // TODO: don't parse the function twice.
246        let comp = syn::parse::<ComponentFn>(item_fn.to_token_stream().into())?;
247        Ok(quote! {
248            #inline_props
249            #comp
250        })
251    } else {
252        let comp = syn::parse::<ComponentFn>(item.into())?;
253        Ok(comp.to_token_stream())
254    }
255}
256
257/// Codegens the new props struct and modifies the component body to accept this new struct as
258/// props.
259fn inline_props_impl(item: &mut ItemFn) -> Result<TokenStream> {
260    let props_vis = &item.vis;
261    let props_struct_ident = format_ident!("{}_Props", item.sig.ident);
262
263    let inputs = item.sig.inputs.clone();
264    let props = inputs.into_iter().collect::<Vec<_>>();
265
266    let generics = &item.sig.generics;
267    let generics_phantoms = generics.params.iter().enumerate().filter_map(|(i, param)| {
268        let phantom_ident = format_ident!("__phantom{i}");
269        match param {
270            syn::GenericParam::Type(ty) => {
271                let ty = &ty.ident;
272                Some(quote! {
273                    #[prop(default, setter(skip))]
274                    #phantom_ident: ::std::marker::PhantomData<#ty>
275                })
276            }
277            syn::GenericParam::Lifetime(lt) => {
278                let lt = &lt.lifetime;
279                Some(quote! {
280                    #[prop(default, setter(skip))]
281                    #phantom_ident: ::std::marker::PhantomData<&#lt ()>
282                })
283            }
284            syn::GenericParam::Const(_) => None,
285        }
286    });
287
288    let doc_comment = format!("Props for [`{}`].", item.sig.ident);
289
290    let ret = Ok(quote! {
291        #[allow(non_camel_case_types)]
292        #[doc = #doc_comment]
293        #[derive(::sycamore::rt::Props)]
294        #props_vis struct #props_struct_ident #generics {
295            #(#props,)*
296            #(#generics_phantoms,)*
297        }
298    });
299
300    // Rewrite component body.
301
302    // Get the ident (technically, patterns) of each prop.
303    let props_pats = props.iter().map(|arg| match arg {
304        FnArg::Receiver(_) => unreachable!("receiver cannot be a prop"),
305        FnArg::Typed(arg) => arg.pat.clone(),
306    });
307    // Rewrite function signature.
308    let props_struct_generics = generics.split_for_impl().1;
309    item.sig.inputs = parse_quote! { __props: #props_struct_ident #props_struct_generics };
310    // Rewrite function body.
311    let block = item.block.clone();
312    item.block = parse_quote! {{
313        let #props_struct_ident {
314            #(#props_pats,)*
315            ..
316        } = __props;
317        #block
318    }};
319
320    ret
321}