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:
1 parent
e41278af45
commit
74952179db
1 file changed
+276
-144
@@ -30,166 +30,298 @@ 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 {
|
inputs,
|
||||||
inputs,
|
ident: fn_ident,
|
||||||
ident: fn_ident,
|
generics,
|
||||||
generics,
|
..
|
||||||
..
|
} = sig;
|
||||||
} = sig;
|
|
||||||
|
|
||||||
// Skip first arg since that's the context
|
// Skip first arg since that's the context
|
||||||
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
let struct_fields = inputs.iter().skip(1).map(move |f| {
|
||||||
match f {
|
match f {
|
||||||
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
|
||||||
FnArg::Typed(pt) => {
|
FnArg::Typed(pt) => {
|
||||||
let arg_pat = &pt.pat; // Pattern (identifier)
|
let arg_pat = &pt.pat; // Pattern (identifier)
|
||||||
let arg_colon = &pt.colon_token;
|
let arg_colon = &pt.colon_token;
|
||||||
let arg_ty = &pt.ty; // Type
|
let arg_ty = &pt.ty; // Type
|
||||||
let arg_attrs = &pt.attrs; // Attributes
|
let arg_attrs = &pt.attrs; // Attributes
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#(#arg_attrs)
|
#(#arg_attrs)
|
||||||
*
|
*
|
||||||
#vis #arg_pat #arg_colon #arg_ty
|
#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),*
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
fn get_function(component_body: &ComponentBody) -> ItemFn {
|
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
||||||
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 first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
||||||
let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span());
|
Some(lt)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Skip first arg since that's the context
|
let struct_attrs = if first_lifetime.is_some() {
|
||||||
let struct_field_names = inputs.iter().skip(1).filter_map(|f| match f {
|
quote! { #[derive(Props)] }
|
||||||
FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
|
} else {
|
||||||
FnArg::Typed(t) => Some(&t.pat),
|
quote! { #[derive(Props, PartialEq)] }
|
||||||
});
|
};
|
||||||
|
|
||||||
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
|
let struct_generics = if first_lifetime.is_some() {
|
||||||
Some(lt)
|
let struct_generics: Punctuated<GenericParam, Comma> = component_body
|
||||||
} else {
|
.item_fn
|
||||||
None
|
.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 {
|
GenericParam::Type(tp)
|
||||||
(quote! { #lt, }, generics.clone())
|
}
|
||||||
} else {
|
_ => it.clone(),
|
||||||
let lifetime: LifetimeParam = parse_quote! { 'a };
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut fn_generics = generics.clone();
|
quote! { <#struct_generics> }
|
||||||
fn_generics
|
} else {
|
||||||
.params
|
quote! { #generics }
|
||||||
.insert(0, GenericParam::Lifetime(lifetime.clone()));
|
};
|
||||||
|
|
||||||
(quote! { #lifetime, }, fn_generics)
|
parse_quote! {
|
||||||
};
|
#struct_attrs
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
let generics_no_bounds = {
|
#vis struct #struct_ident #struct_generics
|
||||||
let mut generics = generics.clone();
|
{
|
||||||
generics.params = generics
|
#(#struct_fields),*
|
||||||
.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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_props_docs(fn_ident: &Ident, inputs: Vec<&FnArg>) -> Vec<Attribute> {
|
||||||
|
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::<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("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!("<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 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
|
||||||
|
}
|
||||||
Reference in new issue
Block a user