Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improved rust-analyzer support in #[component] macro? #2075

Merged
merged 10 commits into from
Nov 28, 2023
33 changes: 24 additions & 9 deletions leptos_macro/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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();

Expand Down Expand Up @@ -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 || {
Expand Down Expand Up @@ -367,7 +365,6 @@ impl ToTokens for Model {
.collect::<TokenStream>();

let body = quote! {
#body
#destructure_props
#tracing_span_expr
#component
Expand Down Expand Up @@ -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<Attribute>,
vis: Visibility,
sig: Signature,
body: TokenStream,
pub attrs: Vec<Attribute>,
pub vis: Visibility,
pub sig: Signature,
pub body: TokenStream,
}

impl Parse for DummyModel {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
83 changes: 51 additions & 32 deletions leptos_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -598,21 +600,30 @@ pub fn component(args: proc_macro::TokenStream, s: TokenStream) -> TokenStream {
false
};

let parse_result = syn::parse::<component::Model>(s.clone());
let mut dummy = syn::parse::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(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
Expand Down Expand Up @@ -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::<DummyModel>(s.clone());
let parse_result = syn::parse::<component::Model>(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`.
Expand Down
9 changes: 1 addition & 8 deletions leptos_macro/tests/ui/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,32 @@ 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;
}

#[component]
fn optional_and_strip_option(
,
#[prop(optional, strip_option)] conflicting: bool,
) -> impl IntoView {
_ = conflicting;
}

#[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;
Expand Down
72 changes: 22 additions & 50 deletions leptos_macro/tests/ui/component.stderr
Original file line number Diff line number Diff line change
@@ -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
|
Expand All @@ -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,
| ^
Loading