|
@@ -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! {
|
|
|
|
- #[allow(non_snake_case)]
|
|
|
|
- #comp_fn
|
|
|
|
- });
|
|
|
|
|
|
+ tokens.append_all(allow_camel_case_for_fn_ident(comp_fn).into_token_stream());
|
|
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 generics_turbofish = ty_generics.as_turbofish();
|
|
|
|
|
|
+ let (_, impl_generics, _) = generics.split_for_impl();
|
|
|
|
+ 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
|
|
|
|
- // This means we need to skip adding the argument to the function signature, and also skip the expanded struct
|
|
|
|
- let props_ident = match inputs.is_empty() {
|
|
|
|
- true => quote! {},
|
|
|
|
- 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; },
|
|
|
|
|
|
+ let inlined_props_argument = if inputs.is_empty() {
|
|
|
|
+ quote! {}
|
|
|
|
+ } else {
|
|
|
|
+ quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics }
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+ // 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 {
|
|
|
|
- // In debug mode we can detect if the user is calling the component like a function
|
|
|
|
- dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
|
|
|
|
-
|
|
|
|
- #expanded_struct
|
|
|
|
- #block
|
|
|
|
|
|
+ #[allow(non_snake_case)]
|
|
|
|
+ #vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause {
|
|
|
|
+ {
|
|
|
|
+ // In debug mode we can detect if the user is calling the component like a function
|
|
|
|
+ dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
|
|
|
|
+ #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 FnArg::Typed(PatType { pat, .. }) = &self.item_fn.sig.inputs[0] {
|
|
|
|
- if let Pat::Ident(ident) = pat.as_ref() {
|
|
|
|
- return ident.ident == "props";
|
|
|
|
- }
|
|
|
|
|
|
+ if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
|
|
|
|
+ if let Pat::Ident(ident) = pat.as_ref() {
|
|
|
|
+ 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);
|
|
|
|
|
|
- let mut pat = pat.clone();
|
|
|
|
|
|
+ quote!(mut #immutable)
|
|
|
|
+}
|
|
|
|
|
|
|
|
+fn strip_pat_mutability(pat: &Pat) -> Pat {
|
|
|
|
+ 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
|
|
|
|
+}
|