From 18a92bbfd89d74295c635bb2b0a64b4a1358d6ac Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Mon, 27 Nov 2023 19:37:43 -0500 Subject: [PATCH] fix: improved rust-analyzer support in `#[component]` macro (#2075) --- leptos_macro/src/component.rs | 33 +++++++--- leptos_macro/src/lib.rs | 83 ++++++++++++++++---------- leptos_macro/tests/ui/component.rs | 9 +-- leptos_macro/tests/ui/component.stderr | 72 +++++++--------------- 4 files changed, 98 insertions(+), 99 deletions(-) diff --git a/leptos_macro/src/component.rs b/leptos_macro/src/component.rs index 496a66104e..5aa779db04 100644 --- a/leptos_macro/src/component.rs +++ b/leptos_macro/src/component.rs @@ -118,8 +118,6 @@ impl ToTokens for Model { let no_props = props.is_empty(); - let mut body = body.to_owned(); - // check for components that end ; if !is_transparent { let ends_semi = @@ -139,7 +137,6 @@ impl ToTokens for Model { } } - body.sig.ident = format_ident!("__{}", body.sig.ident); #[allow(clippy::redundant_clone)] // false positive let body_name = body.sig.ident.clone(); @@ -234,6 +231,7 @@ impl ToTokens for Model { quote! {} }; + let body_name = unmodified_fn_name_from_fn_name(&body_name); let body_expr = if *is_island { quote! { ::leptos::SharedContext::with_hydration(move || { @@ -367,7 +365,6 @@ impl ToTokens for Model { .collect::(); let body = quote! { - #body #destructure_props #tracing_span_expr #component @@ -547,10 +544,10 @@ impl Model { /// used to improve IDEs and rust-analyzer's auto-completion behavior in case /// of a syntax error. pub struct DummyModel { - attrs: Vec, - vis: Visibility, - sig: Signature, - body: TokenStream, + pub attrs: Vec, + pub vis: Visibility, + pub sig: Signature, + pub body: TokenStream, } impl Parse for DummyModel { @@ -588,7 +585,21 @@ impl ToTokens for DummyModel { let mut sig = sig.clone(); sig.inputs.iter_mut().for_each(|arg| { if let FnArg::Typed(ty) = arg { - ty.attrs.clear(); + ty.attrs.retain(|attr| match &attr.meta { + Meta::List(list) => list + .path + .segments + .first() + .map(|n| n.ident != "prop") + .unwrap_or(true), + Meta::NameValue(name_value) => name_value + .path + .segments + .first() + .map(|n| n.ident != "doc") + .unwrap_or(true), + _ => true, + }); } }); sig @@ -1162,3 +1173,7 @@ fn is_valid_into_view_return_type(ty: &ReturnType) -> bool { .iter() .any(|test| ty == test) } + +pub fn unmodified_fn_name_from_fn_name(ident: &Ident) -> Ident { + Ident::new(&format!("__{ident}"), ident.span()) +} diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs index a9cd6e259c..46b7526ebf 100644 --- a/leptos_macro/src/lib.rs +++ b/leptos_macro/src/lib.rs @@ -4,11 +4,12 @@ #[macro_use] extern crate proc_macro_error; +use component::DummyModel; use proc_macro::TokenStream; use proc_macro2::{Span, TokenTree}; use quote::ToTokens; use rstml::{node::KeyedAttribute, parse}; -use syn::parse_macro_input; +use syn::{parse_macro_input, spanned::Spanned, token::Pub, Visibility}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum Mode { @@ -31,6 +32,7 @@ impl Default for Mode { mod params; mod view; +use crate::component::unmodified_fn_name_from_fn_name; use view::{client_template::render_template, render_view}; mod component; mod server; @@ -598,21 +600,30 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { false }; - let parse_result = syn::parse::(s.clone()); + let mut dummy = syn::parse::(s.clone()); + let parse_result = syn::parse::(s); - if let Ok(model) = parse_result { - model - .is_transparent(is_transparent) - .into_token_stream() - .into() + if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) { + let expanded = model.is_transparent(is_transparent).into_token_stream(); + unexpanded.sig.ident = + unmodified_fn_name_from_fn_name(&unexpanded.sig.ident); + quote! { + #expanded + #[doc(hidden)] + #[allow(non_snake_case, dead_code, clippy::too_many_arguments)] + #unexpanded + } + } else if let Ok(mut dummy) = dummy { + dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident); + quote! { + #[doc(hidden)] + #[allow(non_snake_case, dead_code, clippy::too_many_arguments)] + #dummy + } } else { - // When the input syntax is invalid, e.g. while typing, we let - // the dummy model output tokens similar to the input, which improves - // IDEs and rust-analyzer's auto-complete capabilities. - parse_macro_input!(s as component::DummyModel) - .into_token_stream() - .into() + quote! {} } + .into() } /// Defines a component as an interactive island when you are using the @@ -688,28 +699,36 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { /// ``` #[proc_macro_error::proc_macro_error] #[proc_macro_attribute] -pub fn island(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { - let is_transparent = if !args.is_empty() { - let transparent = parse_macro_input!(args as syn::Ident); +pub fn island(_args: proc_macro::TokenStream, s: TokenStream) -> TokenStream { + let mut dummy = syn::parse::(s.clone()); + let parse_result = syn::parse::(s); - if transparent != "transparent" { - abort!( - transparent, - "only `transparent` is supported"; - help = "try `#[island(transparent)]` or `#[island]`" - ); + if let (Ok(ref mut unexpanded), Ok(model)) = (&mut dummy, parse_result) { + let expanded = model.is_island().into_token_stream(); + if !matches!(unexpanded.vis, Visibility::Public(_)) { + unexpanded.vis = Visibility::Public(Pub { + span: unexpanded.vis.span(), + }) + } + unexpanded.sig.ident = + unmodified_fn_name_from_fn_name(&unexpanded.sig.ident); + quote! { + #expanded + #[doc(hidden)] + #[allow(non_snake_case, dead_code, clippy::too_many_arguments)] + #unexpanded + } + } else if let Ok(mut dummy) = dummy { + dummy.sig.ident = unmodified_fn_name_from_fn_name(&dummy.sig.ident); + quote! { + #[doc(hidden)] + #[allow(non_snake_case, dead_code, clippy::too_many_arguments)] + #dummy } - - true } else { - false - }; - - parse_macro_input!(s as component::Model) - .is_transparent(is_transparent) - .is_island() - .into_token_stream() - .into() + quote! {} + } + .into() } /// Annotates a struct so that it can be used with your Component as a `slot`. diff --git a/leptos_macro/tests/ui/component.rs b/leptos_macro/tests/ui/component.rs index d5ecf2cc3c..262bcc0e77 100644 --- a/leptos_macro/tests/ui/component.rs +++ b/leptos_macro/tests/ui/component.rs @@ -13,7 +13,6 @@ fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView { #[component] fn optional_and_optional_no_strip( - , #[prop(optional, optional_no_strip)] conflicting: bool, ) -> impl IntoView { _ = conflicting; @@ -21,7 +20,6 @@ fn optional_and_optional_no_strip( #[component] fn optional_and_strip_option( - , #[prop(optional, strip_option)] conflicting: bool, ) -> impl IntoView { _ = conflicting; @@ -29,23 +27,18 @@ fn optional_and_strip_option( #[component] fn optional_no_strip_and_strip_option( - , #[prop(optional_no_strip, strip_option)] conflicting: bool, ) -> impl IntoView { _ = conflicting; } #[component] -fn default_without_value( - , - #[prop(default)] default: bool, -) -> impl IntoView { +fn default_without_value(#[prop(default)] default: bool) -> impl IntoView { _ = default; } #[component] fn default_with_invalid_value( - , #[prop(default= |)] default: bool, ) -> impl IntoView { _ = default; diff --git a/leptos_macro/tests/ui/component.stderr b/leptos_macro/tests/ui/component.stderr index 37563cdae2..7a425f79ba 100644 --- a/leptos_macro/tests/ui/component.stderr +++ b/leptos_macro/tests/ui/component.stderr @@ -1,33 +1,3 @@ -error: expected parameter name, found `,` - --> tests/ui/component.rs:16:5 - | -16 | , - | ^ expected parameter name - -error: expected parameter name, found `,` - --> tests/ui/component.rs:24:5 - | -24 | , - | ^ expected parameter name - -error: expected parameter name, found `,` - --> tests/ui/component.rs:32:5 - | -32 | , - | ^ expected parameter name - -error: expected parameter name, found `,` - --> tests/ui/component.rs:40:5 - | -40 | , - | ^ expected parameter name - -error: expected parameter name, found `,` - --> tests/ui/component.rs:48:5 - | -48 | , - | ^ expected parameter name - error: return type is incorrect --> tests/ui/component.rs:4:1 | @@ -50,32 +20,34 @@ error: supported fields are `optional`, `optional_no_strip`, `strip_option`, `de 10 | fn unknown_prop_option(#[prop(hello)] test: bool) -> impl IntoView { | ^^^^^ -error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` - --> tests/ui/component.rs:16:5 +error: `optional` conflicts with mutually exclusive `optional_no_strip` + --> tests/ui/component.rs:16:12 | -16 | , - | ^ +16 | #[prop(optional, optional_no_strip)] conflicting: bool, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` - --> tests/ui/component.rs:24:5 +error: `optional` conflicts with mutually exclusive `strip_option` + --> tests/ui/component.rs:23:12 | -24 | , - | ^ +23 | #[prop(optional, strip_option)] conflicting: bool, + | ^^^^^^^^^^^^^^^^^^^^^^ -error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` - --> tests/ui/component.rs:32:5 +error: `optional_no_strip` conflicts with mutually exclusive `strip_option` + --> tests/ui/component.rs:30:12 | -32 | , - | ^ +30 | #[prop(optional_no_strip, strip_option)] conflicting: bool, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` - --> tests/ui/component.rs:40:5 +error: unexpected end of input, expected assignment `=` + --> tests/ui/component.rs:36:40 | -40 | , - | ^ +36 | fn default_without_value(#[prop(default)] default: bool) -> impl IntoView { + | ^ + +error: unexpected end of input, expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` -error: expected one of: identifier, `::`, `<`, `_`, literal, `const`, `ref`, `mut`, `&`, parentheses, square brackets, `..`, `const` - --> tests/ui/component.rs:48:5 + = help: try `#[prop(default=5 * 10)]` + --> tests/ui/component.rs:42:22 | -48 | , - | ^ +42 | #[prop(default= |)] default: bool, + | ^