From 6866d0d3beb66680365d5ab8f27128c3848cd7e8 Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Wed, 1 Nov 2023 11:04:07 +0100 Subject: [PATCH] Port yew-autoprops to yew-macro --- packages/yew-macro/src/autoprops.rs | 218 ++++++++++++++++++ packages/yew-macro/src/function_component.rs | 34 ++- packages/yew-macro/src/lib.rs | 12 + .../function_component_attr/autoprops-fail.rs | 66 ++++++ .../autoprops-fail.stderr | 83 +++++++ .../function_component_attr/autoprops-pass.rs | 102 ++++++++ .../bad-props-param-fail.stderr | 4 +- .../multiple-param-fail.stderr | 8 +- packages/yew/src/functional/mod.rs | 6 +- 9 files changed, 521 insertions(+), 12 deletions(-) create mode 100644 packages/yew-macro/src/autoprops.rs create mode 100644 packages/yew-macro/tests/function_component_attr/autoprops-fail.rs create mode 100644 packages/yew-macro/tests/function_component_attr/autoprops-fail.stderr create mode 100644 packages/yew-macro/tests/function_component_attr/autoprops-pass.rs diff --git a/packages/yew-macro/src/autoprops.rs b/packages/yew-macro/src/autoprops.rs new file mode 100644 index 00000000000..5c0d21c14cb --- /dev/null +++ b/packages/yew-macro/src/autoprops.rs @@ -0,0 +1,218 @@ +use quote::quote; + +use crate::function_component::FunctionComponentName; + +#[derive(Clone)] +pub struct Autoprops { + item_fn: syn::ItemFn, + properties_name: syn::Ident, +} + +impl syn::parse::Parse for Autoprops { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let parsed: syn::Item = input.parse()?; + + let item_fn = match parsed { + syn::Item::Fn(m) => m, + item => { + return Err(syn::Error::new_spanned( + item, + "`autoprops` attribute can only be applied to functions", + )) + } + }; + + let syn::ItemFn { attrs, sig, .. } = &item_fn; + + let mut component_name = item_fn.sig.ident.clone(); + + attrs + .iter() + .find(|attr| { + match &attr.meta { + syn::Meta::Path(path) => { + if let Some(last_segment) = path.segments.last() { + if last_segment.ident == "function_component" { + return true; + } + } + } + syn::Meta::List(syn::MetaList { path, tokens, .. }) => { + if let Some(last_segment) = path.segments.last() { + if last_segment.ident == "function_component" { + if let Ok(attr) = + syn::parse2::(tokens.clone()) + { + if let Some(name) = attr.component_name { + component_name = name; + } + } + return true; + } + } + } + _ => {} + } + false + }) + .ok_or_else(|| { + syn::Error::new_spanned( + sig, + "could not find #[function_component] attribute in function declaration \ + (#[autoprops] must be place *before* #[function_component])", + ) + })?; + + for input in &sig.inputs { + match input { + syn::FnArg::Typed(syn::PatType { pat, .. }) => match pat.as_ref() { + syn::Pat::Wild(wild) => { + return Err(syn::Error::new_spanned( + wild, + "cannot use `_` as field name", + )); + } + _ => {} + }, + _ => {} + } + } + + let properties_name = syn::Ident::new( + &format!("{}Props", component_name), + proc_macro2::Span::call_site(), + ); + + Ok(Self { + properties_name, + item_fn, + }) + } +} + +impl Autoprops { + pub fn apply_args(&mut self, args: AutopropsArgs) { + if let Some(name) = args.properties_name { + self.properties_name = name; + } + } + + fn print_function_component(&self) -> proc_macro2::TokenStream { + let properties_name = &self.properties_name; + let syn::ItemFn { + attrs, + vis, + sig, + block, + } = &self.item_fn; + + let fn_name = &sig.ident; + let (impl_generics, type_generics, where_clause) = sig.generics.split_for_impl(); + let inputs = if sig.inputs.is_empty() { + quote! { (): &() } + } else { + // NOTE: function components currently don't accept receivers, we're just passing the + // information to the next macro to fail and give its own error message + let receivers = sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Receiver(receiver) => Some(receiver), + _ => None, + }) + .collect::>(); + let args = sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Typed(syn::PatType { pat, .. }) => Some(quote! { #pat }), + _ => None, + }) + .collect::>(); + quote! { #(#receivers,)* #properties_name { #(#args),* }: &#properties_name #type_generics } + }; + let clones = sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Typed(syn::PatType { pat, ty, .. }) + if !matches!(**ty, syn::Type::Reference(_)) => + { + Some(quote! { let #pat = ::std::clone::Clone::clone(#pat); }) + } + _ => None, + }) + .collect::>(); + + quote! { + #(#attrs)* + #vis fn #fn_name #impl_generics (#inputs) -> ::yew::Html #where_clause { + #(#clones)* + #block + } + } + } + + fn print_properties_struct(&self) -> proc_macro2::TokenStream { + let properties_name = &self.properties_name; + let syn::ItemFn { vis, sig, .. } = &self.item_fn; + + if sig.inputs.is_empty() { + return quote! {}; + } + + let (impl_generics, _type_generics, where_clause) = sig.generics.split_for_impl(); + let fields = sig + .inputs + .iter() + .filter_map(|arg| match arg { + syn::FnArg::Typed(syn::PatType { attrs, pat, ty, .. }) => match ty.as_ref() { + syn::Type::Reference(syn::TypeReference { elem, .. }) => { + Some(quote! { #(#attrs)* #pat: #elem, }) + } + _ => Some(quote! { #(#attrs)* #pat: #ty, }), + }, + _ => None, + }) + .collect::>(); + + quote! { + #[derive(::yew::Properties, ::std::cmp::PartialEq)] + #vis struct #properties_name #impl_generics #where_clause { + #(#fields)* + } + } + } +} + +impl quote::ToTokens for Autoprops { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let function_component = self.print_function_component(); + let properties_struct = self.print_properties_struct(); + + tokens.extend(quote! { + #function_component + #properties_struct + }) + } +} + +pub struct AutopropsArgs { + pub properties_name: Option, +} + +impl syn::parse::Parse for AutopropsArgs { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + if input.is_empty() { + return Ok(Self { + properties_name: None, + }); + } + + let properties_name = input.parse()?; + + Ok(Self { + properties_name: Some(properties_name), + }) + } +} diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index d17b5b65155..aba7944db31 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -47,6 +47,32 @@ impl Parse for FunctionComponent { block, } = func; + if let Some(_attr) = attrs.iter().find(|attr| { + match &attr.meta { + syn::Meta::Path(path) => { + if let Some(last_segment) = path.segments.last() { + if last_segment.ident == "autoprops" { + return true; + } + } + } + syn::Meta::List(syn::MetaList { path, .. }) => { + if let Some(last_segment) = path.segments.last() { + if last_segment.ident == "autoprops" { + return true; + } + } + } + _ => {} + } + false + }) { + return Err(syn::Error::new_spanned( + sig, + "#[autoprops] must be placed *before* #[function_component]", + )); + } + if sig.generics.lifetimes().next().is_some() { return Err(syn::Error::new_spanned( sig.generics, @@ -111,7 +137,8 @@ impl Parse for FunctionComponent { } ty => { let msg = format!( - "expected a reference to a `Properties` type (try: `&{}`)", + "expected a reference to a `Properties` type \ + (try: `&{}` or use #[autoprops])", ty.to_token_stream() ); return Err(syn::Error::new_spanned(ty, msg)); @@ -133,7 +160,8 @@ impl Parse for FunctionComponent { let params: TokenStream = inputs.map(|it| it.to_token_stream()).collect(); return Err(syn::Error::new_spanned( params, - "function components can accept at most one parameter for the props", + "function components can accept at most one parameter for the props, \ + maybe you wanted to use #[autoprops]?", )); } @@ -393,7 +421,7 @@ impl FunctionComponent { } pub struct FunctionComponentName { - component_name: Option, + pub component_name: Option, } impl Parse for FunctionComponentName { diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs index 5d7216d84a0..e72d42c37a2 100644 --- a/packages/yew-macro/src/lib.rs +++ b/packages/yew-macro/src/lib.rs @@ -48,6 +48,7 @@ //! //! Please refer to [https://github.com/yewstack/yew](https://github.com/yewstack/yew) for how to set this up. +mod autoprops; mod classes; mod derive_props; mod function_component; @@ -58,6 +59,7 @@ mod stringify; mod use_prepared_state; mod use_transitive_state; +use autoprops::{Autoprops, AutopropsArgs}; use derive_props::DerivePropsInput; use function_component::{function_component_impl, FunctionComponent, FunctionComponentName}; use hook::{hook_impl, HookFn}; @@ -148,6 +150,16 @@ pub fn function_component(attr: TokenStream, item: TokenStream) -> proc_macro::T .into() } +#[proc_macro_error::proc_macro_error] +#[proc_macro_attribute] +pub fn autoprops(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream { + let mut autoprops = parse_macro_input!(item as Autoprops); + let args = parse_macro_input!(attr as AutopropsArgs); + autoprops.apply_args(args); + + TokenStream::from(autoprops.into_token_stream()) +} + #[proc_macro_error::proc_macro_error] #[proc_macro_attribute] pub fn hook(attr: TokenStream, item: TokenStream) -> proc_macro::TokenStream { diff --git a/packages/yew-macro/tests/function_component_attr/autoprops-fail.rs b/packages/yew-macro/tests/function_component_attr/autoprops-fail.rs new file mode 100644 index 00000000000..5bc8be54aad --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/autoprops-fail.rs @@ -0,0 +1,66 @@ +use yew::prelude::*; + +#[autoprops] +#[function_component] +fn CantAcceptReceiver(&self, b: bool) -> Html { + html! { +

{b}

+ } +} + +#[autoprops] +fn not_a_function_component(b: bool) -> Html { + html! { +

{b}

+ } +} + +#[function_component(WrongAttrsOrder)] +#[autoprops] +fn wrong_attrs_order(b: bool) -> Html { + html! { +

{b}

+ } +} + +#[autoprops] +#[function_component(let)] +fn BadFunctionComponent(b: bool) -> Html { + html! { +

{b}

+ } +} + +#[derive(PartialEq)] +struct NotClonable(u32); + +#[autoprops] +#[function_component] +fn TypeIsNotClone(stuff: NotClonable) -> Html { + drop(stuff); + html! { +

+ } +} + +#[derive(Clone)] +struct NotPartialEq(u32); + +#[autoprops] +#[function_component] +fn TypeIsNotPartialEq(stuff: NotPartialEq) -> Html { + drop(stuff); + html! { +

+ } +} + +#[autoprops] +#[function_component] +fn InvalidFieldName(_: u32) -> Html { + html! { +

+ } +} + +fn main() {} diff --git a/packages/yew-macro/tests/function_component_attr/autoprops-fail.stderr b/packages/yew-macro/tests/function_component_attr/autoprops-fail.stderr new file mode 100644 index 00000000000..df2a783c24a --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/autoprops-fail.stderr @@ -0,0 +1,83 @@ +error: function components can't accept a receiver + --> tests/function_component_attr/autoprops-fail.rs:5:23 + | +5 | fn CantAcceptReceiver(&self, b: bool) -> Html { + | ^^^^^ + +error: could not find #[function_component] attribute in function declaration (#[autoprops] must be place *before* #[function_component]) + --> tests/function_component_attr/autoprops-fail.rs:12:1 + | +12 | fn not_a_function_component(b: bool) -> Html { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: #[autoprops] must be placed *before* #[function_component] + --> tests/function_component_attr/autoprops-fail.rs:20:1 + | +20 | fn wrong_attrs_order(b: bool) -> Html { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected identifier, found keyword `let` + --> tests/function_component_attr/autoprops-fail.rs:27:22 + | +27 | #[function_component(let)] + | ^^^ + +error: cannot use `_` as field name + --> tests/function_component_attr/autoprops-fail.rs:60:21 + | +60 | fn InvalidFieldName(_: u32) -> Html { + | ^ + +error[E0277]: the trait bound `NotClonable: Clone` is not satisfied + --> tests/function_component_attr/autoprops-fail.rs:39:19 + | +37 | #[autoprops] + | ------------ required by a bound introduced by this call +38 | #[function_component] +39 | fn TypeIsNotClone(stuff: NotClonable) -> Html { + | ^^^^^ the trait `Clone` is not implemented for `NotClonable` + | +help: consider annotating `NotClonable` with `#[derive(Clone)]` + | +35 | #[derive(Clone)] + | + +error[E0369]: binary operation `==` cannot be applied to type `NotPartialEq` + --> tests/function_component_attr/autoprops-fail.rs:51:23 + | +49 | #[autoprops] + | ------------ in this procedural macro expansion +50 | #[function_component] +51 | fn TypeIsNotPartialEq(stuff: NotPartialEq) -> Html { + | ^^^^^^^^^^^^^^^^^^^ + | +note: an implementation of `PartialEq<_>` might be missing for `NotPartialEq` + --> tests/function_component_attr/autoprops-fail.rs:47:1 + | +47 | struct NotPartialEq(u32); + | ^^^^^^^^^^^^^^^^^^^ must implement `PartialEq<_>` + = note: this error originates in the derive macro `::std::cmp::PartialEq` which comes from the expansion of the attribute macro `autoprops` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `NotPartialEq` with `#[derive(PartialEq)]` + | +47 | #[derive(PartialEq)] + | + +error[E0369]: binary operation `!=` cannot be applied to type `NotPartialEq` + --> tests/function_component_attr/autoprops-fail.rs:51:23 + | +49 | #[autoprops] + | ------------ in this procedural macro expansion +50 | #[function_component] +51 | fn TypeIsNotPartialEq(stuff: NotPartialEq) -> Html { + | ^^^^^^^^^^^^^^^^^^^ + | +note: an implementation of `PartialEq<_>` might be missing for `NotPartialEq` + --> tests/function_component_attr/autoprops-fail.rs:47:1 + | +47 | struct NotPartialEq(u32); + | ^^^^^^^^^^^^^^^^^^^ must implement `PartialEq<_>` + = note: this error originates in the derive macro `::std::cmp::PartialEq` which comes from the expansion of the attribute macro `autoprops` (in Nightly builds, run with -Z macro-backtrace for more info) +help: consider annotating `NotPartialEq` with `#[derive(PartialEq)]` + | +47 | #[derive(PartialEq)] + | diff --git a/packages/yew-macro/tests/function_component_attr/autoprops-pass.rs b/packages/yew-macro/tests/function_component_attr/autoprops-pass.rs new file mode 100644 index 00000000000..06c48fdd7b1 --- /dev/null +++ b/packages/yew-macro/tests/function_component_attr/autoprops-pass.rs @@ -0,0 +1,102 @@ +#![no_implicit_prelude] + +// Shadow primitives +#[allow(non_camel_case_types)] +pub struct bool; +#[allow(non_camel_case_types)] +pub struct char; +#[allow(non_camel_case_types)] +pub struct f32; +#[allow(non_camel_case_types)] +pub struct f64; +#[allow(non_camel_case_types)] +pub struct i128; +#[allow(non_camel_case_types)] +pub struct i16; +#[allow(non_camel_case_types)] +pub struct i32; +#[allow(non_camel_case_types)] +pub struct i64; +#[allow(non_camel_case_types)] +pub struct i8; +#[allow(non_camel_case_types)] +pub struct isize; +#[allow(non_camel_case_types)] +pub struct str; +#[allow(non_camel_case_types)] +pub struct u128; +#[allow(non_camel_case_types)] +pub struct u16; +#[allow(non_camel_case_types)] +pub struct u32; +#[allow(non_camel_case_types)] +pub struct u64; +#[allow(non_camel_case_types)] +pub struct u8; +#[allow(non_camel_case_types)] +pub struct usize; + +#[::yew::autoprops] +#[::yew::function_component] +fn CompUseFnName() -> ::yew::Html +{ + ::yew::html! { +

+ } +} + +#[::yew::autoprops] +#[::yew::function_component(CompNoProperties)] +fn comp_no_properties() -> ::yew::Html +{ + ::yew::html! { +

+ } +} + +#[::yew::autoprops] +#[::yew::function_component(CompNoGenerics)] +fn comp_no_generics(#[prop_or_default] b: ::std::primitive::bool, a: &::yew::AttrValue) -> ::yew::Html +{ + let _: ::std::primitive::bool = b; + let _: &::yew::AttrValue = a; + ::yew::html! { +

+ } +} + +#[::yew::autoprops] +#[::yew::function_component(CompGenerics)] +fn comp_generics(b: T1, a: &T2) -> ::yew::Html +where + T1: ::std::cmp::PartialEq + ::std::clone::Clone, + T2: ::std::cmp::PartialEq, +{ + let _: T1 = b; + let _: &T2 = a; + ::yew::html! { +

+ } +} + +#[::yew::autoprops] +#[::yew::function_component(ConstGenerics)] +fn const_generics(xs: [::std::primitive::u32; N]) -> ::yew::Html { + let _: [::std::primitive::u32; N] = xs; + ::yew::html! { +
+ { N } +
+ } +} + +fn compile_pass() { + ::yew::html! { }; + ::yew::html! { }; + ::yew::html! { }; + ::yew::html! { b=true a="foo" /> }; + + ::yew::html! { xs={[1_u32, 2_u32]} /> }; +} + +fn main() {} diff --git a/packages/yew-macro/tests/function_component_attr/bad-props-param-fail.stderr b/packages/yew-macro/tests/function_component_attr/bad-props-param-fail.stderr index 479b44a6154..21ce368ac29 100644 --- a/packages/yew-macro/tests/function_component_attr/bad-props-param-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/bad-props-param-fail.stderr @@ -1,5 +1,5 @@ -error: expected a reference to a `Properties` type (try: `&Props`) - --> $DIR/bad-props-param-fail.rs:9:16 +error: expected a reference to a `Properties` type (try: `&Props` or use #[autoprops]) + --> tests/function_component_attr/bad-props-param-fail.rs:9:16 | 9 | fn comp(props: Props) -> Html { | ^^^^^ diff --git a/packages/yew-macro/tests/function_component_attr/multiple-param-fail.stderr b/packages/yew-macro/tests/function_component_attr/multiple-param-fail.stderr index ca8bc2ed2da..a214dfe124e 100644 --- a/packages/yew-macro/tests/function_component_attr/multiple-param-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/multiple-param-fail.stderr @@ -1,11 +1,11 @@ -error: function components can accept at most one parameter for the props - --> $DIR/multiple-param-fail.rs:9:24 +error: function components can accept at most one parameter for the props, maybe you wanted to use #[autoprops]? + --> tests/function_component_attr/multiple-param-fail.rs:9:24 | 9 | fn comp(props: &Props, invalid: String) -> Html { | ^^^^^^^^^^^^^^^ -error: function components can accept at most one parameter for the props - --> $DIR/multiple-param-fail.rs:19:25 +error: function components can accept at most one parameter for the props, maybe you wanted to use #[autoprops]? + --> tests/function_component_attr/multiple-param-fail.rs:19:25 | 19 | fn comp3(props: &Props, invalid: String, another_invalid: u32) -> Html { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 6ba9f601f91..5486b786935 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -34,6 +34,8 @@ use crate::Properties; mod hooks; pub use hooks::*; +/// This attribute creates a user-defined hook from a normal Rust function. +pub use yew_macro::hook; /// This attribute creates a function component from a normal Rust function. /// /// Functions with this attribute **must** return `Html` and can optionally take an argument @@ -60,9 +62,7 @@ pub use hooks::*; /// } /// } /// ``` -pub use yew_macro::function_component; -/// This attribute creates a user-defined hook from a normal Rust function. -pub use yew_macro::hook; +pub use yew_macro::{autoprops, function_component}; type ReRender = Rc;