diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index d05a12d06..c5761876c 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -4,10 +4,6 @@ use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::*; -/// General struct for parsing a component body. -/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens). -/// -/// Refer to the [module documentation](crate::component_body) for more. pub struct ComponentBody { pub item_fn: ItemFn, } @@ -24,8 +20,14 @@ impl ToTokens for ComponentBody { fn to_tokens(&self, tokens: &mut TokenStream) { let comp_fn = self.comp_fn(); + // If there's no props declared, we simply omit the props argument + // This is basically so you can annotate the App component with #[component] and still be compatible with the + // launch signatures that take fn() -> Element let props_struct = match self.item_fn.sig.inputs.is_empty() { + // No props declared, so we don't need to generate a props struct true => quote! {}, + + // Props declared, so we generate a props struct and thatn also attach the doc attributes to it false => { let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident); let props_struct = self.props_struct(); @@ -64,19 +66,22 @@ impl ComponentBody { .. } = sig; let Generics { where_clause, .. } = generics; + let (_, ty_generics, _) = generics.split_for_impl(); // 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_field_names = inputs.iter().filter_map(strip_mutability); - let (_, ty_generics, _) = generics.split_for_impl(); + // 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 props_docs = self.props_docs(inputs.iter().skip(1).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; }, @@ -92,10 +97,16 @@ impl ComponentBody { } } - // Build the props struct + /// Build an associated struct for the props of the component + /// + /// This will expand to the typed-builder implementation that we have vendored in this crate. + /// TODO: don't vendor typed-builder and instead transform the tokens we give it before expansion. + /// TODO: cache these tokens since this codegen is rather expensive (lots of tokens) + /// + /// We try our best to transfer over any declared doc attributes from the original function signature onto the + /// props struct fields. fn props_struct(&self) -> ItemStruct { - let ComponentBody { item_fn, .. } = &self; - let ItemFn { vis, sig, .. } = item_fn; + let ItemFn { vis, sig, .. } = &self.item_fn; let Signature { inputs, ident, @@ -103,17 +114,21 @@ impl ComponentBody { .. } = sig; - let struct_fields = inputs.iter().map(move |f| make_prop_struct_fields(f, vis)); + let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis)); let struct_ident = Ident::new(&format!("{ident}Props"), ident.span()); parse_quote! { #[derive(Props, Clone, PartialEq)] #[allow(non_camel_case_types)] - #vis struct #struct_ident #generics - { #(#struct_fields),* } + #vis struct #struct_ident #generics { + #(#struct_fields),* + } } } + /// Convert a list of function arguments into a list of doc attributes for the props struct + /// + /// This lets us generate set of attributes that we can apply to the props struct to give it a nice docstring. fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec { let fn_ident = &self.item_fn.sig.ident; @@ -123,58 +138,7 @@ impl ComponentBody { let arg_docs = inputs .iter() - .filter_map(|f| match f { - FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. - FnArg::Typed(pt) => { - let arg_doc = pt - .attrs - .iter() - .filter_map(|attr| { - // TODO: Error reporting - // Check if the path of the attribute is "doc" - if !is_attr_doc(attr) { - return None; - }; - - let Meta::NameValue(meta_name_value) = &attr.meta else { - return None; - }; - - let Expr::Lit(doc_lit) = &meta_name_value.value else { - return None; - }; - - let Lit::Str(doc_lit_str) = &doc_lit.lit else { - return None; - }; - - Some(doc_lit_str.value()) - }) - .fold(String::new(), |mut doc, next_doc_line| { - doc.push('\n'); - doc.push_str(&next_doc_line); - doc - }); - - Some(( - &pt.pat, - &pt.ty, - pt.attrs.iter().find_map(|attr| { - if attr.path() != &parse_quote!(deprecated) { - return None; - } - - let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); - - match res { - Err(e) => panic!("{}", e.to_string()), - Ok(v) => Some(v), - } - }), - arg_doc, - )) - } - }) + .filter_map(|f| build_doc_fields(f)) .collect::>(); let mut props_docs = Vec::with_capacity(5); @@ -186,7 +150,14 @@ impl ComponentBody { #[doc = #header] }); - for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs { + for arg in arg_docs { + let DocField { + arg_name, + arg_type, + deprecation, + input_arg_doc, + } = arg; + let arg_name = arg_name.into_token_stream().to_string(); let arg_type = crate::utils::format_type_string(arg_type); @@ -220,91 +191,70 @@ impl ComponentBody { arg_doc.push_str(&format!("

{input_arg_doc}

")); } - props_docs.push(parse_quote! { - #[doc = #arg_doc] - }); + props_docs.push(parse_quote! { #[doc = #arg_doc] }); } props_docs } } -fn make_prop_struct_fields(f: &FnArg, vis: &Visibility) -> TokenStream { - match f { - FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing - FnArg::Typed(pt) => { - let arg_pat = match pt.pat.as_ref() { - // rip off mutability - Pat::Ident(f) => { - let mut f = f.clone(); - f.mutability = None; - quote! { #f } - } - a => quote! { #a }, +struct DocField<'a> { + arg_name: &'a Box, + arg_type: &'a Box, + deprecation: Option, + input_arg_doc: String, +} + +fn build_doc_fields(f: &FnArg) -> Option { + let FnArg::Typed(pt) = f else { unreachable!() }; + + let arg_doc = pt + .attrs + .iter() + .filter_map(|attr| { + // TODO: Error reporting + // Check if the path of the attribute is "doc" + if !is_attr_doc(attr) { + return None; }; - let arg_colon = &pt.colon_token; - let arg_ty = &pt.ty; // Type - let arg_attrs = &pt.attrs; // Attributes + let Meta::NameValue(meta_name_value) = &attr.meta else { + return None; + }; - quote! { - #(#arg_attrs) - * - #vis #arg_pat #arg_colon #arg_ty - } - } - } -} + let Expr::Lit(doc_lit) = &meta_name_value.value else { + return None; + }; -fn strip_mutability(f: &FnArg) -> Option { - match f { - FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. - FnArg::Typed(pt) => { - let pat = &pt.pat; + let Lit::Str(doc_lit_str) = &doc_lit.lit else { + return None; + }; - let mut pat = pat.clone(); + Some(doc_lit_str.value()) + }) + .fold(String::new(), |mut doc, next_doc_line| { + doc.push('\n'); + doc.push_str(&next_doc_line); + doc + }); - // rip off mutability, but still write it out eventually - if let Pat::Ident(ref mut pat_ident) = pat.as_mut() { - pat_ident.mutability = None; + Some(DocField { + arg_name: &pt.pat, + arg_type: &pt.ty, + deprecation: pt.attrs.iter().find_map(|attr| { + if attr.path() != &parse_quote!(deprecated) { + return None; } - Some(quote!(mut #pat)) - } - } -} + let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); -/// Checks if the attribute is a `#[doc]` attribute. -fn is_attr_doc(attr: &Attribute) -> bool { - attr.path() == &parse_quote!(doc) -} - -fn keep_up_to_n_consecutive_chars( - input: &str, - n_of_consecutive_chars_allowed: usize, - target_char: char, -) -> String { - let mut output = String::new(); - let mut prev_char: Option = None; - let mut consecutive_count = 0; - - for c in input.chars() { - match prev_char { - Some(prev) if c == target_char && prev == target_char => { - if consecutive_count < n_of_consecutive_chars_allowed { - output.push(c); - consecutive_count += 1; - } + match res { + Err(e) => panic!("{}", e.to_string()), + Ok(v) => Some(v), } - _ => { - output.push(c); - prev_char = Some(c); - consecutive_count = 1; - } - } - } - - output + }), + input_arg_doc: arg_doc, + }) } fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> { @@ -356,3 +306,82 @@ fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> { Ok(()) } + +/// Convert a function arg with a given visibility (provided by the function) and then generate a field for the +/// associated props struct. +fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream { + // There's no receivers (&self) allowed in the component body + let FnArg::Typed(pt) = f else { unreachable!() }; + + let arg_pat = match pt.pat.as_ref() { + // rip off mutability + // todo: we actually don't want any of the extra bits of the field pattern + Pat::Ident(f) => { + let mut f = f.clone(); + f.mutability = None; + quote! { #f } + } + a => quote! { #a }, + }; + + let PatType { + attrs, + ty, + colon_token, + .. + } = pt; + + quote! { + #(#attrs)* + #vis #arg_pat #colon_token #ty + } +} + +fn rebind_mutability(f: &FnArg) -> Option { + // There's no receivers (&self) allowed in the component body + let FnArg::Typed(pt) = f else { unreachable!() }; + + let pat = &pt.pat; + + let mut pat = pat.clone(); + + // rip off mutability, but still write it out eventually + if let Pat::Ident(ref mut pat_ident) = pat.as_mut() { + pat_ident.mutability = None; + } + + Some(quote!(mut #pat)) +} + +/// Checks if the attribute is a `#[doc]` attribute. +fn is_attr_doc(attr: &Attribute) -> bool { + attr.path() == &parse_quote!(doc) +} + +fn keep_up_to_n_consecutive_chars( + input: &str, + n_of_consecutive_chars_allowed: usize, + target_char: char, +) -> String { + let mut output = String::new(); + let mut prev_char: Option = None; + let mut consecutive_count = 0; + + for c in input.chars() { + match prev_char { + Some(prev) if c == target_char && prev == target_char => { + if consecutive_count < n_of_consecutive_chars_allowed { + output.push(c); + consecutive_count += 1; + } + } + _ => { + output.push(c); + prev_char = Some(c); + consecutive_count = 1; + } + } + } + + output +}