123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667 |
- 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 })
- }
- }
|