From 550edf75fa60173c3dcffa5a4ca1bfa6333d0a1f Mon Sep 17 00:00:00 2001 From: tigerros Date: Wed, 10 Apr 2024 17:36:48 +0200 Subject: [PATCH 01/15] proper snake case bypass + remove doc building + fix parsing of struct pattern --- packages/core-macro/src/component.rs | 212 +++++---------------------- packages/core-macro/src/lib.rs | 1 - packages/core-macro/src/utils.rs | 129 ---------------- 3 files changed, 39 insertions(+), 303 deletions(-) delete mode 100644 packages/core-macro/src/utils.rs diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index a347fee64e..1d32fdbb38 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -22,12 +22,13 @@ impl ToTokens for ComponentBody { // If there's only one input and the input is `props: Props`, we don't need to generate a props struct // Just attach the non_snake_case attribute to the function // eventually we'll dump this metadata into devtooling that lets us find all these components - if self.is_explicit_props_ident() { + // + // Components can also use the struct pattern to "inline" their props. + // Freya uses this a bunch (because it's clean), + // e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed + if self.is_explicit_props_ident() || self.has_struct_parameter_pattern() { let comp_fn = &self.item_fn; - tokens.append_all(quote! { - #[allow(non_snake_case)] - #comp_fn - }); + tokens.append_all(allow_snake_case_for_fn_ident(comp_fn).into_token_stream()); return; } @@ -53,8 +54,6 @@ impl ToTokens for ComponentBody { tokens.append_all(quote! { #props_struct - - #[allow(non_snake_case)] #comp_fn }); } @@ -75,9 +74,9 @@ impl ComponentBody { ident: fn_ident, generics, output: fn_output, - asyncness, .. } = sig; + let Generics { where_clause, .. } = generics; let (_, ty_generics, _) = generics.split_for_impl(); @@ -87,8 +86,6 @@ impl ComponentBody { // 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() { @@ -100,12 +97,15 @@ impl ComponentBody { false => quote! { let #struct_ident { #(#struct_field_names),* } = __props; }, }; + // The extra nest is for the snake case warning to kick back in parse_quote! { #(#attrs)* - #(#props_docs)* - #asyncness #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause { - #expanded_struct - #block + #[allow(non_snake_case)] + #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause { + { + #expanded_struct + #block + } } } } @@ -139,80 +139,9 @@ impl ComponentBody { } } - /// 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; - - if inputs.len() <= 1 { - return Vec::new(); - } - - let arg_docs = inputs - .iter() - .filter_map(|f| build_doc_fields(f)) - .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 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); - - 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 is_explicit_props_ident(&self) -> bool { - if self.item_fn.sig.inputs.len() == 1 { - if let FnArg::Typed(PatType { pat, .. }) = &self.item_fn.sig.inputs[0] { + if let Some(first_input) = self.item_fn.sig.inputs.first() { + if let FnArg::Typed(PatType { pat, .. }) = &first_input { if let Pat::Ident(ident) = pat.as_ref() { return ident.ident == "props"; } @@ -221,65 +150,18 @@ impl ComponentBody { false } -} - -struct DocField<'a> { - arg_name: &'a Pat, - arg_type: &'a Type, - 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 Meta::NameValue(meta_name_value) = &attr.meta else { - return None; - }; - - let Expr::Lit(doc_lit) = &meta_name_value.value else { - return None; - }; - - let Lit::Str(doc_lit_str) = &doc_lit.lit else { - return None; - }; - - Some(doc_lit_str.value()) - }) - .fold(String::new(), |mut doc, next_doc_line| { - doc.push('\n'); - doc.push_str(&next_doc_line); - doc - }); - - Some(DocField { - arg_name: &pt.pat, - arg_type: &pt.ty, - deprecation: pt.attrs.iter().find_map(|attr| { - if attr.path() != &parse_quote!(deprecated) { - return None; + fn has_struct_parameter_pattern(&self) -> bool { + if let Some(first_input) = self.item_fn.sig.inputs.first() { + if let FnArg::Typed(PatType { pat, .. }) = &first_input { + if matches!(pat.as_ref(), Pat::Struct(_)) { + return true; + } } + } - let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); - - match res { - Err(e) => panic!("{}", e.to_string()), - Ok(v) => Some(v), - } - }), - input_arg_doc: arg_doc, - }) + false + } } fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> { @@ -378,35 +260,19 @@ fn rebind_mutability(f: &FnArg) -> Option { 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; - } +/// Takes a function and returns a clone of it where a non snake case identifier is accepted, +/// but non snake case identifiers in the function body are not. +fn allow_snake_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { + let mut clone = item_fn.clone(); + let block = &item_fn.block; + + clone.attrs.push(parse_quote! { #[allow(non_snake_case)] }); + + clone.block = parse_quote! { + { + #block } - } + }; - output -} + clone +} \ No newline at end of file diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 65bc461987..2ef64c5213 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -9,7 +9,6 @@ use syn::parse_macro_input; mod component; mod props; -mod utils; use dioxus_rsx as rsx; diff --git a/packages/core-macro/src/utils.rs b/packages/core-macro/src/utils.rs deleted file mode 100644 index 918c8d2935..0000000000 --- a/packages/core-macro/src/utils.rs +++ /dev/null @@ -1,129 +0,0 @@ -use quote::ToTokens; -use syn::parse::{Parse, ParseStream}; -use syn::spanned::Spanned; -use syn::{parse_quote, Expr, Lit, Meta, Token, Type}; - -const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:"; -const FORMATTED_TYPE_END: &str = "= unreachable!();"; - -/// Attempts to convert the given literal to a string. -/// Converts ints and floats to their base 10 counterparts. -/// -/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`] -/// and the byte string could not be converted to UTF-8. -pub fn lit_to_string(lit: Lit) -> Option { - match lit { - Lit::Str(l) => Some(l.value()), - Lit::ByteStr(l) => String::from_utf8(l.value()).ok(), - Lit::Byte(l) => Some(String::from(l.value() as char)), - Lit::Char(l) => Some(l.value().to_string()), - Lit::Int(l) => Some(l.base10_digits().to_string()), - Lit::Float(l) => Some(l.base10_digits().to_string()), - Lit::Bool(l) => Some(l.value().to_string()), - Lit::Verbatim(_) => None, - _ => None, - } -} - -pub fn format_type_string(ty: &Type) -> String { - let ty_unformatted = ty.into_token_stream().to_string(); - let ty_unformatted = ty_unformatted.trim(); - - // This should always be valid syntax. - // Not Rust code, but syntax, which is the only thing that `syn` cares about. - let Ok(file_unformatted) = syn::parse_file(&format!( - "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}" - )) else { - return ty_unformatted.to_string(); - }; - - let file_formatted = prettyplease::unparse(&file_unformatted); - - let file_trimmed = file_formatted.trim(); - let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START); - let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END); - let ty_formatted = end_removed.trim(); - - ty_formatted.to_string() -} - -/// Represents the `#[deprecated]` attribute. -/// -/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct. -#[derive(Default)] -pub struct DeprecatedAttribute { - pub since: Option, - pub note: Option, -} - -impl DeprecatedAttribute { - /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute. - pub fn from_meta(meta: &Meta) -> syn::Result { - if meta.path() != &parse_quote!(deprecated) { - return Err(syn::Error::new( - meta.span(), - "attribute path is not `deprecated`", - )); - } - - match &meta { - Meta::Path(_) => Ok(Self::default()), - Meta::NameValue(name_value) => { - let Expr::Lit(expr_lit) = &name_value.value else { - return Err(syn::Error::new( - name_value.span(), - "literal in `deprecated` value must be a string", - )); - }; - - Ok(Self { - since: None, - note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()), - }) - } - Meta::List(list) => { - let parsed = list.parse_args::()?; - - Ok(Self { - since: parsed.since.map(|s| s.trim().to_string()), - note: parsed.note.map(|s| s.trim().to_string()), - }) - } - } - } -} - -mod kw { - use syn::custom_keyword; - custom_keyword!(since); - custom_keyword!(note); -} - -struct DeprecatedAttributeArgsParser { - since: Option, - note: Option, -} - -impl Parse for DeprecatedAttributeArgsParser { - fn parse(input: ParseStream) -> syn::Result { - let mut since: Option = None; - let mut note: Option = None; - - if input.peek(kw::since) { - input.parse::()?; - input.parse::()?; - - since = lit_to_string(input.parse()?); - } - - if input.peek(Token![,]) && input.peek2(kw::note) { - input.parse::()?; - input.parse::()?; - input.parse::()?; - - note = lit_to_string(input.parse()?); - } - - Ok(Self { since, note }) - } -} From 0ff152060262b063908644603ab0e97c3d04d232 Mon Sep 17 00:00:00 2001 From: tigerros Date: Wed, 10 Apr 2024 18:07:05 +0200 Subject: [PATCH 02/15] simplify and fix docs --- packages/core-macro/src/lib.rs | 34 +++++++++------------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 2ef64c5213..ef501c4491 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -47,41 +47,25 @@ 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. +/// * Silences warnings for the `PascalCase` function name. +/// * Seamlessly creates a props struct if there's more than 1 parameter in the function. /// * Verifies the validity of your component. /// /// # Examples +/// /// * Without props: -/// ```rust,ignore +/// ```rust /// #[component] -/// fn GreetBob() -> Element { -/// rsx! { "hello, bob" } +/// fn Greet() -> Element { +/// rsx! { "hello, someone" } /// } /// ``` /// /// * With props: -/// ```rust,ignore +/// ```rust /// #[component] -/// fn GreetBob(bob: String) -> Element { -/// rsx! { "hello, {bob}" } +/// fn Greet(person: String) -> Element { +/// rsx! { "hello, {person}" } /// } /// ``` #[proc_macro_attribute] From 892d819dbc0f5e6274ffb3f7235629477da5c1ce Mon Sep 17 00:00:00 2001 From: tigerros Date: Wed, 10 Apr 2024 18:12:01 +0200 Subject: [PATCH 03/15] Update lib.rs --- packages/core-macro/src/lib.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index ef501c4491..b63dd2b228 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -52,7 +52,7 @@ pub fn render(tokens: TokenStream) -> TokenStream { /// * Verifies the validity of your component. /// /// # Examples -/// +/// /// * Without props: /// ```rust /// #[component] @@ -65,7 +65,19 @@ pub fn render(tokens: TokenStream) -> TokenStream { /// ```rust /// #[component] /// fn Greet(person: String) -> Element { -/// rsx! { "hello, {person}" } +/// rsx! { "hello, " {person} } +/// } +/// +/// // is roughly equivalent to +/// +/// #[derive(PartialEq, Clone, Props)] +/// struct GreetProps { +/// person: String, +/// } +/// +/// #[component] +/// fn Greet(GreetProps { person }: GreetProps) -> Element { +/// rsx! { "hello, " {person} } /// } /// ``` #[proc_macro_attribute] From 3d93646db927f3a0374bfc4c01acf74607318052 Mon Sep 17 00:00:00 2001 From: tigerros Date: Wed, 10 Apr 2024 18:17:08 +0200 Subject: [PATCH 04/15] prefer camel case --- packages/core-macro/src/component.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index 1d32fdbb38..360b491303 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -28,7 +28,7 @@ impl ToTokens for ComponentBody { // e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed if self.is_explicit_props_ident() || self.has_struct_parameter_pattern() { let comp_fn = &self.item_fn; - tokens.append_all(allow_snake_case_for_fn_ident(comp_fn).into_token_stream()); + tokens.append_all(prefer_camel_case_for_fn_ident(comp_fn).into_token_stream()); return; } @@ -103,6 +103,7 @@ impl ComponentBody { #[allow(non_snake_case)] #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause { { + { struct #fn_ident {} } #expanded_struct #block } @@ -260,16 +261,17 @@ fn rebind_mutability(f: &FnArg) -> Option { Some(quote!(mut #pat)) } -/// Takes a function and returns a clone of it where a non snake case identifier is accepted, -/// but non snake case identifiers in the function body are not. -fn allow_snake_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { +/// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is preferred by the compiler. +fn prefer_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { let mut clone = item_fn.clone(); + let ident = &item_fn.sig.ident; let block = &item_fn.block; clone.attrs.push(parse_quote! { #[allow(non_snake_case)] }); clone.block = parse_quote! { { + { struct #ident {} } #block } }; From 7c56c08f1e536cf0d8ca6971b347416ed4fc97e9 Mon Sep 17 00:00:00 2001 From: tigerros Date: Wed, 10 Apr 2024 18:20:00 +0200 Subject: [PATCH 05/15] cargo fmt --- packages/core-macro/src/component.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index 360b491303..0478e15f48 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -22,7 +22,7 @@ impl ToTokens for ComponentBody { // If there's only one input and the input is `props: Props`, we don't need to generate a props struct // Just attach the non_snake_case attribute to the function // eventually we'll dump this metadata into devtooling that lets us find all these components - // + // // Components can also use the struct pattern to "inline" their props. // Freya uses this a bunch (because it's clean), // e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed @@ -268,7 +268,7 @@ fn prefer_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { let block = &item_fn.block; clone.attrs.push(parse_quote! { #[allow(non_snake_case)] }); - + clone.block = parse_quote! { { { struct #ident {} } @@ -277,4 +277,4 @@ fn prefer_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { }; clone -} \ No newline at end of file +} From b55505914460c0d8389ebd4f7125b101b36679e9 Mon Sep 17 00:00:00 2001 From: tigerros Date: Wed, 10 Apr 2024 18:39:08 +0200 Subject: [PATCH 06/15] Update lib.rs --- packages/core-macro/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index b63dd2b228..fb8257d54f 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -47,7 +47,7 @@ pub fn render(tokens: TokenStream) -> TokenStream { rsx(tokens) } -/// * Silences warnings for the `PascalCase` function name. +/// * Makes the compiler prefer an `UpperCamelCase` function identifier. /// * Seamlessly creates a props struct if there's more than 1 parameter in the function. /// * Verifies the validity of your component. /// From d898b60c3b8665ebfe9a63f897919ec616549462 Mon Sep 17 00:00:00 2001 From: tigerros Date: Thu, 11 Apr 2024 10:04:32 +0200 Subject: [PATCH 07/15] fix clippy --- packages/core-macro/src/component.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index 0478e15f48..d827b2e8a2 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -141,11 +141,9 @@ impl ComponentBody { } fn is_explicit_props_ident(&self) -> bool { - if let Some(first_input) = self.item_fn.sig.inputs.first() { - if let FnArg::Typed(PatType { pat, .. }) = &first_input { - if let Pat::Ident(ident) = pat.as_ref() { - return ident.ident == "props"; - } + if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() { + if let Pat::Ident(ident) = pat.as_ref() { + return ident.ident == "props"; } } @@ -153,11 +151,9 @@ impl ComponentBody { } fn has_struct_parameter_pattern(&self) -> bool { - if let Some(first_input) = self.item_fn.sig.inputs.first() { - if let FnArg::Typed(PatType { pat, .. }) = &first_input { - if matches!(pat.as_ref(), Pat::Struct(_)) { - return true; - } + if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() { + if matches!(pat.as_ref(), Pat::Struct(_)) { + return true; } } From 8e5f97626c22c62653986ce8a97c217f9992504e Mon Sep 17 00:00:00 2001 From: tigerros Date: Thu, 11 Apr 2024 10:12:23 +0200 Subject: [PATCH 08/15] remove lowercase components --- examples/rsx_usage.rs | 18 ------------------ packages/core/src/error_boundary.rs | 6 +++--- packages/core/src/virtual_dom.rs | 8 ++++---- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 361a3839b1..a627805ce6 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -229,16 +229,6 @@ fn helper(text: &str) -> Element { } } -// no_case_check disables PascalCase checking if you *really* want a snake_case component. -// This will likely be deprecated/removed in a future update that will introduce a more polished linting system, -// something like Clippy. -#[component(no_case_check)] -fn lowercase_helper() -> Element { - rsx! { - "asd" - } -} - mod baller { use super::*; @@ -247,14 +237,6 @@ mod baller { pub fn Baller() -> Element { rsx! { "ballin'" } } - - // no_case_check disables PascalCase checking if you *really* want a snake_case component. - // This will likely be deprecated/removed in a future update that will introduce a more polished linting system, - // something like Clippy. - #[component(no_case_check)] - pub fn lowercase_component() -> Element { - rsx! { "look ma, no uppercase" } - } } /// Documention for this component is visible within the rsx macro diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index 88359dd1ca..dbb3440dc6 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -154,7 +154,7 @@ impl ErrorBoundary { /// /// ```rust, ignore /// #[component] -/// fn app(count: String) -> Element { +/// fn App(count: String) -> Element { /// let id: i32 = count.parse().throw()?; /// /// rsx! { @@ -179,7 +179,7 @@ pub trait Throw: Sized { /// /// ```rust, ignore /// #[component] - /// fn app( count: String) -> Element { + /// fn App(count: String) -> Element { /// let id: i32 = count.parse().throw()?; /// /// rsx! { @@ -202,7 +202,7 @@ pub trait Throw: Sized { /// /// ```rust, ignore /// #[component] - /// fn app( count: String) -> Element { + /// fn App(count: String) -> Element { /// let id: i32 = count.parse().throw()?; /// /// rsx! { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index f223118eca..32a6f02476 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -58,16 +58,16 @@ use tracing::instrument; /// static ROUTES: &str = ""; /// /// #[component] -/// fn app(cx: AppProps) -> Element { +/// fn App(props: AppProps) -> Element { /// rsx!( /// NavBar { routes: ROUTES } -/// Title { "{cx.title}" } +/// Title { "{props.title}" } /// Footer {} /// ) /// } /// /// #[component] -/// fn NavBar( routes: &'static str) -> Element { +/// fn NavBar(routes: &'static str) -> Element { /// rsx! { /// div { "Routes: {routes}" } /// } @@ -130,7 +130,7 @@ use tracing::instrument; /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. /// ```rust, ignore /// #[component] -/// fn app() -> Element { +/// fn App() -> Element { /// rsx! { /// div { "Hello World" } /// } From 82b1142260fb5cb1e99679b0cdc57c731027740c Mon Sep 17 00:00:00 2001 From: tigerros Date: Thu, 11 Apr 2024 10:23:57 +0200 Subject: [PATCH 09/15] Update rsx_usage.rs --- examples/rsx_usage.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index a627805ce6..c3be8e9932 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -197,12 +197,6 @@ fn app() -> Element { Label { text: "hello geneirc world!" } Label { text: 99.9 } - // Lowercase components work too, as long as they are access using a path - baller::lowercase_component {} - - // For in-scope lowercase components, use the `self` keyword - self::lowercase_helper {} - // helper functions // Anything that implements IntoVnode can be dropped directly into Rsx {helper("hello world!")} From 2ac6863e7aaf76287cd92c81741090174e2c0da6 Mon Sep 17 00:00:00 2001 From: tigerros Date: Thu, 11 Apr 2024 18:11:13 +0200 Subject: [PATCH 10/15] remove `__props` argument in favor of a struct pattern --- packages/core-macro/src/component.rs | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index d827b2e8a2..622f28a23e 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -11,7 +11,7 @@ pub struct ComponentBody { impl Parse for ComponentBody { fn parse(input: ParseStream) -> Result { let item_fn: ItemFn = input.parse()?; - validate_component_fn_signature(&item_fn)?; + validate_component_fn(&item_fn)?; Ok(Self { item_fn }) } } @@ -41,7 +41,7 @@ impl ToTokens for ComponentBody { // 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 + // Props declared, so we generate a props struct and then also attach the doc attributes to it false => { let doc = format!("Properties for the [`{}`] component.", &comp_fn.sig.ident); let props_struct = self.props_struct(); @@ -78,33 +78,26 @@ 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()); // 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); - - // 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; }, + + let inlined_props_argument = if inputs.is_empty() { + quote! {} + } else { + quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident } }; // The extra nest is for the snake case warning to kick back in parse_quote! { #(#attrs)* #[allow(non_snake_case)] - #vis fn #fn_ident #generics (#props_ident) #fn_output #where_clause { + #vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause { { { struct #fn_ident {} } - #expanded_struct #block } } @@ -161,7 +154,7 @@ impl ComponentBody { } } -fn validate_component_fn_signature(item_fn: &ItemFn) -> Result<()> { +fn validate_component_fn(item_fn: &ItemFn) -> Result<()> { // Do some validation.... // 1. Ensure the component returns *something* if item_fn.sig.output == ReturnType::Default { @@ -254,7 +247,7 @@ fn rebind_mutability(f: &FnArg) -> Option { pat_ident.mutability = None; } - Some(quote!(mut #pat)) + Some(quote!(mut #pat)) } /// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is preferred by the compiler. From 53de7002fab380c22781000aba90d4d9245d9a77 Mon Sep 17 00:00:00 2001 From: tigerros Date: Thu, 11 Apr 2024 19:05:47 +0200 Subject: [PATCH 11/15] fix clippy --- packages/core-macro/src/component.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index 622f28a23e..5e09c4a8c6 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -78,17 +78,18 @@ impl ComponentBody { } = sig; let Generics { where_clause, .. } = generics; + let (_, impl_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()); // 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 inlined_props_argument = if inputs.is_empty() { quote! {} } else { - quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident } + quote! { #struct_ident { #(#struct_field_names),* }: #struct_ident #impl_generics } }; // The extra nest is for the snake case warning to kick back in From 530a989b31ecc991f231abd9c31017cd9d2ac1c3 Mon Sep 17 00:00:00 2001 From: tigerros Date: Fri, 26 Apr 2024 20:16:46 +0200 Subject: [PATCH 12/15] remove warning when using camel case --- packages/core-macro/src/component.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index 5e09c4a8c6..661226f680 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -28,7 +28,7 @@ impl ToTokens for ComponentBody { // e.g. `fn Navbar(NavbarProps { title }: NavbarProps)` was previously being incorrectly parsed if self.is_explicit_props_ident() || self.has_struct_parameter_pattern() { let comp_fn = &self.item_fn; - tokens.append_all(prefer_camel_case_for_fn_ident(comp_fn).into_token_stream()); + tokens.append_all(allow_camel_case_for_fn_ident(comp_fn).into_token_stream()); return; } @@ -98,7 +98,6 @@ impl ComponentBody { #[allow(non_snake_case)] #vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause { { - { struct #fn_ident {} } #block } } @@ -251,17 +250,15 @@ fn rebind_mutability(f: &FnArg) -> Option { Some(quote!(mut #pat)) } -/// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is preferred by the compiler. -fn prefer_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { +/// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is allowed by the compiler. +fn allow_camel_case_for_fn_ident(item_fn: &ItemFn) -> ItemFn { let mut clone = item_fn.clone(); - let ident = &item_fn.sig.ident; let block = &item_fn.block; clone.attrs.push(parse_quote! { #[allow(non_snake_case)] }); clone.block = parse_quote! { { - { struct #ident {} } #block } }; From 5fef88e319a1b1b6aaa8efd2cb3b9b35e39cc9e1 Mon Sep 17 00:00:00 2001 From: tigerros Date: Fri, 26 Apr 2024 20:31:15 +0200 Subject: [PATCH 13/15] fix doctests + improve docs --- packages/core-macro/src/lib.rs | 45 ++++++++-------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index fb8257d54f..5a906d6222 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -29,7 +29,7 @@ pub fn derive_typed_builder(input: TokenStream) -> TokenStream { } } -/// The rsx! macro makes it easy for developers to write jsx-style markup in their components. +/// The `rsx!` macro makes it easy for developers to write jsx-style markup in their components. #[proc_macro] pub fn rsx(tokens: TokenStream) -> TokenStream { match syn::parse::(tokens) { @@ -38,16 +38,14 @@ pub fn rsx(tokens: TokenStream) -> TokenStream { } } -/// The rsx! macro makes it easy for developers to write jsx-style markup in their components. -/// -/// The render macro automatically renders rsx - making it unhygienic. +/// This macro has been deprecated in favor of [`rsx`]. #[deprecated(note = "Use `rsx!` instead.")] #[proc_macro] pub fn render(tokens: TokenStream) -> TokenStream { rsx(tokens) } -/// * Makes the compiler prefer an `UpperCamelCase` function identifier. +/// * Makes the compiler allow an `UpperCamelCase` function identifier. /// * Seamlessly creates a props struct if there's more than 1 parameter in the function. /// * Verifies the validity of your component. /// @@ -55,6 +53,7 @@ pub fn render(tokens: TokenStream) -> TokenStream { /// /// * Without props: /// ```rust +/// # use dioxus::prelude::*; /// #[component] /// fn Greet() -> Element { /// rsx! { "hello, someone" } @@ -63,19 +62,20 @@ pub fn render(tokens: TokenStream) -> TokenStream { /// /// * With props: /// ```rust +/// # use dioxus::prelude::*; /// #[component] /// fn Greet(person: String) -> Element { /// rsx! { "hello, " {person} } /// } -/// -/// // is roughly equivalent to -/// +/// ``` +/// Which is roughly equivalent to: +/// ```rust +/// # use dioxus::prelude::*; /// #[derive(PartialEq, Clone, Props)] /// struct GreetProps { /// person: String, /// } /// -/// #[component] /// fn Greet(GreetProps { person }: GreetProps) -> Element { /// rsx! { "hello, " {person} } /// } @@ -87,32 +87,7 @@ pub fn component(_args: TokenStream, input: TokenStream) -> TokenStream { .into() } -/// Derive props for a component within the component definition. -/// -/// This macro provides a simple transformation from `Scope<{}>` to `Scope

`, -/// removing some boilerplate when defining props. -/// -/// You don't *need* to use this macro at all, but it can be helpful in cases where -/// you would be repeating a lot of the usual Rust boilerplate. -/// -/// # Example -/// ```rust,ignore -/// #[inline_props] -/// fn app(bob: String) -> Element { -/// rsx! { "hello, {bob}") } -/// } -/// -/// // is equivalent to -/// -/// #[derive(PartialEq, Props)] -/// struct AppProps { -/// bob: String, -/// } -/// -/// fn app(props: AppProps) -> Element { -/// rsx! { "hello, {bob}") } -/// } -/// ``` +/// This macro has been deprecated in favor of [`component`]. #[proc_macro_attribute] #[deprecated(note = "Use `#[component]` instead.")] pub fn inline_props(args: TokenStream, input: TokenStream) -> TokenStream { From 19e7a3ee7f47b4251e1af1e4b64eaf7a3f68181b Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 8 Aug 2024 09:41:48 -0700 Subject: [PATCH 14/15] revert changes outside of core macro --- examples/rsx_usage.rs | 24 ++++++++++++++++++++++++ packages/core/src/error_boundary.rs | 6 +++--- packages/core/src/virtual_dom.rs | 8 ++++---- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index c3be8e9932..361a3839b1 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -197,6 +197,12 @@ fn app() -> Element { Label { text: "hello geneirc world!" } Label { text: 99.9 } + // Lowercase components work too, as long as they are access using a path + baller::lowercase_component {} + + // For in-scope lowercase components, use the `self` keyword + self::lowercase_helper {} + // helper functions // Anything that implements IntoVnode can be dropped directly into Rsx {helper("hello world!")} @@ -223,6 +229,16 @@ fn helper(text: &str) -> Element { } } +// no_case_check disables PascalCase checking if you *really* want a snake_case component. +// This will likely be deprecated/removed in a future update that will introduce a more polished linting system, +// something like Clippy. +#[component(no_case_check)] +fn lowercase_helper() -> Element { + rsx! { + "asd" + } +} + mod baller { use super::*; @@ -231,6 +247,14 @@ mod baller { pub fn Baller() -> Element { rsx! { "ballin'" } } + + // no_case_check disables PascalCase checking if you *really* want a snake_case component. + // This will likely be deprecated/removed in a future update that will introduce a more polished linting system, + // something like Clippy. + #[component(no_case_check)] + pub fn lowercase_component() -> Element { + rsx! { "look ma, no uppercase" } + } } /// Documention for this component is visible within the rsx macro diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index dbb3440dc6..88359dd1ca 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -154,7 +154,7 @@ impl ErrorBoundary { /// /// ```rust, ignore /// #[component] -/// fn App(count: String) -> Element { +/// fn app(count: String) -> Element { /// let id: i32 = count.parse().throw()?; /// /// rsx! { @@ -179,7 +179,7 @@ pub trait Throw: Sized { /// /// ```rust, ignore /// #[component] - /// fn App(count: String) -> Element { + /// fn app( count: String) -> Element { /// let id: i32 = count.parse().throw()?; /// /// rsx! { @@ -202,7 +202,7 @@ pub trait Throw: Sized { /// /// ```rust, ignore /// #[component] - /// fn App(count: String) -> Element { + /// fn app( count: String) -> Element { /// let id: i32 = count.parse().throw()?; /// /// rsx! { diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 32a6f02476..f223118eca 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -58,16 +58,16 @@ use tracing::instrument; /// static ROUTES: &str = ""; /// /// #[component] -/// fn App(props: AppProps) -> Element { +/// fn app(cx: AppProps) -> Element { /// rsx!( /// NavBar { routes: ROUTES } -/// Title { "{props.title}" } +/// Title { "{cx.title}" } /// Footer {} /// ) /// } /// /// #[component] -/// fn NavBar(routes: &'static str) -> Element { +/// fn NavBar( routes: &'static str) -> Element { /// rsx! { /// div { "Routes: {routes}" } /// } @@ -130,7 +130,7 @@ use tracing::instrument; /// Putting everything together, you can build an event loop around Dioxus by using the methods outlined above. /// ```rust, ignore /// #[component] -/// fn App() -> Element { +/// fn app() -> Element { /// rsx! { /// div { "Hello World" } /// } From 54680186fa8d1c1e3a0bf0bd5c4c8fe5700a1746 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 8 Aug 2024 10:28:26 -0700 Subject: [PATCH 15/15] restore doc changes --- packages/core-macro/src/component.rs | 183 ++++++++++++++++++++++++++- packages/core-macro/src/lib.rs | 1 + packages/core-macro/src/utils.rs | 129 +++++++++++++++++++ 3 files changed, 306 insertions(+), 7 deletions(-) create mode 100644 packages/core-macro/src/utils.rs diff --git a/packages/core-macro/src/component.rs b/packages/core-macro/src/component.rs index 9e85682900..fb8cf0c204 100644 --- a/packages/core-macro/src/component.rs +++ b/packages/core-macro/src/component.rs @@ -83,13 +83,15 @@ impl ComponentBody { let Generics { where_clause, .. } = generics; let (_, impl_generics, _) = generics.split_for_impl(); - let generics_turbofish = ty_generics.as_turbofish(); + let generics_turbofish = impl_generics.as_turbofish(); // We generate a struct with the same name as the component but called `Props` let struct_ident = Ident::new(&format!("{fn_ident}Props"), fn_ident.span()); // We pull in the field names from the original function signature, but need to strip off the mutability - let struct_field_names = inputs.iter().filter_map(rebind_mutability); + let struct_field_names = inputs.iter().map(rebind_mutability); + + let props_docs = self.props_docs(inputs.iter().collect()); let inlined_props_argument = if inputs.is_empty() { quote! {} @@ -100,6 +102,7 @@ impl ComponentBody { // The extra nest is for the snake case warning to kick back in parse_quote! { #(#attrs)* + #(#props_docs)* #[allow(non_snake_case)] #vis fn #fn_ident #generics (#inlined_props_argument) #fn_output #where_clause { { @@ -140,6 +143,77 @@ impl ComponentBody { } } + /// 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; + + if inputs.is_empty() { + return Vec::new(); + } + + let arg_docs = inputs + .iter() + .filter_map(|f| build_doc_fields(f)) + .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 in arg_docs { + let DocField { + arg_name, + arg_type, + deprecation, + input_arg_doc, + } = arg; + + let arg_name = strip_pat_mutability(arg_name).to_token_stream().to_string(); + let arg_type = crate::utils::format_type_string(arg_type); + + let input_arg_doc = keep_up_to_n_consecutive_chars(input_arg_doc.trim(), 2, '\n') + .replace("\n\n", "

"); + 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 is_explicit_props_ident(&self) -> bool { if let Some(FnArg::Typed(PatType { pat, .. })) = self.item_fn.sig.inputs.first() { if let Pat::Ident(ident) = pat.as_ref() { @@ -186,6 +260,65 @@ impl ComponentBody { } } +struct DocField<'a> { + arg_name: &'a Pat, + arg_type: &'a Type, + 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 Meta::NameValue(meta_name_value) = &attr.meta else { + return None; + }; + + let Expr::Lit(doc_lit) = &meta_name_value.value else { + return None; + }; + + let Lit::Str(doc_lit_str) = &doc_lit.lit else { + return None; + }; + + Some(doc_lit_str.value()) + }) + .fold(String::new(), |mut doc, next_doc_line| { + doc.push('\n'); + doc.push_str(&next_doc_line); + doc + }); + + Some(DocField { + arg_name: &pt.pat, + arg_type: &pt.ty, + deprecation: pt.attrs.iter().find_map(|attr| { + if !attr.path().is_ident("deprecated") { + return None; + } + + let res = crate::utils::DeprecatedAttribute::from_meta(&attr.meta); + + match res { + Err(e) => panic!("{}", e.to_string()), + Ok(v) => Some(v), + } + }), + input_arg_doc: arg_doc, + }) +} + fn validate_component_fn(item_fn: &ItemFn) -> Result<()> { // Do some validation.... // 1. Ensure the component returns *something* @@ -266,20 +399,56 @@ fn make_prop_struct_field(f: &FnArg, vis: &Visibility) -> TokenStream { } } -fn rebind_mutability(f: &FnArg) -> Option { +fn rebind_mutability(f: &FnArg) -> TokenStream { // There's no receivers (&self) allowed in the component body let FnArg::Typed(pt) = f else { unreachable!() }; - let pat = &pt.pat; + let immutable = strip_pat_mutability(&pt.pat); - let mut pat = pat.clone(); + quote!(mut #immutable) +} +fn strip_pat_mutability(pat: &Pat) -> Pat { + let mut pat = pat.clone(); // rip off mutability, but still write it out eventually - if let Pat::Ident(ref mut pat_ident) = pat.as_mut() { + if let Pat::Ident(ref mut pat_ident) = &mut pat { pat_ident.mutability = None; } - Some(quote!(mut #pat)) + 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 } /// Takes a function and returns a clone of it where an `UpperCamelCase` identifier is allowed by the compiler. diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 7b2c844313..3ff19d9be2 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -9,6 +9,7 @@ use syn::parse_macro_input; mod component; mod props; +mod utils; use dioxus_rsx as rsx; diff --git a/packages/core-macro/src/utils.rs b/packages/core-macro/src/utils.rs new file mode 100644 index 0000000000..918c8d2935 --- /dev/null +++ b/packages/core-macro/src/utils.rs @@ -0,0 +1,129 @@ +use quote::ToTokens; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{parse_quote, Expr, Lit, Meta, Token, Type}; + +const FORMATTED_TYPE_START: &str = "static TY_AFTER_HERE:"; +const FORMATTED_TYPE_END: &str = "= unreachable!();"; + +/// Attempts to convert the given literal to a string. +/// Converts ints and floats to their base 10 counterparts. +/// +/// Returns `None` if the literal is [`Lit::Verbatim`] or if the literal is [`Lit::ByteStr`] +/// and the byte string could not be converted to UTF-8. +pub fn lit_to_string(lit: Lit) -> Option { + match lit { + Lit::Str(l) => Some(l.value()), + Lit::ByteStr(l) => String::from_utf8(l.value()).ok(), + Lit::Byte(l) => Some(String::from(l.value() as char)), + Lit::Char(l) => Some(l.value().to_string()), + Lit::Int(l) => Some(l.base10_digits().to_string()), + Lit::Float(l) => Some(l.base10_digits().to_string()), + Lit::Bool(l) => Some(l.value().to_string()), + Lit::Verbatim(_) => None, + _ => None, + } +} + +pub fn format_type_string(ty: &Type) -> String { + let ty_unformatted = ty.into_token_stream().to_string(); + let ty_unformatted = ty_unformatted.trim(); + + // This should always be valid syntax. + // Not Rust code, but syntax, which is the only thing that `syn` cares about. + let Ok(file_unformatted) = syn::parse_file(&format!( + "{FORMATTED_TYPE_START}{ty_unformatted}{FORMATTED_TYPE_END}" + )) else { + return ty_unformatted.to_string(); + }; + + let file_formatted = prettyplease::unparse(&file_unformatted); + + let file_trimmed = file_formatted.trim(); + let start_removed = file_trimmed.trim_start_matches(FORMATTED_TYPE_START); + let end_removed = start_removed.trim_end_matches(FORMATTED_TYPE_END); + let ty_formatted = end_removed.trim(); + + ty_formatted.to_string() +} + +/// Represents the `#[deprecated]` attribute. +/// +/// You can use the [`DeprecatedAttribute::from_meta`] function to try to parse an attribute to this struct. +#[derive(Default)] +pub struct DeprecatedAttribute { + pub since: Option, + pub note: Option, +} + +impl DeprecatedAttribute { + /// Returns `None` if the given attribute was not a valid form of the `#[deprecated]` attribute. + pub fn from_meta(meta: &Meta) -> syn::Result { + if meta.path() != &parse_quote!(deprecated) { + return Err(syn::Error::new( + meta.span(), + "attribute path is not `deprecated`", + )); + } + + match &meta { + Meta::Path(_) => Ok(Self::default()), + Meta::NameValue(name_value) => { + let Expr::Lit(expr_lit) = &name_value.value else { + return Err(syn::Error::new( + name_value.span(), + "literal in `deprecated` value must be a string", + )); + }; + + Ok(Self { + since: None, + note: lit_to_string(expr_lit.lit.clone()).map(|s| s.trim().to_string()), + }) + } + Meta::List(list) => { + let parsed = list.parse_args::()?; + + Ok(Self { + since: parsed.since.map(|s| s.trim().to_string()), + note: parsed.note.map(|s| s.trim().to_string()), + }) + } + } + } +} + +mod kw { + use syn::custom_keyword; + custom_keyword!(since); + custom_keyword!(note); +} + +struct DeprecatedAttributeArgsParser { + since: Option, + note: Option, +} + +impl Parse for DeprecatedAttributeArgsParser { + fn parse(input: ParseStream) -> syn::Result { + let mut since: Option = None; + let mut note: Option = None; + + if input.peek(kw::since) { + input.parse::()?; + input.parse::()?; + + since = lit_to_string(input.parse()?); + } + + if input.peek(Token![,]) && input.peek2(kw::note) { + input.parse::()?; + input.parse::()?; + input.parse::()?; + + note = lit_to_string(input.parse()?); + } + + Ok(Self { since, note }) + } +}