668 lines
21 KiB
Rust
668 lines
21 KiB
Rust
use proc_macro2::TokenStream;
|
|
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
|
use syn::parse::{Parse, ParseStream};
|
|
use syn::punctuated::Punctuated;
|
|
use syn::spanned::Spanned;
|
|
use syn::*;
|
|
|
|
pub struct ComponentBody {
|
|
pub item_fn: ItemFn,
|
|
pub options: ComponentMacroOptions,
|
|
}
|
|
|
|
impl Parse for ComponentBody {
|
|
fn parse(input: ParseStream) -> Result<Self> {
|
|
let item_fn: ItemFn = input.parse()?;
|
|
validate_component_fn(&item_fn)?;
|
|
Ok(Self {
|
|
item_fn,
|
|
options: ComponentMacroOptions::default(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ComponentBody {
|
|
pub fn with_options(mut self, options: ComponentMacroOptions) -> Self {
|
|
self.options = options;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ToTokens for ComponentBody {
|
|
fn to_tokens(&self, tokens: &mut TokenStream) {
|
|
// https://github.com/DioxusLabs/dioxus/issues/1938
|
|
// If there's only one input and the input is `props: Props`, we don't need to generate a props struct
|
|
// Just attach the non_snake_case attribute to the function
|
|
// eventually we'll dump this metadata into devtooling that lets us find all these components
|
|
//
|
|
// Components can also use the struct pattern to "inline" their props.
|
|
// Freya uses this a bunch (because it's clean),
|
|
// e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed
|
|
if self.is_explicit_props_ident() || self.has_struct_parameter_pattern() {
|
|
let comp_fn = &self.item_fn;
|
|
tokens.append_all(allow_camel_case_for_fn_ident(comp_fn).into_token_stream());
|
|
return;
|
|
}
|
|
|
|
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 then also attach the doc attributes to it
|
|
false => {
|
|
let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
|
|
let (props_struct, props_impls) = self.props_struct();
|
|
quote! {
|
|
#[doc = #doc]
|
|
#[allow(missing_docs)]
|
|
#props_struct
|
|
|
|
#(#props_impls)*
|
|
}
|
|
}
|
|
};
|
|
|
|
let completion_hints = self.completion_hints();
|
|
|
|
tokens.append_all(quote! {
|
|
#props_struct
|
|
#comp_fn
|
|
|
|
#completion_hints
|
|
});
|
|
}
|
|
}
|
|
|
|
impl ComponentBody {
|
|
// build a new item fn, transforming the original item fn
|
|
fn comp_fn(&self) -> ItemFn {
|
|
let ComponentBody { item_fn, .. } = self;
|
|
let ItemFn {
|
|
attrs,
|
|
vis,
|
|
sig,
|
|
block,
|
|
} = item_fn;
|
|
let Signature {
|
|
inputs,
|
|
ident: fn_ident,
|
|
generics,
|
|
output: fn_output,
|
|
..
|
|
} = sig;
|
|
|
|
let Generics { where_clause, .. } = generics;
|
|
let (_, impl_generics, _) = generics.split_for_impl();
|
|
let generics_turbofish = impl_generics.as_turbofish();
|
|
|
|
// 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());
|
|
|
|
// We pull in the field names from the original function signature, but need to strip off the mutability
|
|
let struct_field_names = inputs.iter().map(rebind_mutability);
|
|
|
|
let props_docs = self.props_docs(inputs.iter().collect());
|
|
|
|
let inlined_props_argument = if inputs.is_empty() {
|
|
quote! {}
|
|
} else {
|
|
quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics }
|
|
};
|
|
|
|
// Defer to the lazy_body if we're using lazy
|
|
let body: TokenStream = if self.options.lazy {
|
|
self.lazy_body(
|
|
&struct_ident,
|
|
generics,
|
|
&impl_generics,
|
|
fn_output,
|
|
where_clause,
|
|
&inlined_props_argument,
|
|
block,
|
|
)
|
|
} else {
|
|
quote! { #block }
|
|
};
|
|
|
|
// We need a props type to exist even if the inputs are empty with lazy components
|
|
let emit_props = if self.options.lazy {
|
|
if inputs.is_empty() {
|
|
quote! {props: ()}
|
|
} else {
|
|
quote!(props: #struct_ident #impl_generics)
|
|
}
|
|
} else {
|
|
inlined_props_argument
|
|
};
|
|
|
|
// The extra nest is for the snake case warning to kick back in
|
|
parse_quote! {
|
|
#(#attrs)*
|
|
#(#props_docs)*
|
|
#[allow(non_snake_case)]
|
|
#vis fn #fn_ident #generics (#emit_props) #fn_output #where_clause {
|
|
{
|
|
// In debug mode we can detect if the user is calling the component like a function
|
|
dioxus_core::internal::verify_component_called_as_component(#fn_ident #generics_turbofish);
|
|
#body
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate the body of the lazy component
|
|
///
|
|
/// This extracts the body into a new component that is wrapped in a lazy loader
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn lazy_body(
|
|
&self,
|
|
struct_ident: &Ident,
|
|
generics: &Generics,
|
|
impl_generics: &TypeGenerics,
|
|
fn_output: &ReturnType,
|
|
where_clause: &Option<WhereClause>,
|
|
inlined_props_argument: &TokenStream,
|
|
block: &Block,
|
|
) -> TokenStream {
|
|
let fn_ident = &self.item_fn.sig.ident;
|
|
let inputs = &self.item_fn.sig.inputs;
|
|
|
|
let lazy_name = format_ident!("Lazy{fn_ident}");
|
|
let out_ty = match &self.item_fn.sig.output {
|
|
ReturnType::Default => quote! { () },
|
|
ReturnType::Type(_, ty) => quote! { #ty },
|
|
};
|
|
let props_ty = if inputs.is_empty() {
|
|
quote! { () }
|
|
} else {
|
|
quote! { #struct_ident #impl_generics }
|
|
};
|
|
let anon_props = if inputs.is_empty() {
|
|
quote! { props: () }
|
|
} else {
|
|
quote! { #inlined_props_argument}
|
|
};
|
|
|
|
quote! {
|
|
fn #lazy_name #generics (#anon_props) #fn_output #where_clause {
|
|
#block
|
|
}
|
|
|
|
dioxus::config_macros::maybe_wasm_split! {
|
|
if wasm_split {
|
|
{
|
|
static __MODULE: wasm_split::LazyLoader<#props_ty, #out_ty> =
|
|
wasm_split::lazy_loader!(extern "lazy" fn #lazy_name(props: #props_ty,) -> #out_ty);
|
|
|
|
use_resource(|| async move { __MODULE.load().await }).suspend()?;
|
|
__MODULE.call(props).unwrap()
|
|
}
|
|
} else {
|
|
{
|
|
#lazy_name(props)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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, Vec<ItemImpl>) {
|
|
let ItemFn { vis, sig, .. } = &self.item_fn;
|
|
let Signature {
|
|
inputs,
|
|
ident,
|
|
generics,
|
|
..
|
|
} = sig;
|
|
|
|
let generic_arguments = if !generics.params.is_empty() {
|
|
let generic_arguments = generics
|
|
.params
|
|
.iter()
|
|
.map(make_prop_struct_generics)
|
|
.collect::<Punctuated<_, Token![,]>>();
|
|
quote! { <#generic_arguments> }
|
|
} else {
|
|
quote! {}
|
|
};
|
|
let where_clause = &generics.where_clause;
|
|
let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(f, vis));
|
|
let struct_field_idents = inputs
|
|
.iter()
|
|
.map(make_prop_struct_field_idents)
|
|
.collect::<Vec<_>>();
|
|
let struct_ident = Ident::new(&format!("{ident}Props"), ident.span());
|
|
|
|
let item_struct = parse_quote! {
|
|
#[derive(Props)]
|
|
#[allow(non_camel_case_types)]
|
|
#vis struct #struct_ident #generics #where_clause {
|
|
#(#struct_fields),*
|
|
}
|
|
};
|
|
|
|
let item_impl_clone = parse_quote! {
|
|
impl #generics ::core::clone::Clone for #struct_ident #generic_arguments #where_clause {
|
|
#[inline]
|
|
fn clone(&self) -> Self {
|
|
Self {
|
|
#(#struct_field_idents: ::core::clone::Clone::clone(&self.#struct_field_idents)),*
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let item_impl_partial_eq = parse_quote! {
|
|
impl #generics ::core::cmp::PartialEq for #struct_ident #generic_arguments #where_clause {
|
|
#[inline]
|
|
fn eq(&self, other: &Self) -> bool {
|
|
#(
|
|
self.#struct_field_idents == other.#struct_field_idents &&
|
|
)*
|
|
true
|
|
}
|
|
}
|
|
};
|
|
|
|
(item_struct, vec![item_impl_clone, item_impl_partial_eq])
|
|
}
|
|
|
|
/// 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;
|
|
|
|
if inputs.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
|
|
let arg_docs = inputs
|
|
.iter()
|
|
.filter_map(|f| build_doc_fields(f))
|
|
.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 in arg_docs {
|
|
let DocField {
|
|
arg_name,
|
|
arg_type,
|
|
deprecation,
|
|
input_arg_doc,
|
|
} = arg;
|
|
|
|
let arg_name = strip_pat_mutability(arg_name).to_token_stream().to_string();
|
|
let arg_type = crate::utils::format_type_string(arg_type);
|
|
|
|
let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n')
|
|
.replace("\n\n", "</p><p>");
|
|
let prop_def_link = format!("{props_def_link}::{arg_name}");
|
|
let mut arg_doc = format!("- [`{arg_name}`]({prop_def_link}) : `{arg_type}`");
|
|
|
|
if let Some(deprecation) = deprecation {
|
|
arg_doc.push_str("<p>👎 Deprecated");
|
|
|
|
if let Some(since) = deprecation.since {
|
|
arg_doc.push_str(&format!(" since {since}"));
|
|
}
|
|
|
|
if let Some(note) = deprecation.note {
|
|
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\n').replace('\n', " ");
|
|
let note = keep_up_to_n_consecutive_chars(¬e, 1, '\t').replace('\t', " ");
|
|
|
|
arg_doc.push_str(&format!(": {note}"));
|
|
}
|
|
|
|
arg_doc.push_str("</p>");
|
|
|
|
if !input_arg_doc.is_empty() {
|
|
arg_doc.push_str("<hr/>");
|
|
}
|
|
}
|
|
|
|
if !input_arg_doc.is_empty() {
|
|
arg_doc.push_str(&format!("<p>{input_arg_doc}</p>"));
|
|
}
|
|
|
|
props_docs.push(parse_quote! { #[doc = #arg_doc] });
|
|
}
|
|
|
|
props_docs
|
|
}
|
|
|
|
fn is_explicit_props_ident(&self) -> bool {
|
|
if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
|
|
if let Pat::Ident(ident) = pat.as_ref() {
|
|
return ident.ident == "props";
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
fn has_struct_parameter_pattern(&self) -> bool {
|
|
if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() {
|
|
if matches!(pat.as_ref(), Pat::Struct(_)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// We generate an extra enum to help us autocomplete the braces after the component.
|
|
// This is a bit of a hack, but it's the only way to get the braces to autocomplete.
|
|
fn completion_hints(&self) -> TokenStream {
|
|
let comp_fn = &self.item_fn.sig.ident;
|
|
let completions_mod = Ident::new(&format!("{}_completions", comp_fn), comp_fn.span());
|
|
|
|
let vis = &self.item_fn.vis;
|
|
|
|
quote! {
|
|
#[allow(non_snake_case)]
|
|
#[doc(hidden)]
|
|
mod #completions_mod {
|
|
#[doc(hidden)]
|
|
#[allow(non_camel_case_types)]
|
|
/// This enum is generated to help autocomplete the braces after the component. It does nothing
|
|
pub enum Component {
|
|
#comp_fn {}
|
|
}
|
|
}
|
|
|
|
#[allow(unused)]
|
|
#vis use #completions_mod::Component::#comp_fn;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DocField<'a> {
|
|
arg_name: &'a Pat,
|
|
arg_type: &'a Type,
|
|
deprecation: Option<crate::utils::DeprecatedAttribute>,
|
|
input_arg_doc: String,
|
|
}
|
|
|
|
fn build_doc_fields(f: &FnArg) -> Option<DocField> {
|
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
|
|
|
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(DocField {
|
|
arg_name: &pt.pat,
|
|
arg_type: &pt.ty,
|
|
deprecation: pt.attrs.iter().find_map(|attr| {
|
|
if !attr.path().is_ident("deprecated") {
|
|
return None;
|
|
}
|
|
|
|
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(item_fn: &ItemFn) -> Result<()> {
|
|
// Do some validation....
|
|
// 1. Ensure the component returns *something*
|
|
if item_fn.sig.output == ReturnType::Default {
|
|
return Err(Error::new(
|
|
item_fn.sig.output.span(),
|
|
"Must return a <dioxus_core::Element>".to_string(),
|
|
));
|
|
}
|
|
|
|
// 2. make sure there's no lifetimes on the component - we don't know how to handle those
|
|
if item_fn.sig.generics.lifetimes().count() > 0 {
|
|
return Err(Error::new(
|
|
item_fn.sig.generics.span(),
|
|
"Lifetimes are not supported in components".to_string(),
|
|
));
|
|
}
|
|
|
|
// 3. we can't handle async components
|
|
if item_fn.sig.asyncness.is_some() {
|
|
return Err(Error::new(
|
|
item_fn.sig.asyncness.span(),
|
|
"Async components are not supported".to_string(),
|
|
));
|
|
}
|
|
|
|
// 4. we can't handle const components
|
|
if item_fn.sig.constness.is_some() {
|
|
return Err(Error::new(
|
|
item_fn.sig.constness.span(),
|
|
"Const components are not supported".to_string(),
|
|
));
|
|
}
|
|
|
|
// 5. no receiver parameters
|
|
if item_fn
|
|
.sig
|
|
.inputs
|
|
.iter()
|
|
.any(|f| matches!(f, FnArg::Receiver(_)))
|
|
{
|
|
return Err(Error::new(
|
|
item_fn.sig.inputs.span(),
|
|
"Receiver parameters are not supported".to_string(),
|
|
));
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
/// Get ident from a function arg
|
|
fn make_prop_struct_field_idents(f: &FnArg) -> &Ident {
|
|
// There's no receivers (&self) allowed in the component body
|
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
|
|
|
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) => &f.ident,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
fn make_prop_struct_generics(generics: &GenericParam) -> TokenStream {
|
|
match generics {
|
|
GenericParam::Type(ty) => {
|
|
let ident = &ty.ident;
|
|
quote! { #ident }
|
|
}
|
|
GenericParam::Lifetime(lifetime) => {
|
|
let lifetime = &lifetime.lifetime;
|
|
quote! { #lifetime }
|
|
}
|
|
GenericParam::Const(c) => {
|
|
let ident = &c.ident;
|
|
quote! { #ident }
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rebind_mutability(f: &FnArg) -> TokenStream {
|
|
// There's no receivers (&self) allowed in the component body
|
|
let FnArg::Typed(pt) = f else { unreachable!() };
|
|
|
|
let immutable = strip_pat_mutability(&pt.pat);
|
|
|
|
quote!(mut #immutable)
|
|
}
|
|
|
|
fn strip_pat_mutability(pat: &Pat) -> Pat {
|
|
let mut pat = pat.clone();
|
|
// rip off mutability, but still write it out eventually
|
|
if let Pat::Ident(ref mut pat_ident) = &mut pat {
|
|
pat_ident.mutability = None;
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
/// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is allowed by the compiler.
|
|
fn allow_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn {
|
|
let mut clone = item_fn.clone();
|
|
let block = &item_fn.block;
|
|
|
|
clone.attrs.push(parse_quote! { #[allow(non_snake_case)] });
|
|
|
|
clone.block = parse_quote! {
|
|
{
|
|
#block
|
|
}
|
|
};
|
|
|
|
clone
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct ComponentMacroOptions {
|
|
pub lazy: bool,
|
|
}
|
|
|
|
impl Parse for ComponentMacroOptions {
|
|
fn parse(input: ParseStream) -> Result<Self> {
|
|
let mut lazy_load = false;
|
|
|
|
while !input.is_empty() {
|
|
let ident = input.parse::<Ident>()?;
|
|
let ident_name = ident.to_string();
|
|
if ident_name == "lazy" {
|
|
lazy_load = true;
|
|
} else if ident_name == "no_case_check" {
|
|
// we used to have this?
|
|
} else {
|
|
return Err(Error::new(
|
|
ident.span(),
|
|
"Unknown option for component macro",
|
|
));
|
|
}
|
|
|
|
if input.peek(Token![,]) {
|
|
input.parse::<Token![,]>()?;
|
|
}
|
|
}
|
|
|
|
Ok(Self { lazy: lazy_load })
|
|
}
|
|
}
|