1use 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 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 let ident = Ident::new("props", pat_struct.span());
127 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 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 #(#attrs)*
204 #[::sycamore::component]
205 #vis #non_async_sig {
206 #[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
223pub 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 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 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
257fn 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 = <.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 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 let props_struct_generics = generics.split_for_impl().1;
309 item.sig.inputs = parse_quote! { __props: #props_struct_ident #props_struct_generics };
310 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}