From 74952179db25e37ae6f9957bc7109ee276889d31 Mon Sep 17 00:00:00 2001 From: Leonard Date: Sun, 22 Oct 2023 16:28:17 +0200 Subject: [PATCH] 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` --- .../inline_props.rs | 420 ++++++++++++------ 1 file changed, 276 insertions(+), 144 deletions(-) diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs index 8e4581c19..a6e0a07d2 100644 --- a/packages/core-macro/src/component_body_deserializers/inline_props.rs +++ b/packages/core-macro/src/component_body_deserializers/inline_props.rs @@ -30,166 +30,298 @@ impl ToTokens for InlinePropsDeserializerOutput { impl DeserializerArgs for InlinePropsDeserializerArgs { fn to_output(&self, component_body: &ComponentBody) -> Result { 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; +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 + // 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 - } + 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 = 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_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 struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span()); - let cx_pat = &cx_pat_type.pat; - 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 + }; - // 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(t) => Some(&t.pat), - }); + let struct_attrs = if first_lifetime.is_some() { + quote! { #[derive(Props)] } + } else { + quote! { #[derive(Props, PartialEq)] } + }; - let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { - Some(lt) - } else { - None - }; + let struct_generics = if first_lifetime.is_some() { + let struct_generics: Punctuated = 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 )); - let (scope_lifetime, fn_generics) = if let Some(lt) = first_lifetime { - (quote! { #lt, }, generics.clone()) - } else { - let lifetime: LifetimeParam = parse_quote! { 'a }; + GenericParam::Type(tp) + } + _ => it.clone(), + }) + .collect(); - let mut fn_generics = generics.clone(); - fn_generics - .params - .insert(0, GenericParam::Lifetime(lifetime.clone())); + quote! { <#struct_generics> } + } else { + quote! { #generics } + }; - (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 - } + 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 { + if inputs.len() <= 1 { + return Vec::new(); + } + + 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.to_token_stream().to_string(), + pt.ty.to_token_stream().to_string(), + pt.attrs.iter().filter(|a| !is_attr_doc(a)), + arg_doc, + )) + } + }) + .collect::>(); + + 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', "
"); + 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("e!(#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!("
Type: `{arg_type}`")); + + if !attribute_list.is_empty() { + // Truncate the last `, ` + attribute_list.truncate(attribute_list.len() - 2); + arg_doc.push_str(&format!("
Attributes: {attribute_list}")); + } + + arg_doc.push_str(&format!("
{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 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 = 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 +}