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`
This commit is contained in:
Leonard authored and GitHub committed 2023-10-22 09:28:17 -05:00
1 parent e41278af45
commit 74952179db
1 file changed
+138 -6
@@ -30,14 +30,13 @@ impl ToTokens for InlinePropsDeserializerOutput {
impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs { impl DeserializerArgs<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> { fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
Ok(InlinePropsDeserializerOutput { Ok(InlinePropsDeserializerOutput {
comp_fn: Self::get_function(component_body), comp_fn: get_function(component_body),
props_struct: Self::get_props_struct(component_body), props_struct: get_props_struct(component_body),
}) })
} }
} }
impl InlinePropsDeserializerArgs { fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
fn get_props_struct(component_body: &ComponentBody) -> ItemStruct {
let ComponentBody { item_fn, .. } = component_body; let ComponentBody { item_fn, .. } = component_body;
let ItemFn { vis, sig, .. } = item_fn; let ItemFn { vis, sig, .. } = item_fn;
let Signature { let Signature {
@@ -111,9 +110,111 @@ impl InlinePropsDeserializerArgs {
#(#struct_fields),* #(#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 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::<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 { let ComponentBody {
item_fn, item_fn,
cx_pat_type, cx_pat_type,
@@ -141,7 +242,7 @@ impl InlinePropsDeserializerArgs {
// Skip first arg since that's the context // Skip first arg since that's the context
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f { let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
FnArg::Typed(t) => Some(&t.pat), FnArg::Typed(pt) => Some(&pt.pat),
}); });
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() { let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
@@ -182,8 +283,11 @@ impl InlinePropsDeserializerArgs {
generics generics
}; };
let props_docs = get_props_docs(fn_ident, inputs.iter().skip(1).collect());
parse_quote! { parse_quote! {
#(#fn_attrs)* #(#fn_attrs)*
#(#props_docs)*
#asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output #asyncness #vis fn #fn_ident #fn_generics (#cx_pat: Scope<#scope_lifetime #struct_ident #generics_no_bounds>) #fn_output
#where_clause #where_clause
{ {
@@ -191,5 +295,33 @@ impl InlinePropsDeserializerArgs {
#fn_block #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
} }