");
- 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(" = 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)
}
From 494f7e727da9152bea3214a73f0da05032a2e9de Mon Sep 17 00:00:00 2001
From: Jonathan Kelley
Date: Sat, 9 Mar 2024 01:31:40 -0800
Subject: [PATCH 2/3] Heavily document component macro
---
packages/core-macro/src/component.rs | 299 +++++++++++++++------------
1 file changed, 164 insertions(+), 135 deletions(-)
diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs
index d05a12d06..c5761876c 100644
--- a/packages/core-macro/src/component.rs
+++ b/packages/core-macro/src/component.rs
@@ -4,10 +4,6 @@ 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,
}
@@ -24,8 +20,14 @@ impl ToTokens for ComponentBody {
fn to_tokens(&self, tokens: &mut TokenStream) {
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 thatn also attach the doc attributes to it
false => {
let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident);
let props_struct = self.props_struct();
@@ -64,19 +66,22 @@ impl ComponentBody {
..
} = sig;
let Generics { where_clause, .. } = generics;
+ let (_, ty_generics, _) = generics.split_for_impl();
// 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();
+ // We pull in the field names from the original function signature, but need to strip off the mutability
+ let struct_field_names = inputs.iter().filter_map(rebind_mutability);
+
let props_docs = self.props_docs(inputs.iter().skip(1).collect());
+ // Don't generate the props argument if there are no inputs
+ // This means we need to skip adding the argument to the function signature, and also skip the expanded struct
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; },
@@ -92,10 +97,16 @@ impl ComponentBody {
}
}
- // Build the props struct
+ /// 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 {
- let ComponentBody { item_fn, .. } = &self;
- let ItemFn { vis, sig, .. } = item_fn;
+ let ItemFn { vis, sig, .. } = &self.item_fn;
let Signature {
inputs,
ident,
@@ -103,17 +114,21 @@ impl ComponentBody {
..
} = sig;
- let struct_fields = inputs.iter().map(move |f| make_prop_struct_fields(f, vis));
+ let struct_fields = inputs.iter().map(move |f| make_prop_struct_field(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),* }
+ #vis struct #struct_ident #generics {
+ #(#struct_fields),*
+ }
}
}
+ /// 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 {
let fn_ident = &self.item_fn.sig.ident;
@@ -123,58 +138,7 @@ impl ComponentBody {
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,
- ))
- }
- })
+ .filter_map(|f| build_doc_fields(f))
.collect::>();
let mut props_docs = Vec::with_capacity(5);
@@ -186,7 +150,14 @@ impl ComponentBody {
#[doc = #header]
});
- for (arg_name, arg_type, deprecation, input_arg_doc) in arg_docs {
+ for arg in arg_docs {
+ let DocField {
+ arg_name,
+ arg_type,
+ deprecation,
+ input_arg_doc,
+ } = arg;
+
let arg_name = arg_name.into_token_stream().to_string();
let arg_type = crate::utils::format_type_string(arg_type);
@@ -220,91 +191,70 @@ impl ComponentBody {
arg_doc.push_str(&format!("{input_arg_doc}
"));
}
- props_docs.push(parse_quote! {
- #[doc = #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 },
+struct DocField<'a> {
+ arg_name: &'a Box,
+ arg_type: &'a Box,
+ deprecation: Option,
+ input_arg_doc: String,
+}
+
+fn build_doc_fields(f: &FnArg) -> Option {
+ 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 arg_colon = &pt.colon_token;
- let arg_ty = &pt.ty; // Type
- let arg_attrs = &pt.attrs; // Attributes
+ let Meta::NameValue(meta_name_value) = &attr.meta else {
+ return None;
+ };
- quote! {
- #(#arg_attrs)
- *
- #vis #arg_pat #arg_colon #arg_ty
- }
- }
- }
-}
+ let Expr::Lit(doc_lit) = &meta_name_value.value else {
+ return None;
+ };
-fn strip_mutability(f: &FnArg) -> Option {
- match f {
- FnArg::Receiver(_) => unreachable!(), // ComponentBody prohibits receiver parameters.
- FnArg::Typed(pt) => {
- let pat = &pt.pat;
+ let Lit::Str(doc_lit_str) = &doc_lit.lit else {
+ return None;
+ };
- let mut pat = pat.clone();
+ Some(doc_lit_str.value())
+ })
+ .fold(String::new(), |mut doc, next_doc_line| {
+ doc.push('\n');
+ doc.push_str(&next_doc_line);
+ doc
+ });
- // 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(DocField {
+ arg_name: &pt.pat,
+ arg_type: &pt.ty,
+ deprecation: pt.attrs.iter().find_map(|attr| {
+ if attr.path() != &parse_quote!(deprecated) {
+ return None;
}
- Some(quote!(mut #pat))
- }
- }
-}
+ let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta);
-/// 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;
- }
+ match res {
+ Err(e) => panic!("{}", e.to_string()),
+ Ok(v) => Some(v),
}
- _ => {
- output.push(c);
- prev_char = Some(c);
- consecutive_count = 1;
- }
- }
- }
-
- output
+ }),
+ input_arg_doc: arg_doc,
+ })
}
fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
@@ -356,3 +306,82 @@ fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> {
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
+ }
+}
+
+fn rebind_mutability(f: &FnArg) -> Option {
+ // There's no receivers (&self) allowed in the component body
+ let FnArg::Typed(pt) = f else { unreachable!() };
+
+ 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
+}
From 32595ce161d8bfc13a4119113976e67f123de4b8 Mon Sep 17 00:00:00 2001
From: Jonathan Kelley
Date: Sat, 9 Mar 2024 01:54:25 -0800
Subject: [PATCH 3/3] Fix clippy on box references
---
packages/core-macro/src/component.rs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs
index c5761876c..2421e4660 100644
--- a/packages/core-macro/src/component.rs
+++ b/packages/core-macro/src/component.rs
@@ -199,8 +199,8 @@ impl ComponentBody {
}
struct DocField<'a> {
- arg_name: &'a Box,
- arg_type: &'a Box,
+ arg_name: &'a Pat,
+ arg_type: &'a Type,
deprecation: Option,
input_arg_doc: String,
}