Add system for creating component attributes + new #[component] attribute (#1448)

* Add `#[component]` attribute + system for creating component attributes + other stuff

* Delete inlineprops.rs

* Update inline_props.rs

* Cargo fmt

* Fix clippy warnings and paths in props/mods.rs

* Include where clause in `#[inline_props]` output

* Allow Clippy type complexity in `LinkProps`

* Allow the type complexity lint for the entire link.rs file

* Remove snake_case -> PascalCase converter, but rather enforce PascalCase

Also:
- Put the second function inside the main one instead of besides it.
- Simplify

* Simplify type check lints so they don't return false positives

They will not always work, but they won't return any false positives, like for aliases. This is likely going to be replaced by a more polished Clippy-backed linting system.

* Fix #583

* Cargo fmt

* Add docs for `deserialize()` and remove useless comment

* Add `#[component]` to prelude

* Merge branch 'master' of https://github.com/tigerros/dioxus

* #[inline_props] is no more. Except in the docs folder, but that's going to be removed

* Remove docs folder

* Remove docs from workspace

* Resolve `DeserializerOutput` conversation
This commit is contained in:
Leonard authored and GitHub committed 2023-09-15 09:13:36 -05:00
1 parent b68a1f57e0
commit c866ae602b
281 files changed
+1122 -12818

No files matched your search

@@ -0,0 +1,237 @@
//! 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::<Path, Token![,]>::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(cx: Scope) -> Element {
//! render! { "I want a cool name!" }
//! }
//!
//! pub fn App(cx: Scope) -> Element {
//! render! { 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<NameChangerDeserializerOutput> for NameChangerDeserializerArgs<'a> {
//! fn to_output(&self, component_body: &ComponentBody) -> Result<NameChangerDeserializerOutput> {
//! 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;
pub use utils::TypeHelper;
use dioxus_core::{Element, Scope};
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,
/// Parsing tries to ensure that this argument will be a [`Scope`].
/// **However, macros have limitations that prevent this from always working,
/// so don't take this for granted!**
pub cx_arg: FnArg,
/// The pattern (name) and type of the context argument.
pub cx_pat_type: PatType,
/// 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<TOutput, TArgs>(&self, args: TArgs) -> Result<TOutput>
where
TOutput: DeserializerOutput,
TArgs: DeserializerArgs<TOutput>,
{
args.to_output(self)
}
}
impl Parse for ComponentBody {
fn parse(input: ParseStream) -> Result<Self> {
let item_fn: ItemFn = input.parse()?;
let scope_type_path = Scope::get_path_string();
let (cx_arg, cx_pat_type) = if let Some(first_arg) = item_fn.sig.inputs.first() {
let incorrect_first_arg_err = Err(Error::new(
first_arg.span(),
format!("First argument must be a <{}>", scope_type_path),
));
match first_arg.to_owned() {
FnArg::Receiver(_) => {
return incorrect_first_arg_err;
}
FnArg::Typed(f) => (first_arg.to_owned(), f),
}
} else {
return Err(Error::new(
item_fn.sig.ident.span(), // Or maybe just item_f.sig.span()?
format!(
"Must have at least one argument that's a <{}>",
scope_type_path
),
));
};
let element_type_path = Element::get_path_string();
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.len() > 1;
Ok(Self {
item_fn,
cx_arg,
cx_pat_type,
has_extra_args,
})
}
}
@@ -0,0 +1,57 @@
use crate::component_body::ComponentBody;
use dioxus_core::{Element, Scope};
use quote::ToTokens;
use syn::{parse_quote, Path};
/// 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<T: ToTokens> 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<TOutput>: 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<TOutput>;
}
pub trait TypeHelper {
fn get_path() -> Path;
fn get_path_string() -> String {
Self::get_path().to_token_stream().to_string()
}
}
impl<'a> TypeHelper for Scope<'a> {
fn get_path() -> Path {
parse_quote!(::dioxus::core::Scope)
}
}
impl<'a> TypeHelper for Element<'a> {
fn get_path() -> Path {
parse_quote!(::dioxus::core::Element)
}
}
@@ -0,0 +1,152 @@
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, cx_pat: &Pat) -> 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()
};
ItemFn {
block: parse_quote! {
{
#[warn(non_snake_case)]
#[inline(always)]
#inner_comp_fn
#inner_comp_ident (#cx_pat)
}
},
..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<ItemStruct>,
}
impl ToTokens for ComponentDeserializerOutput {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let comp_fn = &self.comp_fn;
let props_struct = &self.props_struct;
tokens.append_all(quote! {
#props_struct
#[allow(non_snake_case)]
#comp_fn
});
}
}
impl DeserializerArgs<ComponentDeserializerOutput> for ComponentDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result<ComponentDeserializerOutput> {
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,
cx_pat_type,
..
} = component_body;
let cx_pat = &cx_pat_type.pat;
let comp_fn = get_out_comp_fn(item_fn, cx_pat);
ComponentDeserializerOutput {
comp_fn,
props_struct: None,
}
}
fn deserialize_with_props(
component_body: &ComponentBody,
) -> Result<ComponentDeserializerOutput> {
let ComponentBody {
item_fn,
cx_pat_type,
..
} = component_body;
let cx_pat = &cx_pat_type.pat;
let comp_parsed = match parse2::<ComponentBody>(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, cx_pat);
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
}
@@ -0,0 +1,195 @@
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<InlinePropsDeserializerOutput> for InlinePropsDeserializerArgs {
fn to_output(&self, component_body: &ComponentBody) -> Result<InlinePropsDeserializerOutput> {
Ok(InlinePropsDeserializerOutput {
comp_fn: Self::get_function(component_body),
props_struct: Self::get_props_struct(component_body),
})
}
}
impl InlinePropsDeserializerArgs {
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;
// Skip first arg since that's the context
let struct_fields = inputs.iter().skip(1).map(move |f| {
match f {
FnArg::Receiver(_) => unreachable!(), // Unreachable because of ComponentBody parsing
FnArg::Typed(pt) => {
let arg_pat = &pt.pat; // Pattern (identifier)
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)] }
} 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 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(t) => Some(&t.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
};
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
}
}
}
}
@@ -0,0 +1,4 @@
//! This module contains all [`ComponentBody`](crate::component_body::ComponentBody) deserializers.
pub mod component;
pub mod inline_props;
-185
View File
@@ -1,185 +0,0 @@
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
punctuated::Punctuated,
*,
};
pub struct InlinePropsBody {
pub attrs: Vec<Attribute>,
pub vis: syn::Visibility,
pub maybe_async: Option<Token![async]>,
pub fn_token: Token![fn],
pub ident: Ident,
pub cx_token: Box<Pat>,
pub generics: Generics,
pub paren_token: token::Paren,
pub inputs: Punctuated<FnArg, Token![,]>,
// pub fields: FieldsNamed,
pub output: ReturnType,
pub where_clause: Option<WhereClause>,
pub block: Box<Block>,
}
/// The custom rusty variant of parsing rsx!
impl Parse for InlinePropsBody {
fn parse(input: ParseStream) -> Result<Self> {
let attrs: Vec<Attribute> = input.call(Attribute::parse_outer)?;
let maybe_async: Option<Token![async]> = input.parse().ok();
let vis: Visibility = input.parse()?;
let fn_token = input.parse()?;
let ident = input.parse()?;
let generics: Generics = input.parse()?;
let content;
let paren_token = syn::parenthesized!(content in input);
let first_arg: FnArg = content.parse()?;
let cx_token = {
match first_arg {
FnArg::Receiver(_) => panic!("first argument must not be a receiver argument"),
FnArg::Typed(f) => f.pat,
}
};
let _: Result<Token![,]> = content.parse();
let inputs = syn::punctuated::Punctuated::parse_terminated(&content)?;
let output = input.parse()?;
let where_clause = input
.peek(syn::token::Where)
.then(|| input.parse())
.transpose()?;
let block = input.parse()?;
Ok(Self {
vis,
maybe_async,
fn_token,
ident,
generics,
paren_token,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
})
}
}
/// Serialize the same way, regardless of flavor
impl ToTokens for InlinePropsBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let Self {
vis,
ident,
generics,
inputs,
output,
where_clause,
block,
cx_token,
attrs,
maybe_async,
..
} = self;
let fields = inputs.iter().map(|f| {
quote! { #vis #f }
});
let struct_name = Ident::new(&format!("{ident}Props"), Span::call_site());
let field_names = inputs.iter().filter_map(|f| match f {
FnArg::Receiver(_) => todo!(),
FnArg::Typed(t) => Some(&t.pat),
});
let first_lifetime = if let Some(GenericParam::Lifetime(lt)) = generics.params.first() {
Some(lt)
} else {
None
};
let modifiers = if first_lifetime.is_some() {
quote! { #[derive(Props)] }
} else {
quote! { #[derive(Props, PartialEq)] }
};
let (scope_lifetime, fn_generics, struct_generics) = if let Some(lt) = first_lifetime {
let struct_generics: Punctuated<_, token::Comma> = 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! { #lt, },
generics.clone(),
quote! { <#struct_generics> },
)
} 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, quote! { #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
};
out_tokens.append_all(quote! {
#modifiers
#[allow(non_camel_case_types)]
#vis struct #struct_name #struct_generics
#where_clause
{
#(#fields),*
}
#(#attrs)*
#maybe_async #vis fn #ident #fn_generics (#cx_token: Scope<#scope_lifetime #struct_name #generics_no_bounds>) #output
#where_clause
{
let #struct_name { #(#field_names),* } = &#cx_token.props;
#block
}
});
}
}
+123 -8
View File
@@ -1,12 +1,18 @@
use proc_macro::TokenStream;
use quote::ToTokens;
use rsx::RenderCallBody;
use syn::parse_macro_input;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{parse_macro_input, Path, Token};
mod inlineprops;
mod component_body;
mod component_body_deserializers;
mod props;
// 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]
@@ -18,7 +24,7 @@ pub fn format_args_f(input: TokenStream) -> TokenStream {
}
#[proc_macro_derive(Props, attributes(props))]
pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
pub fn derive_typed_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as syn::DeriveInput);
match props::impl_my_derive(&input) {
Ok(output) => output.into(),
@@ -37,7 +43,7 @@ pub fn rsx(s: TokenStream) -> TokenStream {
/// The render! macro makes it easy for developers to write jsx-style markup in their components.
///
/// The render macro automatically renders rsx - making it unhygenic.
/// The render macro automatically renders rsx - making it unhygienic.
#[proc_macro]
pub fn render(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
@@ -55,7 +61,7 @@ pub fn render(s: TokenStream) -> TokenStream {
/// you would be repeating a lot of the usual Rust boilerplate.
///
/// # Example
/// ```ignore
/// ```rust,ignore
/// #[inline_props]
/// fn app(cx: Scope, bob: String) -> Element {
/// cx.render(rsx!("hello, {bob}"))
@@ -73,9 +79,118 @@ pub fn render(s: TokenStream) -> TokenStream {
/// }
/// ```
#[proc_macro_attribute]
pub fn inline_props(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
match syn::parse::<inlineprops::InlinePropsBody>(s) {
#[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(s) => s.to_token_stream().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(cx: Scope) -> Element {
/// render! { "hello, bob" }
/// }
///
/// // is equivalent to
///
/// #[allow(non_snake_case)]
/// fn GreetBob(cx: Scope) -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp(cx: Scope) -> Element {
/// render! { "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(cx: Scope, person: String) -> Element {
/// render! { "hello, {person}" }
/// }
///
/// // is equivalent to
///
/// #[derive(Props, PartialEq)]
/// #[allow(non_camel_case_types)]
/// struct GreetPersonProps {
/// person: String,
/// }
///
/// #[allow(non_snake_case)]
/// fn GreetPerson<'a>(cx: Scope<'a, GreetPersonProps>) -> Element {
/// #[warn(non_snake_case)]
/// #[inline(always)]
/// fn __dx_inner_comp<'a>(cx: Scope<'a, GreetPersonProps>e) -> Element {
/// let GreetPersonProps { person } = &cx.props;
/// {
/// render! { "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::<Path, Token![,]>::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(),
}
}
+29 -26
View File
@@ -2,9 +2,9 @@
//!
//! However, it has been adopted to fit the Dioxus Props builder pattern.
//!
//! For dioxus, we make a few changes:
//! - [ ] automatically implement Into<Option> on the setters (IE the strip setter option)
//! - [ ] automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
//! For Dioxus, we make a few changes:
//! - [ ] Automatically implement Into<Option> on the setters (IE the strip setter option)
//! - [ ] Automatically implement a default of none for optional fields (those explicitly wrapped with Option<T>)
use proc_macro2::TokenStream;
@@ -194,8 +194,9 @@ mod field_info {
// children field is automatically defaulted to None
if name == "children" {
builder_attr.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
// auto detect optional
@@ -204,8 +205,9 @@ mod field_info {
&& type_from_inside_option(&field.ty, true).is_some();
if !builder_attr.strip_option && strip_option_auto {
builder_attr.strip_option = true;
builder_attr.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
builder_attr.default = Some(
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
);
}
Ok(FieldInfo {
@@ -213,7 +215,7 @@ mod field_info {
name,
generic_ident: syn::Ident::new(
&format!("__{}", strip_raw_ident_prefix(name.to_string())),
proc_macro2::Span::call_site(),
name.span(),
),
ty: &field.ty,
builder_attr,
@@ -345,14 +347,18 @@ mod field_info {
.ok_or_else(|| Error::new_spanned(&path, "Expected identifier"))?;
match name.as_str() {
"default" => {
self.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
);
Ok(())
}
"optional" => {
self.default =
Some(syn::parse(quote!(Default::default()).into()).unwrap());
self.default = Some(
syn::parse(quote!(::core::default::Default::default()).into())
.unwrap(),
);
self.strip_option = true;
Ok(())
}
@@ -505,15 +511,12 @@ mod struct_info {
.map(|(i, f)| FieldInfo::new(i, f, builder_attr.field_defaults.clone()))
.collect::<Result<_, _>>()?,
builder_attr,
builder_name: syn::Ident::new(&builder_name, proc_macro2::Span::call_site()),
builder_name: syn::Ident::new(&builder_name, ast.ident.span()),
conversion_helper_trait_name: syn::Ident::new(
&format!("{builder_name}_Optional"),
proc_macro2::Span::call_site(),
),
core: syn::Ident::new(
&format!("{builder_name}_core"),
proc_macro2::Span::call_site(),
ast.ident.span(),
),
core: syn::Ident::new(&format!("{builder_name}_core"), ast.ident.span()),
})
}
@@ -551,11 +554,11 @@ mod struct_info {
let phantom_generics = self.generics.params.iter().map(|param| match param {
syn::GenericParam::Lifetime(lifetime) => {
let lifetime = &lifetime.lifetime;
quote!(core::marker::PhantomData<&#lifetime ()>)
quote!(::core::marker::PhantomData<&#lifetime ()>)
}
syn::GenericParam::Type(ty) => {
let ty = &ty.ident;
quote!(core::marker::PhantomData<#ty>)
quote!(::core::marker::PhantomData<#ty>)
}
syn::GenericParam::Const(_cnst) => {
quote!()
@@ -634,7 +637,7 @@ Finally, call `.build()` to create the instance of `{name}`.
#vis fn builder() -> #builder_name #generics_with_empty {
#builder_name {
fields: #empties_tuple,
_phantom: core::default::Default::default(),
_phantom: ::core::default::Default::default(),
}
}
}
@@ -651,12 +654,12 @@ Finally, call `.build()` to create the instance of `{name}`.
fn clone(&self) -> Self {
Self {
fields: self.fields.clone(),
_phantom: Default::default(),
_phantom: ::core::default::Default::default(),
}
}
}
impl #impl_generics dioxus::prelude::Properties for #name #ty_generics
impl #impl_generics ::dioxus::prelude::Properties for #name #ty_generics
#b_generics_where_extras_predicates
{
type Builder = #builder_name #generics_with_empty;
@@ -793,7 +796,7 @@ Finally, call `.build()` to create the instance of `{name}`.
};
let (arg_type, arg_expr) = if field.builder_attr.auto_into {
(
quote!(impl core::convert::Into<#arg_type>),
quote!(impl ::core::convert::Into<#arg_type>),
quote!(#field_name.into()),
)
} else {
@@ -811,7 +814,7 @@ Finally, call `.build()` to create the instance of `{name}`.
builder_name,
strip_raw_ident_prefix(field_name.to_string())
),
proc_macro2::Span::call_site(),
builder_name.span(),
);
let repeated_fields_error_message = format!("Repeated field {field_name}");
@@ -930,7 +933,7 @@ Finally, call `.build()` to create the instance of `{name}`.
builder_name,
strip_raw_ident_prefix(field_name.to_string())
),
proc_macro2::Span::call_site(),
builder_name.span(),
);
let early_build_error_message = format!("Missing required field {field_name}");