Simplify and fix some issues with #[component] macro (#2289)
This commit is contained in:
1 parent
a84c2ec6d2
commit
b97e607aca
1 file changed
+65
-42
@@ -11,7 +11,7 @@ pub struct ComponentBody {
|
|||||||
impl Parse for ComponentBody {
|
impl Parse for ComponentBody {
|
||||||
fn parse(input: ParseStream) -> Result<Self> {
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
let item_fn: ItemFn = input.parse()?;
|
let item_fn: ItemFn = input.parse()?;
|
||||||
validate_component_fn_signature(&item_fn)?;
|
validate_component_fn(&item_fn)?;
|
||||||
Ok(Self { item_fn })
|
Ok(Self { item_fn })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,12 +22,13 @@ impl ToTokens for ComponentBody {
|
|||||||
// If there's only one input and the input is `props: Props`, we don't need to generate a props struct
|
// If there's only one input and the input is `props: Props`, we don't need to generate a props struct
|
||||||
// Just attach the non_snake_case attribute to the function
|
// Just attach the non_snake_case attribute to the function
|
||||||
// eventually we'll dump this metadata into devtooling that lets us find all these components
|
// eventually we'll dump this metadata into devtooling that lets us find all these components
|
||||||
if self.is_explicit_props_ident() {
|
//
|
||||||
|
// Components can also use the struct pattern to "inline" their props.
|
||||||
|
// Freya uses this a bunch (because it's clean),
|
||||||
|
// e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed
|
||||||
|
if self.is_explicit_props_ident() || self.has_struct_parameter_pattern() {
|
||||||
let comp_fn = &self.item_fn;
|
let comp_fn = &self.item_fn;
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(allow_camel_case_for_fn_ident(comp_fn).into_token_stream());
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#comp_fn
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,8 +56,6 @@ impl ToTokens for ComponentBody {
|
|||||||
|
|
||||||
tokens.append_all(quote! {
|
tokens.append_all(quote! {
|
||||||
#props_struct
|
#props_struct
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#comp_fn
|
#comp_fn
|
||||||
|
|
||||||
#completion_hints
|
#completion_hints
|
||||||
@@ -79,41 +78,38 @@ impl ComponentBody {
|
|||||||
ident: fn_ident,
|
ident: fn_ident,
|
||||||
generics,
|
generics,
|
||||||
output: fn_output,
|
output: fn_output,
|
||||||
asyncness,
|
|
||||||
..
|
..
|
||||||
} = sig;
|
} = sig;
|
||||||
|
|
||||||
let Generics { where_clause, .. } = generics;
|
let Generics { where_clause, .. } = generics;
|
||||||
let (_, ty_generics, _) = generics.split_for_impl();
|
let (_, impl_generics, _) = generics.split_for_impl();
|
||||||
let generics_turbofish = ty_generics.as_turbofish();
|
let generics_turbofish = impl_generics.as_turbofish();
|
||||||
|
|
||||||
// We generate a struct with the same name as the component but called `Props`
|
// We generate a struct with the same name as the component but called `Props`
|
||||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||||
|
|
||||||
// We pull in the field names from the original function signature, but need to strip off the mutability
|
// We pull in the field names from the original function signature, but need to strip off the mutability
|
||||||
let struct_field_names = inputs.iter().filter_map(rebind_mutability);
|
let struct_field_names = inputs.iter().map(rebind_mutability);
|
||||||
|
|
||||||
let props_docs = self.props_docs(inputs.iter().skip(1).collect());
|
let props_docs = self.props_docs(inputs.iter().collect());
|
||||||
|
|
||||||
// Don't generate the props argument if there are no inputs
|
let inlined_props_argument = if inputs.is_empty() {
|
||||||
// This means we need to skip adding the argument to the function signature, and also skip the expanded struct
|
quote! {}
|
||||||
let props_ident = match inputs.is_empty() {
|
} else {
|
||||||
true => quote! {},
|
quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics }
|
||||||
false => quote! { mut __props: #struct_ident #ty_generics },
|
|
||||||
};
|
|
||||||
let expanded_struct = match inputs.is_empty() {
|
|
||||||
true => quote! {},
|
|
||||||
false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The extra nest is for the snake case warning to kick back in
|
||||||
parse_quote! {
|
parse_quote! {
|
||||||
#(#attrs)*
|
#(#attrs)*
|
||||||
#(#props_docs)*
|
#(#props_docs)*
|
||||||
#asyncness #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause {
|
#[allow(non_snake_case)]
|
||||||
// In debug mode we can detect if the user is calling the component like a function
|
#vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause {
|
||||||
dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
|
{
|
||||||
|
// In debug mode we can detect if the user is calling the component like a function
|
||||||
#expanded_struct
|
dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
|
||||||
#block
|
#block
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +149,7 @@ impl ComponentBody {
|
|||||||
fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||||
let fn_ident = &self.item_fn.sig.ident;
|
let fn_ident = &self.item_fn.sig.ident;
|
||||||
|
|
||||||
if inputs.len() <= 1 {
|
if inputs.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +175,7 @@ impl ComponentBody {
|
|||||||
input_arg_doc,
|
input_arg_doc,
|
||||||
} = arg;
|
} = arg;
|
||||||
|
|
||||||
let arg_name = arg_name.into_token_stream().to_string();
|
let arg_name = strip_pat_mutability(arg_name).to_token_stream().to_string();
|
||||||
let arg_type = crate::utils::format_type_string(arg_type);
|
let arg_type = crate::utils::format_type_string(arg_type);
|
||||||
|
|
||||||
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
||||||
@@ -219,11 +215,19 @@ impl ComponentBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_explicit_props_ident(&self) -> bool {
|
fn is_explicit_props_ident(&self) -> bool {
|
||||||
if self.item_fn.sig.inputs.len() == 1 {
|
if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
|
||||||
if let FnArg::Typed(PatType { pat, .. }) = &self.item_fn.sig.inputs[0] {
|
if let Pat::Ident(ident) = pat.as_ref() {
|
||||||
if let Pat::Ident(ident) = pat.as_ref() {
|
return ident.ident == "props";
|
||||||
return ident.ident == "props";
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_struct_parameter_pattern(&self) -> bool {
|
||||||
|
if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
|
||||||
|
if matches!(pat.as_ref(), Pat::Struct(_)) {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +304,7 @@ fn build_doc_fields(f: &FnArg) -> Option<DocField> {
|
|||||||
arg_name: &pt.pat,
|
arg_name: &pt.pat,
|
||||||
arg_type: &pt.ty,
|
arg_type: &pt.ty,
|
||||||
deprecation: pt.attrs.iter().find_map(|attr| {
|
deprecation: pt.attrs.iter().find_map(|attr| {
|
||||||
if attr.path() != &parse_quote!(deprecated) {
|
if !attr.path().is_ident("deprecated") {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +319,7 @@ fn build_doc_fields(f: &FnArg) -> Option<DocField> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
|
fn validate_component_fn(item_fn: &ItemFn) -> Result<()> {
|
||||||
// Do some validation....
|
// Do some validation....
|
||||||
// 1. Ensure the component returns *something*
|
// 1. Ensure the component returns *something*
|
||||||
if item_fn.sig.output == ReturnType::Default {
|
if item_fn.sig.output == ReturnType::Default {
|
||||||
@@ -395,20 +399,23 @@ fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebind_mutability(f: &FnArg) -> Option<TokenStream> {
|
fn rebind_mutability(f: &FnArg) -> TokenStream {
|
||||||
// There's no receivers (&self) allowed in the component body
|
// There's no receivers (&self) allowed in the component body
|
||||||
let FnArg::Typed(pt) = f else { unreachable!() };
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
||||||
|
|
||||||
let pat = &pt.pat;
|
let immutable = strip_pat_mutability(&pt.pat);
|
||||||
|
|
||||||
|
quote!(mut #immutable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_pat_mutability(pat: &Pat) -> Pat {
|
||||||
let mut pat = pat.clone();
|
let mut pat = pat.clone();
|
||||||
|
|
||||||
// rip off mutability, but still write it out eventually
|
// rip off mutability, but still write it out eventually
|
||||||
if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
|
if let Pat::Ident(ref mut pat_ident) = &mut pat {
|
||||||
pat_ident.mutability = None;
|
pat_ident.mutability = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(quote!(mut #pat))
|
pat
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the attribute is a `#[doc]` attribute.
|
/// Checks if the attribute is a `#[doc]` attribute.
|
||||||
@@ -443,3 +450,19 @@ fn keep_up_to_n_consecutive_chars(
|
|||||||
|
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is allowed by the compiler.
|
||||||
|
fn allow_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn {
|
||||||
|
let mut clone = item_fn.clone();
|
||||||
|
let block = &item_fn.block;
|
||||||
|
|
||||||
|
clone.attrs.push(parse_quote! { #[allow(non_snake_case)] });
|
||||||
|
|
||||||
|
clone.block = parse_quote! {
|
||||||
|
{
|
||||||
|
#block
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
clone
|
||||||
|
}
|
||||||
Reference in new issue
Block a user