1
0
Эх сурвалжийг харах

Append prop documentation to function when using `#[inline_props]` or `#[component]` (#1563)

* Do the stuff

* Add deprecated warning and attribute list

Also move out methods that didn't take self as a parameter from `InlinePropsDeserializer`
Leonard 1 жил өмнө
parent
commit
74952179db

+ 278 - 146
packages/core-macro/src/component_body_deserializers/inline_props.rs

@@ -30,166 +30,298 @@ impl ToTokens for InlinePropsDeserializerOutput {
 impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
     fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
         Ok(InlinePropsDeserializerOutput {
-            comp_fn: Self::get_function(component_body),
-            props_struct: Self::get_props_struct(component_body),
+            comp_fn: get_function(component_body),
+            props_struct: get_props_struct(component_body),
         })
     }
 }
 
-impl InlinePropsDeserializerArgs {
-    fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
-        let ComponentBody { item_fn, .. } = component_body;
-        let ItemFn { vis, sig, .. } = item_fn;
-        let Signature {
-            inputs,
-            ident: fn_ident,
-            generics,
-            ..
-        } = sig;
-
-        // Skip first arg since that's the context
-        let struct_fields = inputs.iter().skip(1).map(move |f| {
-            match f {
-                FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
-                FnArg::Typed(pt) => {
-                    let arg_pat = &pt.pat; // Pattern (identifier)
-                    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
-                    }
+fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
+    let ComponentBody { item_fn, .. } = component_body;
+    let ItemFn { vis, sig, .. } = item_fn;
+    let Signature {
+        inputs,
+        ident: fn_ident,
+        generics,
+        ..
+    } = sig;
+
+    // Skip first arg since that's the context
+    let struct_fields = inputs.iter().skip(1).map(move |f| {
+        match f {
+            FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
+            FnArg::Typed(pt) => {
+                let arg_pat = &pt.pat; // Pattern (identifier)
+                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
                 }
             }
-        });
+        }
+    });
 
-        let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
-        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
-            Some(lt)
-        } else {
-            None
-        };
-
-        let struct_attrs = if first_lifetime.is_some() {
-            quote! { #[derive(Props)] }
-        } else {
-            quote! { #[derive(Props, PartialEq)] }
-        };
-
-        let struct_generics = if first_lifetime.is_some() {
-            let struct_generics: Punctuated<GenericParam, Comma> = component_body
-                .item_fn
-                .sig
-                .generics
-                .params
-                .iter()
-                .map(|it| match it {
-                    GenericParam::Type(tp) => {
-                        let mut tp = tp.clone();
-                        tp.bounds.push(parse_quote!( 'a ));
-
-                        GenericParam::Type(tp)
-                    }
-                    _ => it.clone(),
-                })
-                .collect();
-
-            quote! { <#struct_generics> }
-        } else {
-            quote! { #generics }
-        };
-
-        parse_quote! {
-            #struct_attrs
-            #[allow(non_camel_case_types)]
-            #vis struct #struct_ident #struct_generics
-            {
-                #(#struct_fields),*
-            }
+    let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+    let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+        Some(lt)
+    } else {
+        None
+    };
+
+    let struct_attrs = if first_lifetime.is_some() {
+        quote! { #[derive(Props)] }
+    } else {
+        quote! { #[derive(Props, PartialEq)] }
+    };
+
+    let struct_generics = if first_lifetime.is_some() {
+        let struct_generics: Punctuated<GenericParam, Comma> = component_body
+            .item_fn
+            .sig
+            .generics
+            .params
+            .iter()
+            .map(|it| match it {
+                GenericParam::Type(tp) => {
+                    let mut tp = tp.clone();
+                    tp.bounds.push(parse_quote!( 'a ));
+
+                    GenericParam::Type(tp)
+                }
+                _ => it.clone(),
+            })
+            .collect();
+
+        quote! { <#struct_generics> }
+    } else {
+        quote! { #generics }
+    };
+
+    parse_quote! {
+        #struct_attrs
+        #[allow(non_camel_case_types)]
+        #vis struct #struct_ident #struct_generics
+        {
+            #(#struct_fields),*
         }
     }
+}
+
+fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
+    if inputs.len() <= 1 {
+        return Vec::new();
+    }
 
-    fn get_function(component_body: &ComponentBody) -> ItemFn {
-        let ComponentBody {
-            item_fn,
-            cx_pat_type,
-            ..
-        } = component_body;
-        let ItemFn {
-            attrs: fn_attrs,
-            vis,
-            sig,
-            block: fn_block,
-        } = item_fn;
-        let Signature {
-            inputs,
-            ident: fn_ident,
-            generics,
-            output: fn_output,
-            asyncness,
-            ..
-        } = sig;
-        let Generics { where_clause, .. } = generics;
-
-        let cx_pat = &cx_pat_type.pat;
-        let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
-
-        // Skip first arg since that's the context
-        let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+    let arg_docs = inputs
+        .iter()
+        .filter_map(|f| match f {
             FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
-            FnArg::Typed(t) => Some(&t.pat),
+            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.to_token_stream().to_string(),
+                    pt.ty.to_token_stream().to_string(),
+                    pt.attrs.iter().filter(|a| !is_attr_doc(a)),
+                    arg_doc,
+                ))
+            }
+        })
+        .collect::<Vec<_>>();
+
+    let mut props_docs = Vec::with_capacity(5);
+    let props_def_link = fn_ident.to_string() + "Props";
+    let header =
+        format!("# Props\n*For details, see the [props struct definition]({props_def_link}).*");
+
+    props_docs.push(parse_quote! {
+        #[doc = #header]
+    });
+
+    for (arg_name, arg_type, attrs, input_arg_doc) in arg_docs {
+        let input_arg_doc =
+            keep_up_to_two_consecutive_chars(input_arg_doc.trim_matches('\n'), '\n')
+                .replace('\n', "<br>");
+        let prop_def_link = format!("{props_def_link}::{arg_name}");
+        let mut attribute_list = String::new();
+        let mut is_deprecated = false;
+
+        for attr in attrs {
+            if attr.path() == &parse_quote!(deprecated) {
+                is_deprecated = true;
+                continue;
+            }
+
+            attribute_list.push('`');
+            attribute_list.push_str(&quote!(#attr).into_token_stream().to_string());
+            attribute_list.push_str("`, ");
+        }
+
+        let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link})");
+
+        if is_deprecated {
+            arg_doc.push_str(" 👎 Deprecated");
+        }
+
+        arg_doc.push_str(&format!("<br>Type: `{arg_type}`"));
+
+        if !attribute_list.is_empty() {
+            // Truncate the last `, `
+            attribute_list.truncate(attribute_list.len() - 2);
+            arg_doc.push_str(&format!("<br>Attributes: {attribute_list}"));
+        }
+
+        arg_doc.push_str(&format!("<hr>{input_arg_doc}"));
+
+        props_docs.push(parse_quote! {
+            #[doc = #arg_doc]
         });
+    }
+
+    props_docs
+}
+
+fn get_function(component_body: &ComponentBody) -> ItemFn {
+    let ComponentBody {
+        item_fn,
+        cx_pat_type,
+        ..
+    } = component_body;
+    let ItemFn {
+        attrs: fn_attrs,
+        vis,
+        sig,
+        block: fn_block,
+    } = item_fn;
+    let Signature {
+        inputs,
+        ident: fn_ident,
+        generics,
+        output: fn_output,
+        asyncness,
+        ..
+    } = sig;
+    let Generics { where_clause, .. } = generics;
+
+    let cx_pat = &cx_pat_type.pat;
+    let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
+
+    // Skip first arg since that's the context
+    let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
+        FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
+        FnArg::Typed(pt) => Some(&pt.pat),
+    });
+
+    let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
+        Some(lt)
+    } else {
+        None
+    };
+
+    let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
+        (quote! { #lt, }, generics.clone())
+    } else {
+        let lifetime: LifetimeParam = parse_quote! { 'a };
+
+        let mut fn_generics = generics.clone();
+        fn_generics
+            .params
+            .insert(0, GenericParam::Lifetime(lifetime.clone()));
+
+        (quote! { #lifetime, }, fn_generics)
+    };
 
-        let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
-            Some(lt)
-        } else {
-            None
-        };
-
-        let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime {
-            (quote! { #lt, }, generics.clone())
-        } else {
-            let lifetime: LifetimeParam = parse_quote! { 'a };
-
-            let mut fn_generics = generics.clone();
-            fn_generics
-                .params
-                .insert(0, GenericParam::Lifetime(lifetime.clone()));
-
-            (quote! { #lifetime, }, fn_generics)
-        };
-
-        let generics_no_bounds = {
-            let mut generics = generics.clone();
-            generics.params = generics
-                .params
-                .iter()
-                .map(|it| match it {
-                    GenericParam::Type(tp) => {
-                        let mut tp = tp.clone();
-                        tp.bounds.clear();
-
-                        GenericParam::Type(tp)
-                    }
-                    _ => it.clone(),
-                })
-                .collect();
-
-            generics
-        };
-
-        parse_quote! {
-            #(#fn_attrs)*
-            #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
-            #where_clause
-            {
-                let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
-                #fn_block
+    let generics_no_bounds = {
+        let mut generics = generics.clone();
+        generics.params = generics
+            .params
+            .iter()
+            .map(|it| match it {
+                GenericParam::Type(tp) => {
+                    let mut tp = tp.clone();
+                    tp.bounds.clear();
+
+                    GenericParam::Type(tp)
+                }
+                _ => it.clone(),
+            })
+            .collect();
+
+        generics
+    };
+
+    let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
+
+    parse_quote! {
+        #(#fn_attrs)*
+        #(#props_docs)*
+        #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
+        #where_clause
+        {
+            let #struct_ident { #(#struct_field_names),* } = &#cx_pat.props;
+            #fn_block
+        }
+    }
+}
+
+/// Checks if the attribute is a `#[doc]` attribute.
+fn is_attr_doc(attr: &Attribute) -> bool {
+    attr.path() == &parse_quote!(doc)
+}
+
+fn keep_up_to_two_consecutive_chars(input: &str, 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 < 2 {
+                    output.push(c);
+                    consecutive_count += 1;
+                }
+            }
+            _ => {
+                output.push(c);
+                prev_char = Some(c);
+                consecutive_count = 1;
             }
         }
     }
+
+    output
 }