فهرست منبع

Heavily document component macro

Jonathan Kelley 1 سال پیش
والد
کامیت
494f7e727d
1فایلهای تغییر یافته به همراه164 افزوده شده و 135 حذف شده
  1. 164 135
      packages/core-macro/src/component.rs

+ 164 - 135
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<Attribute> {
         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::<Vec<_>>();
 
         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!("<p>{input_arg_doc}</p>"));
             }
 
-            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 },
-            };
-
-            let arg_colon = &pt.colon_token;
-            let arg_ty = &pt.ty; // Type
-            let arg_attrs = &pt.attrs; // Attributes
-
-            quote! {
-                #(#arg_attrs)
-                *
-                #vis #arg_pat #arg_colon #arg_ty
-            }
-        }
-    }
+struct DocField<'a> {
+    arg_name: &'a Box<Pat>,
+    arg_type: &'a Box<Type>,
+    deprecation: Option<crate::utils::DeprecatedAttribute>,
+    input_arg_doc: String,
 }
 
-fn strip_mutability(f: &FnArg) -> Option<TokenStream> {
-    match f {
-        FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
-        FnArg::Typed(pt) => {
-            let pat = &pt.pat;
+fn build_doc_fields(f: &FnArg) -> Option<DocField> {
+    let FnArg::Typed(pt) = f else { unreachable!() };
 
-            let mut pat = pat.clone();
+    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;
+            };
 
-            // rip off mutability, but still write it out eventually
-            if let Pat::Ident(ref mut pat_ident) = pat.as_mut() {
-                pat_ident.mutability = None;
-            }
+            let Meta::NameValue(meta_name_value) = &attr.meta else {
+                return None;
+            };
 
-            Some(quote!(mut  #pat))
-        }
-    }
-}
+            let Expr::Lit(doc_lit) = &meta_name_value.value else {
+                return None;
+            };
 
-/// Checks if the attribute is a `#[doc]` attribute.
-fn is_attr_doc(attr: &Attribute) -> bool {
-    attr.path() == &parse_quote!(doc)
-}
+            let Lit::Str(doc_lit_str) = &doc_lit.lit else {
+                return None;
+            };
 
-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<char> = None;
-    let mut consecutive_count = 0;
+            Some(doc_lit_str.value())
+        })
+        .fold(String::new(), |mut doc, next_doc_line| {
+            doc.push('\n');
+            doc.push_str(&next_doc_line);
+            doc
+        });
 
-    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;
+    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;
             }
-        }
-    }
 
-    output
+            let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
+
+            match res {
+                Err(e) => panic!("{}", e.to_string()),
+                Ok(v) => Some(v),
+            }
+        }),
+        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<TokenStream> {
+    // 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<char> = 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
+}