diff --git a/packages/core-macro/.vscode/settings.json b/packages/core-macro/.vscode/settings.json new file mode 100644 index 000000000..7149654fd --- /dev/null +++ b/packages/core-macro/.vscode/settings.json @@ -0,0 +1,20 @@ +{ + "rust-analyzer.check.workspace": false, + // "rust-analyzer.check.command": "check alsjdlaskdjljkasd", + "rust-analyzer.check.allTargets": false, + "rust-analyzer.cargo.buildScripts.enable": true, + "rust-analyzer.cargo.buildScripts.overrideCommand": [ + "cargo", + "check", + "--quiet", + // "--package", + // "dioxus-core-macro", + "--message-format", + "json", + "--all-targets" + ], + "rust-analyzer.cargo.buildScripts.rebuildOnSave": false, + "rust-analyzer.cargo.buildScripts.invocationLocation": "root", + "rust-analyzer.cargo.buildScripts.invocationStrategy": "once", + // "rust-analyzer.check.command": "check --package dioxus-core-macro" +} diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs new file mode 100644 index 000000000..d05a12d06 --- /dev/null +++ b/packages/core-macro/src/component.rs @@ -0,0 +1,358 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::*; + +/// General struct for parsing a component body. +/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens). +/// +/// Refer to the [module documentation](crate::component_body) for more. +pub struct ComponentBody { + pub item_fn: ItemFn, +} + +impl Parse for ComponentBody { + fn parse(input: ParseStream) -> Result { + let item_fn: ItemFn = input.parse()?; + validate_component_fn_signature(&item_fn)?; + Ok(Self { item_fn }) + } +} + +impl ToTokens for ComponentBody { + fn to_tokens(&self, tokens: &mut TokenStream) { + let comp_fn = self.comp_fn(); + + let props_struct = match self.item_fn.sig.inputs.is_empty() { + true => quote! {}, + false => { + let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident); + let props_struct = self.props_struct(); + quote! { + #[doc = #doc] + #props_struct + } + } + }; + + tokens.append_all(quote! { + #props_struct + + #[allow(non_snake_case)] + #comp_fn + }); + } +} + +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, + asyncness, + .. + } = sig; + let Generics { where_clause, .. } = generics; + + // 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()); + + let struct_field_names = inputs.iter().filter_map(strip_mutability); + let (_, ty_generics, _) = generics.split_for_impl(); + let props_docs = self.props_docs(inputs.iter().skip(1).collect()); + + let props_ident = match inputs.is_empty() { + true => quote! {}, + false => quote! { mut __props: #struct_ident #ty_generics }, + }; + + let expanded_struct = match inputs.is_empty() { + true => quote! {}, + false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; }, + }; + + parse_quote! { + #(#attrs)* + #(#props_docs)* + #asyncness #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause { + #expanded_struct + #block + } + } + } + + // Build the props struct + fn props_struct(&self) -> ItemStruct { + let ComponentBody { item_fn, .. } = &self; + let ItemFn { vis, sig, .. } = item_fn; + let Signature { + inputs, + ident, + generics, + .. + } = sig; + + let struct_fields = inputs.iter().map(move |f| make_prop_struct_fields(f, vis)); + let struct_ident = Ident::new(&format!("{ident}Props"), ident.span()); + + parse_quote! { + #[derive(Props, Clone, PartialEq)] + #[allow(non_camel_case_types)] + #vis struct #struct_ident #generics + { #(#struct_fields),* } + } + } + + fn props_docs(&self, inputs: Vec<&FnArg>) -> Vec { + let fn_ident = &self.item_fn.sig.ident; + + 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, + &pt.ty, + pt.attrs.iter().find_map(|attr| { + if attr.path() != &parse_quote!(deprecated) { + return None; + } + + let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); + + match res { + Err(e) => panic!("{}", e.to_string()), + Ok(v) => Some(v), + } + }), + 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, deprecation, input_arg_doc) in arg_docs { + let arg_name = arg_name.into_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", "

"); + 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("

👎 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("

"); + + if !input_arg_doc.is_empty() { + arg_doc.push_str("
"); + } + } + + if !input_arg_doc.is_empty() { + arg_doc.push_str(&format!("

{input_arg_doc}

")); + } + + props_docs.push(parse_quote! { + #[doc = #arg_doc] + }); + } + + props_docs + } +} + +fn make_prop_struct_fields(f: &FnArg, vis: &Visibility) -> TokenStream { + match f { + FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing + FnArg::Typed(pt) => { + let arg_pat = match pt.pat.as_ref() { + // rip off mutability + Pat::Ident(f) => { + let mut f = f.clone(); + f.mutability = None; + quote! { #f } + } + a => quote! { #a }, + }; + + 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 + } + } + } +} + +fn strip_mutability(f: &FnArg) -> Option { + match f { + FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. + FnArg::Typed(pt) => { + let pat = &pt.pat; + + let mut pat = pat.clone(); + + // rip off mutability, but still write it out eventually + if let Pat::Ident(ref mut pat_ident) = pat.as_mut() { + pat_ident.mutability = None; + } + + Some(quote!(mut #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 = 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 +} + +fn validate_component_fn_signature(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 ".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(()) +} diff --git a/packages/core-macro/src/component_body/mod.rs b/packages/core-macro/src/component_body/mod.rs deleted file mode 100644 index 2e1475657..000000000 --- a/packages/core-macro/src/component_body/mod.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! This module is used for parsing a component function into a struct that is subsequently -//! deserialized into something useful using deserializer arguments. -//! -//! Let's break that down with a term glossary and examples which show usage and implementing. -//! -//! # Glossary -//! * `component body` - The [`ComponentBody`] struct. It's used to parse a component function [`proc_macro::TokenStream`] -//! to a reusable struct that deserializers use to modify the token stream. -//! * `deserializer` - A struct that deserializes the [`ComponentBody`] into a [`DeserializerOutput`]. -//! It implements the [`DeserializerArgs`] trait, but as you can see, it's called "DeserializerArgs", -//! not "Deserializer". Why? -//! Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which -//! takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer. -//! * `deserializer output` - A struct that implements the [`DeserializerOutput`] trait. -//! This struct is what enables deserializers to use each other, since it contains the fields that -//! a deserializer needs to turn a token stream to a different token stream. -//! This means a deserializer can get the output of another deserializer, and use that output, -//! thereby using the functionality of a different deserializer. -//! This struct also implements [`ToTokens`], which means that this is the final stage of the whole process. -//! -//! # Examples -//! *Not all imports might be included.* -//! -//! ## Usage in a procedural macro attribute -//! ```rs,ignore -//! use proc_macro::TokenStream; -//! -//! // Some documentation. You can reuse this in your deserializer structs. -//! /// This attribute changes the name of a component function to whatever the first argument is. -//! #[proc_macro_attribute] -//! pub fn name_changer(args: TokenStream, input: TokenStream) -> TokenStream { -//! // Parse the component body. -//! let component_body = parse_macro_input!(input as ComponentBody); -//! -//! // Parse the first argument, which is going to be the components new name. -//! let new_name: String = match Punctuated::::parse_terminated.parse(args) { -//! Err(e) => return e.to_compile_error().into(), // Convert to a compile error and return -//! Ok(args) => { -//! // If the argument exists, then convert it to a string -//! if let Some(first) = args.first() { -//! first.to_token_stream().to_string() -//! } else { -//! // If the argument doesn't exist, return an error with the appropriate message. -//! // The "span" is the location of some code. -//! // The error occurred in the "args" token stream, so point the error there. -//! return Error::new(args.span(), "No new name provided").to_compile_error().into(); -//! } -//! } -//! }; -//! -//! let new_name = &*new_name; -//! -//! // Deserialize the component body to an output with the given args. -//! let output = component_body.deserialize(NameChangerDeserializerArgs { new_name }); -//! -//! // Error handling like before, except now you're ready to return the final value. -//! match output { -//! Err(e) => e.to_compile_error().into(), -//! Ok(output) => output.to_token_stream().into(), -//! } -//! } -//! ``` -//! ## Using the macro in Dioxus code: -//! ```rs -//! use your_proc_macro_library::name_changer; -//! use dioxus::prelude::*; -//! -//! #[name_changer(CoolName)] -//! pub fn LameName() -> Element { -//! rsx! { "I want a cool name!" } -//! } -//! -//! pub fn App() -> Element { -//! rsx! { CoolName {} } // Renders: "I want a cool name!" -//! } -//! ``` -//! ## Implementing a component body deserializer -//! ```rs -//! use syn::{Result, ItemFn, Signature, Ident}; -//! use quote::quote; -//! -//! // Create a list of arguments. -//! // If there was no args, just make it empty. The "args" struct is also the deserializer struct. -//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path. -//! // Although unfortunately, the link does not work -//! // Just make sure that your macro is well documented. -//! /// The args and deserializing implementation for the [`name_changer`] macro. -//! #[derive(Clone)] -//! pub struct NameChangerDeserializerArgs<'a> { -//! pub new_name: &'a str, -//! } -//! -//! // Create an output struct. -//! // The ItemFn represents a modified component function. -//! // To read what fields should be here, check out the `DeserializerOutput` struct docs. -//! // For the docs, you can basically copy paste this text and replace "name_changer" with your macro path. -//! // Just make sure that your macro is well documented. -//! /// The output fields and [`ToTokens`] implementation for the [`name_changer`] macro. -//! #[derive(Clone)] -//! pub struct NameChangerDeserializerOutput { -//! pub comp_fn: ItemFn, -//! } -//! -//! // Implement `ToTokens`, which is forced by `DeserializerOutput`. -//! // This will usually be very simple like this, even for complex deserializers. -//! // That's because of the way the `DeserializerOutput` is designed. -//! impl ToTokens for NameChangerDeserializerOutput { -//! fn to_tokens(&self, tokens: &mut TokenStream) { -//! let comp_fn = &self.comp_fn; -//! -//! tokens.append_all(quote! { -//! #comp_fn -//! }); -//! } -//! } -//! -//! impl DeserializerOutput for NameChangerDeserializerOutput {} -//! -//! // Implement `DeserializerArgs`. This is the core part of deserializers. -//! impl<'a> DeserializerArgs for NameChangerDeserializerArgs<'a> { -//! fn to_output(&self, component_body: &ComponentBody) -> Result { -//! let old_fn = &component_body.item_fn; -//! let old_sig = &old_fn.sig; -//! -//! // For more complex uses, you will probably use `quote::parse_quote!` in combination with -//! // creating the structs manually. -//! // However, create the structs manually if you can. -//! // It's more reliable, because you only modify a certain struct field -//! // and set the others to be the clone of the original component body. -//! // That ensures that no information will be accidentally removed. -//! let new_sig = Signature { -//! ident: Ident::new(self.new_name, old_sig.ident.span()), -//! ..old_sig.clone() -//! }; -//! let new_fn = ItemFn { -//! sig: new_sig, -//! ..old_fn.clone() -//! }; -//! -//! Ok(NameChangerDeserializerOutput { -//! comp_fn: new_fn -//! }) -//! } -//! ``` - -pub mod utils; - -pub use utils::DeserializerArgs; -pub use utils::DeserializerOutput; - -use syn::parse::{Parse, ParseStream}; -use syn::spanned::Spanned; -use syn::*; - -/// General struct for parsing a component body. -/// However, because it's ambiguous, it does not implement [`ToTokens`](quote::to_tokens::ToTokens). -/// -/// Refer to the [module documentation](crate::component_body) for more. -pub struct ComponentBody { - /// The component function definition. You can parse this back into a [`ComponentBody`]. - /// For example, you might modify it, parse it into a [`ComponentBody`], and deserialize that - /// using some deserializer. This is how deserializers use other deserializers. - /// - /// **`item_fn.sig.inputs` includes the context argument!** - /// Keep this in mind when creating deserializers, because you often might want to ignore it. - /// That might be annoying, but it would be bad design for this kind of struct to not be parsable from itself. - pub item_fn: ItemFn, - /// If the function has any arguments other than the context. - pub has_extra_args: bool, -} - -impl ComponentBody { - /// Deserializes the body into the [`TOutput`] with the specific [`TArgs`]. - /// Even if the args are empty, the [`TArg`] type still determines what [`TOutput`] will be generated. - pub fn deserialize(&self, args: TArgs) -> Result - where - TOutput: DeserializerOutput, - TArgs: DeserializerArgs, - { - args.to_output(self) - } -} - -impl Parse for ComponentBody { - fn parse(input: ParseStream) -> Result { - let item_fn: ItemFn = input.parse()?; - - let element_type_path = "dioxus_core::Element"; - - if item_fn.sig.output == ReturnType::Default { - return Err(Error::new( - item_fn.sig.output.span(), - format!("Must return a <{}>", element_type_path), - )); - } - - let has_extra_args = !item_fn.sig.inputs.is_empty(); - - Ok(Self { - item_fn, - has_extra_args, - }) - } -} diff --git a/packages/core-macro/src/component_body/utils.rs b/packages/core-macro/src/component_body/utils.rs deleted file mode 100644 index decf95419..000000000 --- a/packages/core-macro/src/component_body/utils.rs +++ /dev/null @@ -1,36 +0,0 @@ -use crate::component_body::ComponentBody; -use quote::ToTokens; - -/// The output produced by a deserializer. -/// -/// # For implementors -/// Struct field guidelines: -/// * Must be public, so that other deserializers can utilize them. -/// * Should usually be [`Item`]s that you then simply combine in a [`quote!`] -/// in the [`ComponentBodyDeserializer::output_to_token_stream2`] function. -/// * If an [`Item`] might not be included, wrap it in an [`Option`]. -/// * Must be at the component function "level"/"context". -/// For example, the [`InlinePropsDeserializer`](crate::component_body_deserializers::inline_props::InlinePropsDeserializer) -/// produces two [`Item`]s; the function but with arguments turned into props, and the props struct. -/// It does not return any [`Item`]s inside the struct or function. -pub trait DeserializerOutput: ToTokens {} - -impl DeserializerOutput for T {} - -/// The args passed to a [`ComponentBody`] when deserializing it. -/// -/// It's also the struct that does the deserializing. -/// It's called "DeserializerArgs", not "Deserializer". Why? -/// Because "args" makes more sense to the caller of [`ComponentBody::deserialize`], which -/// takes an [`DeserializerArgs`] argument. However, you can think of "DeserializerArgs" as the deserializer. -pub trait DeserializerArgs: Clone -where - TOutput: ToTokens, -{ - // There's a lot of Results out there... let's make sure that this is a syn::Result. - // Let's also make sure there's not a warning. - /// Creates a [`ToTokens`] struct from the `self` args and a [`ComponentBody`]. - /// The [`ComponentBody::deserialize`] provides a cleaner way of calling this function. - #[allow(unused_qualifications)] - fn to_output(&self, component_body: &ComponentBody) -> syn::Result; -} diff --git a/packages/core-macro/src/component_body_deserializers/component.rs b/packages/core-macro/src/component_body_deserializers/component.rs deleted file mode 100644 index b6be85c9b..000000000 --- a/packages/core-macro/src/component_body_deserializers/component.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::component_body::{ComponentBody, DeserializerArgs}; -use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs; -use constcat::concat; -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::*; - -pub(crate) const COMPONENT_ARG_CASE_CHECK_ERROR: &str = concat!( - "This component does not use PascalCase. \ -To ignore this check, pass the \"", - crate::COMPONENT_ARG_CASE_CHECK_OFF, - "\" argument, like so: #[component(", - crate::COMPONENT_ARG_CASE_CHECK_OFF, - ")]" -); - -const INNER_FN_NAME: &str = "__dx_inner_comp"; - -fn get_out_comp_fn(orig_comp_fn: &ItemFn) -> ItemFn { - let inner_comp_ident = Ident::new(INNER_FN_NAME, orig_comp_fn.sig.ident.span()); - - let inner_comp_fn = ItemFn { - sig: Signature { - ident: inner_comp_ident.clone(), - ..orig_comp_fn.sig.clone() - }, - ..orig_comp_fn.clone() - }; - - let props_ident = match orig_comp_fn.sig.inputs.is_empty() { - true => quote! {}, - false => quote! { __props }, - }; - - ItemFn { - block: parse_quote! { - { - #[warn(non_snake_case)] - #[allow(clippy::inline_always)] - #[inline(always)] - #inner_comp_fn - #inner_comp_ident(#props_ident) - } - }, - ..orig_comp_fn.clone() - } -} - -/// The args and deserializing implementation for the [`crate::component`] macro. -#[derive(Clone)] -pub struct ComponentDeserializerArgs { - pub case_check: bool, -} - -/// The output fields and [`ToTokens`] implementation for the [`crate::component`] macro. -#[derive(Clone)] -pub struct ComponentDeserializerOutput { - pub comp_fn: ItemFn, - pub props_struct: Option, -} - -impl ToTokens for ComponentDeserializerOutput { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let comp_fn = &self.comp_fn; - let props_struct = &self.props_struct; - let fn_ident = &comp_fn.sig.ident; - - let doc = format!("Properties for the [`{fn_ident}`] component."); - tokens.append_all(quote! { - #[doc = #doc] - #props_struct - #[allow(non_snake_case)] - #comp_fn - }); - } -} - -impl DeserializerArgs for ComponentDeserializerArgs { - fn to_output(&self, component_body: &ComponentBody) -> Result { - let Signature { ident, .. } = &component_body.item_fn.sig; - - if self.case_check && !is_pascal_case(&ident.to_string()) { - return Err(Error::new(ident.span(), COMPONENT_ARG_CASE_CHECK_ERROR)); - } - - if component_body.has_extra_args { - Self::deserialize_with_props(component_body) - } else { - Ok(Self::deserialize_no_props(component_body)) - } - } -} - -impl ComponentDeserializerArgs { - fn deserialize_no_props(component_body: &ComponentBody) -> ComponentDeserializerOutput { - let ComponentBody { item_fn, .. } = component_body; - - let comp_fn = get_out_comp_fn(item_fn); - - ComponentDeserializerOutput { - comp_fn, - props_struct: None, - } - } - - fn deserialize_with_props( - component_body: &ComponentBody, - ) -> Result { - let ComponentBody { item_fn, .. } = component_body; - - let comp_parsed = match parse2::(quote!(#item_fn)) { - Ok(comp_body) => comp_body, - Err(e) => { - return Err(Error::new( - e.span(), - format!( - "This is probably a bug in our code, please report it! Error: {}", - e - ), - )) - } - }; - - let inlined_props_output = comp_parsed.deserialize(InlinePropsDeserializerArgs {})?; - let props_struct = inlined_props_output.props_struct; - let props_fn = inlined_props_output.comp_fn; - - let comp_fn = get_out_comp_fn(&props_fn); - - Ok(ComponentDeserializerOutput { - comp_fn, - props_struct: Some(props_struct), - }) - } -} - -fn is_pascal_case(input: &str) -> bool { - let mut is_next_lowercase = false; - - for c in input.chars() { - let is_upper = c.is_ascii_uppercase(); - - if (c == '_') || (is_upper && is_next_lowercase) { - return false; - } - - is_next_lowercase = is_upper; - } - - true -} diff --git a/packages/core-macro/src/component_body_deserializers/inline_props.rs b/packages/core-macro/src/component_body_deserializers/inline_props.rs deleted file mode 100644 index 6b9f2b599..000000000 --- a/packages/core-macro/src/component_body_deserializers/inline_props.rs +++ /dev/null @@ -1,355 +0,0 @@ -use crate::component_body::{ComponentBody, DeserializerArgs}; -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::token::Comma; -use syn::{punctuated::Punctuated, *}; - -/// The args and deserializing implementation for the [`crate::inline_props`] macro. -#[derive(Clone)] -pub struct InlinePropsDeserializerArgs; - -/// The output fields and [`ToTokens`] implementation for the [`crate::inline_props`] macro. -#[derive(Clone)] -pub struct InlinePropsDeserializerOutput { - pub comp_fn: ItemFn, - pub props_struct: ItemStruct, -} - -impl ToTokens for InlinePropsDeserializerOutput { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let function = &self.comp_fn; - let props_struct = &self.props_struct; - - tokens.append_all(quote! { - #function - #props_struct - }); - } -} - -impl DeserializerArgs for InlinePropsDeserializerArgs { - fn to_output(&self, component_body: &ComponentBody) -> Result { - Ok(InlinePropsDeserializerOutput { - comp_fn: get_function(component_body), - props_struct: get_props_struct(component_body), - }) - } -} - -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; - - let struct_fields = inputs.iter().map(move |f| { - match f { - FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing - FnArg::Typed(pt) => { - let arg_pat = match pt.pat.as_ref() { - // rip off mutability - Pat::Ident(f) => { - let mut f = f.clone(); - f.mutability = None; - quote! { #f } - } - a => quote! { #a }, - }; - - 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 - } - } - } - }); - - 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, Clone)] } - } else { - quote! { #[derive(Props, Clone, 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_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, - &pt.ty, - pt.attrs.iter().find_map(|attr| { - if attr.path() != &parse_quote!(deprecated) { - return None; - } - - let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); - - match res { - Err(e) => panic!("{}", e.to_string()), - Ok(v) => Some(v), - } - }), - 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, deprecation, input_arg_doc) in arg_docs { - let arg_name = arg_name.into_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", "

"); - 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("

👎 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("

"); - - if !input_arg_doc.is_empty() { - arg_doc.push_str("
"); - } - } - - if !input_arg_doc.is_empty() { - 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, .. } = 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()); - - // Skip first arg since that's the context - let struct_field_names = inputs.iter().filter_map(|f| match f { - FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters. - FnArg::Typed(pt) => { - let pat = &pt.pat; - - let mut pat = pat.clone(); - - // rip off mutability, but still write it out eventually - if let Pat::Ident(ref mut pat_ident) = pat.as_mut() { - pat_ident.mutability = None; - } - - Some(quote!(mut #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 (mut __props: #struct_ident #generics_no_bounds) #fn_output - #where_clause - { - let #struct_ident { #(#struct_field_names),* } = __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_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 = 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 -} diff --git a/packages/core-macro/src/component_body_deserializers/mod.rs b/packages/core-macro/src/component_body_deserializers/mod.rs deleted file mode 100644 index a3027796d..000000000 --- a/packages/core-macro/src/component_body_deserializers/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers. - -pub mod component; -pub mod inline_props; diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index f9a29c0b1..94a4d0f49 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -2,21 +2,15 @@ #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")] #![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")] +use component::ComponentBody; use proc_macro::TokenStream; use quote::ToTokens; -use syn::parse::Parser; -use syn::punctuated::Punctuated; -use syn::{parse_macro_input, Path, Token}; +use syn::parse_macro_input; -mod component_body; -mod component_body_deserializers; +mod component; mod props; mod utils; -// mod rsx; -use crate::component_body::ComponentBody; -use crate::component_body_deserializers::component::ComponentDeserializerArgs; -use crate::component_body_deserializers::inline_props::InlinePropsDeserializerArgs; use dioxus_rsx as rsx; #[proc_macro] @@ -54,6 +48,50 @@ pub fn render(tokens: TokenStream) -> TokenStream { rsx(tokens) } +/// Streamlines component creation. +/// This is the recommended way of creating components, +/// though you might want lower-level control with more advanced uses. +/// +/// # Arguments +/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names. +/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.** +/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas +/// macros are extremely limited. +/// +/// # Features +/// This attribute: +/// * Enforces that your component uses `PascalCase`. +/// No warnings are generated for the `PascalCase` +/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`. +/// Does not disable warnings anywhere else, so if you, for example, +/// accidentally don't use `snake_case` +/// for a variable name in the function, the compiler will still warn you. +/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function. +/// * Verifies the validity of your component. +/// +/// # Examples +/// * Without props: +/// ```rust,ignore +/// #[component] +/// fn GreetBob() -> Element { +/// rsx! { "hello, bob" } +/// } +/// ``` +/// +/// * With props: +/// ```rust,ignore +/// #[component] +/// fn GreetBob(bob: String) -> Element { +/// rsx! { "hello, {bob}" } +/// } +/// ``` +#[proc_macro_attribute] +pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream { + parse_macro_input!(input as ComponentBody) + .into_token_stream() + .into() +} + /// Derive props for a component within the component definition. /// /// This macro provides a simple transformation from `Scope<{}>` to `Scope

`, @@ -82,117 +120,6 @@ pub fn render(tokens: TokenStream) -> TokenStream { /// ``` #[proc_macro_attribute] #[deprecated(note = "Use `#[component]` instead.")] -pub fn inline_props(_args: TokenStream, s: TokenStream) -> TokenStream { - let comp_body = parse_macro_input!(s as ComponentBody); - - match comp_body.deserialize(InlinePropsDeserializerArgs {}) { - Err(e) => e.to_compile_error().into(), - Ok(output) => output.to_token_stream().into(), - } -} - -pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check"; - -/// Streamlines component creation. -/// This is the recommended way of creating components, -/// though you might want lower-level control with more advanced uses. -/// -/// # Arguments -/// * `no_case_check` - Doesn't enforce `PascalCase` on your component names. -/// **This will be removed/deprecated in a future update in favor of a more complete Clippy-backed linting system.** -/// The reasoning behind this is that Clippy allows more robust and powerful lints, whereas -/// macros are extremely limited. -/// -/// # Features -/// This attribute: -/// * Enforces that your component uses `PascalCase`. -/// No warnings are generated for the `PascalCase` -/// function name, but everything else will still raise a warning if it's incorrectly `PascalCase`. -/// Does not disable warnings anywhere else, so if you, for example, -/// accidentally don't use `snake_case` -/// for a variable name in the function, the compiler will still warn you. -/// * Automatically uses `#[inline_props]` if there's more than 1 parameter in the function. -/// * Verifies the validity of your component. -/// E.g. if it has a [`Scope`](dioxus_core::Scope) argument. -/// Notes: -/// * This doesn't work 100% of the time, because of macro limitations. -/// * Provides helpful messages if your component is not correct. -/// Possible bugs (please, report these!): -/// * There might be bugs where it incorrectly *denies* validity. -/// This is bad as it means that you can't use the attribute or you have to change the component. -/// * There might be bugs where it incorrectly *confirms* validity. -/// You will still know if the component is invalid once you use it, -/// but the error might be less helpful. -/// -/// # Examples -/// * Without props: -/// ```rust,ignore -/// #[component] -/// fn GreetBob() -> Element { -/// rsx! { "hello, bob" } -/// } -/// -/// // is equivalent to -/// -/// #[allow(non_snake_case)] -/// fn GreetBob() -> Element { -/// #[warn(non_snake_case)] -/// #[inline(always)] -/// fn __dx_inner_comp() -> Element { -/// rsx! { "hello, bob" } -/// } -/// // There's no function call overhead since __dx_inner_comp has the #[inline(always)] attribute, -/// // so don't worry about performance. -/// __dx_inner_comp(cx) -/// } -/// ``` -/// * With props: -/// ```rust,ignore -/// #[component(no_case_check)] -/// fn GreetPerson(person: String) -> Element { -/// rsx! { "hello, {person}" } -/// } -/// -/// // is equivalent to -/// -/// #[derive(Props, PartialEq)] -/// #[allow(non_camel_case_types)] -/// struct GreetPersonProps { -/// person: String, -/// } -/// -/// #[allow(non_snake_case)] -/// fn GreetPerson(props: GreetPersonProps>) -> Element { -/// #[warn(non_snake_case)] -/// #[inline(always)] -/// fn __dx_inner_comp(props: GreetPersonProps>e) -> Element { -/// let GreetPersonProps { person } = props; -/// { -/// rsx! { "hello, {person}" } -/// } -/// } -/// -/// __dx_inner_comp(cx) -/// } -/// ``` -// TODO: Maybe add an option to input a custom component name through the args. -// I think that's unnecessary, but there might be some scenario where it could be useful. -#[proc_macro_attribute] -pub fn component(args: TokenStream, input: TokenStream) -> TokenStream { - let component_body = parse_macro_input!(input as ComponentBody); - let case_check = match Punctuated::::parse_terminated.parse(args) { - Err(e) => return e.to_compile_error().into(), - Ok(args) => { - if let Some(first) = args.first() { - !first.is_ident(COMPONENT_ARG_CASE_CHECK_OFF) - } else { - true - } - } - }; - - match component_body.deserialize(ComponentDeserializerArgs { case_check }) { - Err(e) => e.to_compile_error().into(), - Ok(output) => output.to_token_stream().into(), - } +pub fn inline_props(args: TokenStream, input: TokenStream) -> TokenStream { + component(args, input) }