From ffa26f73de8ab139e50457525bc287bda5653612 Mon Sep 17 00:00:00 2001 From: mc1098 Date: Tue, 10 May 2022 16:57:33 +0100 Subject: [PATCH] Fix destructuring async props (#419) * Add failing trybuild test Adds a test for destructed struct pattern using an async component that is currently failing just like in #410. * Fix destructuring props in async components This change fixes `#[component]` proc macro generation to support the struct destructured pattern. This was different than non-async structs as async structs are converted into non-async structs. * Remove map and unwrap early We can unwrap so avoid the nesting with map and just unwrap immediately and perform the match on the prop argument. * Remove incorrect comment regarding sync args * Improve test to include generic and lifetimes Replace the simple destructuring test with the more complicated one to show that destructuring works even with lifetimes and generics included. * Fix async component test Makes the `AsyncComponentWithPropDestructuring` function actually async.. --- packages/sycamore-macro/src/component/mod.rs | 139 +++++++++++++++--- .../tests/view/component-pass.rs | 14 ++ 2 files changed, 130 insertions(+), 23 deletions(-) diff --git a/packages/sycamore-macro/src/component/mod.rs b/packages/sycamore-macro/src/component/mod.rs index 4768656bc..56300320a 100644 --- a/packages/sycamore-macro/src/component/mod.rs +++ b/packages/sycamore-macro/src/component/mod.rs @@ -64,9 +64,16 @@ impl Parse for ComponentFunction { )); } - if let FnArg::Receiver(arg) = &inputs[0] { + if let FnArg::Typed(t) = &inputs[0] { + if !matches!(&*t.pat, Pat::Ident(_)) { + return Err(syn::Error::new( + t.span(), + "First argument to a component is expected to be a `sycamore::reactive::Scope`", + )); + } + } else { return Err(syn::Error::new( - arg.span(), + inputs[0].span(), "function components can't accept a receiver", )); } @@ -97,6 +104,95 @@ impl Parse for ComponentFunction { } } +struct AsyncCompInputs { + cx: syn::Ident, + sync_input: Punctuated, + async_args: Vec, +} + +fn async_comp_inputs_from_sig_inputs( + inputs: &Punctuated, +) -> AsyncCompInputs { + let mut sync_input = Punctuated::new(); + let mut async_args = Vec::with_capacity(2); + + #[inline] + fn pat_ident_arm( + sync_input: &mut Punctuated, + fn_arg: &FnArg, + id: &syn::PatIdent, + ) -> syn::Expr { + sync_input.push(fn_arg.clone()); + let ident = &id.ident; + parse_quote! { #ident } + } + + let mut inputs = inputs.iter(); + + let cx_fn_arg = inputs.next().unwrap(); + + let cx = if let FnArg::Typed(t) = cx_fn_arg { + if let Pat::Ident(id) = &*t.pat { + async_args.push(pat_ident_arm(&mut sync_input, cx_fn_arg, id)); + id.ident.clone() + } else { + unreachable!("We check in parsing that the first argument is a Ident"); + } + } else { + unreachable!("We check in parsing that the first argument is not a receiver"); + }; + + // In parsing we checked that there were two args so we can unwrap here. + let prop_fn_arg = inputs.next().unwrap(); + let prop_arg = match prop_fn_arg { + FnArg::Typed(t) => match &*t.pat { + Pat::Ident(id) => pat_ident_arm(&mut sync_input, prop_fn_arg, id), + Pat::Wild(_) => { + sync_input.push(prop_fn_arg.clone()); + parse_quote!(()) + } + Pat::Struct(pat_struct) => { + // For the sync input we don't want a destructured pattern but just to take a + // `syn::PatType` (i.e. `props: MyPropStruct`) then the inner async function + // signature can have the destructured pattern and it will work correctly + // aslong as we provide our brand new ident that we used in the + // `syn::PatIdent`. + let ident = syn::Ident::new("props", pat_struct.span()); + // props are taken by value so no refs or mutability required here + // The destructured pattern can add mutability (if required) even without this + // set. + let pat_ident = syn::PatIdent { + attrs: vec![], + by_ref: None, + mutability: None, + ident, + subpat: None, + }; + let pat_type = syn::PatType { + attrs: vec![], + pat: Box::new(Pat::Ident(pat_ident)), + colon_token: Default::default(), + ty: t.ty.clone(), + }; + + let fn_arg = FnArg::Typed(pat_type); + sync_input.push(fn_arg); + parse_quote! { props } + } + _ => panic!("unexpected pattern!"), + }, + FnArg::Receiver(_) => unreachable!(), + }; + + async_args.push(prop_arg); + + AsyncCompInputs { + cx, + async_args, + sync_input, + } +} + impl ToTokens for ComponentFunction { fn to_tokens(&self, tokens: &mut TokenStream) { let ComponentFunction { f } = self; @@ -108,23 +204,27 @@ impl ToTokens for ComponentFunction { } = &f; if sig.asyncness.is_some() { + // When the component function is async then we need to extract out some of the + // function signature (Syn::Signature) so that we can wrap the async function with + // a non-async component. + // + // In order to support the struct destructured pattern for props we alter the existing + // signature for the non-async component so that it is defined as a `Syn::PatType` + // (i.e. props: MyPropStruct) with a new `Syn::Ident` "props". We then use this ident + // again as an argument to the inner async function which has the user defined + // destructured pattern which will work as expected. + // + // Note: that the change to the signature is not semantically different to a would be caller. let inputs = &sig.inputs; - let args: Vec = inputs - .iter() - .map(|x| match x { - FnArg::Typed(t) => match &*t.pat { - Pat::Ident(id) => { - let id = &id.ident; - parse_quote! { #id } - } - Pat::Wild(_) => parse_quote!(()), - _ => panic!("unexpected pattern"), // TODO - }, - FnArg::Receiver(_) => unreachable!(), - }) - .collect::>(); + let AsyncCompInputs { + cx, + sync_input, + async_args: args, + } = async_comp_inputs_from_sig_inputs(inputs); + let non_async_sig = Signature { asyncness: None, + inputs: sync_input, ..sig.clone() }; let inner_ident = format_ident!("{}_inner", sig.ident); @@ -132,13 +232,6 @@ impl ToTokens for ComponentFunction { ident: inner_ident.clone(), ..sig.clone() }; - let cx = match inputs.first().unwrap() { - FnArg::Typed(t) => match &*t.pat { - Pat::Ident(id) => &id.ident, - _ => unreachable!(), - }, - FnArg::Receiver(_) => unreachable!(), - }; tokens.extend(quote! { #[allow(non_snake_case)] #(#attrs)* diff --git a/packages/sycamore-macro/tests/view/component-pass.rs b/packages/sycamore-macro/tests/view/component-pass.rs index 8d513d1b3..c4ca3f4b3 100644 --- a/packages/sycamore-macro/tests/view/component-pass.rs +++ b/packages/sycamore-macro/tests/view/component-pass.rs @@ -22,6 +22,14 @@ pub fn ComponentWithChildren<'a, G: Html>(cx: Scope<'a>, prop: PropWithChildren< prop.children.call(cx) } +#[component] +pub async fn AsyncComponentWithPropDestructuring<'a, G: Html>( + cx: Scope<'a>, + PropWithChildren { children }: PropWithChildren<'a, G>, +) -> View { + children.call(cx) +} + #[component] pub fn Component(cx: Scope) -> View { view! { cx, @@ -49,6 +57,12 @@ fn compile_pass() { Component {} } }; + + let _: View = view! { cx, + AsyncComponentWithPropDestructuring { + Component {} + } + }; }); }