Skip to content

Commit

Permalink
Fix destructuring async props (#419)
Browse files Browse the repository at this point in the history
* 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..
  • Loading branch information
mc1098 authored May 10, 2022
1 parent 49135bd commit ffa26f7
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 23 deletions.
139 changes: 116 additions & 23 deletions packages/sycamore-macro/src/component/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
));
}
Expand Down Expand Up @@ -97,6 +104,95 @@ impl Parse for ComponentFunction {
}
}

struct AsyncCompInputs {
cx: syn::Ident,
sync_input: Punctuated<FnArg, syn::token::Comma>,
async_args: Vec<Expr>,
}

fn async_comp_inputs_from_sig_inputs(
inputs: &Punctuated<FnArg, syn::token::Comma>,
) -> AsyncCompInputs {
let mut sync_input = Punctuated::new();
let mut async_args = Vec::with_capacity(2);

#[inline]
fn pat_ident_arm(
sync_input: &mut Punctuated<FnArg, syn::token::Comma>,
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;
Expand All @@ -108,37 +204,34 @@ 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<Expr> = 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::<Vec<_>>();
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);
let inner_sig = Signature {
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)*
Expand Down
14 changes: 14 additions & 0 deletions packages/sycamore-macro/tests/view/component-pass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<G> {
children.call(cx)
}

#[component]
pub fn Component<G: Html>(cx: Scope) -> View<G> {
view! { cx,
Expand Down Expand Up @@ -49,6 +57,12 @@ fn compile_pass<G: Html>() {
Component {}
}
};

let _: View<G> = view! { cx,
AsyncComponentWithPropDestructuring {
Component {}
}
};
});
}

Expand Down

0 comments on commit ffa26f7

Please sign in to comment.