diff --git a/packages/yew-macro/src/function_component.rs b/packages/yew-macro/src/function_component.rs index d2b6c21d340..a2de56d13ec 100644 --- a/packages/yew-macro/src/function_component.rs +++ b/packages/yew-macro/src/function_component.rs @@ -1,10 +1,11 @@ use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote, ToTokens}; +use quote::{quote, ToTokens}; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::token::{Comma, Fn}; use syn::{ - visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, ReturnType, Type, Visibility, + parse_quote_spanned, visit_mut, Attribute, Block, FnArg, Generics, Ident, Item, ItemFn, + ReturnType, Type, Visibility, }; use crate::hook::BodyRewriter; @@ -20,6 +21,8 @@ pub struct FunctionComponent { name: Ident, return_type: Box, fn_token: Fn, + + component_name: Option, } impl Parse for FunctionComponent { @@ -144,10 +147,96 @@ impl Parse for FunctionComponent { name: sig.ident, return_type, fn_token: sig.fn_token, + component_name: None, }) } } +impl FunctionComponent { + /// Filters attributes that should be copied to component definition. + fn filter_attrs_for_component_struct(&self) -> Vec { + self.attrs + .iter() + .filter_map(|m| { + m.path + .get_ident() + .and_then(|ident| match ident.to_string().as_str() { + "doc" | "allow" => Some(m.clone()), + _ => None, + }) + }) + .collect() + } + + /// Filters attributes that should be copied to the component impl block. + fn filter_attrs_for_component_impl(&self) -> Vec { + self.attrs + .iter() + .filter_map(|m| { + m.path + .get_ident() + .and_then(|ident| match ident.to_string().as_str() { + "allow" => Some(m.clone()), + _ => None, + }) + }) + .collect() + } + + fn phantom_generics(&self) -> Punctuated { + self.generics + .type_params() + .map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds + .collect::>() + } + + fn merge_component_name(&mut self, name: FunctionComponentName) -> syn::Result<()> { + if let Some(ref m) = name.component_name { + if m == &self.name { + return Err(syn::Error::new_spanned( + m, + "the component must not have the same name as the function", + )); + } + } + + self.component_name = name.component_name; + + Ok(()) + } + + fn inner_fn_ident(&self) -> Ident { + if self.component_name.is_some() { + self.name.clone() + } else { + Ident::new("inner", Span::mixed_site()) + } + } + + fn component_name(&self) -> Ident { + self.component_name + .clone() + .unwrap_or_else(|| self.name.clone()) + } + + // We need to cast 'static on all generics for into component. + fn create_into_component_generics(&self) -> Generics { + let mut generics = self.generics.clone(); + + let where_clause = generics.make_where_clause(); + for ty_generic in self.generics.type_params() { + let ident = &ty_generic.ident; + let bound = parse_quote_spanned! { ident.span() => + #ident: 'static + }; + + where_clause.predicates.push(bound); + } + + generics + } +} + pub struct FunctionComponentName { component_name: Option, } @@ -168,34 +257,30 @@ impl Parse for FunctionComponentName { } } -fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream { +fn print_fn(func_comp: &FunctionComponent) -> TokenStream { + let name = func_comp.inner_fn_ident(); let FunctionComponent { - fn_token, - name, - attrs, - mut block, - return_type, - generics, - arg, + ref fn_token, + ref attrs, + ref block, + ref return_type, + ref generics, + ref arg, .. } = func_comp; + let mut block = *block.clone(); + let (impl_generics, _ty_generics, where_clause) = generics.split_for_impl(); - let (_impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let name = if use_fn_name { - name - } else { - Ident::new("inner", Span::mixed_site()) - }; - - let ctx_ident = Ident::new("ctx", Span::mixed_site()); + // We use _ctx here so if the component does not use any hooks, the usused_vars lint will not + // be triggered. + let ctx_ident = Ident::new("_ctx", Span::mixed_site()); - let mut body_rewriter = BodyRewriter::default(); - visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); + let mut body_rewriter = BodyRewriter::new(ctx_ident.clone()); + visit_mut::visit_block_mut(&mut body_rewriter, &mut block); quote! { #(#attrs)* - #fn_token #name #ty_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type + #fn_token #name #impl_generics (#ctx_ident: &mut ::yew::functional::HookContext, #arg) -> #return_type #where_clause { #block @@ -205,74 +290,63 @@ fn print_fn(func_comp: FunctionComponent, use_fn_name: bool) -> TokenStream { pub fn function_component_impl( name: FunctionComponentName, - component: FunctionComponent, + mut component: FunctionComponent, ) -> syn::Result { - let FunctionComponentName { component_name } = name; + component.merge_component_name(name)?; - let has_separate_name = component_name.is_some(); + let func = print_fn(&component); - let func = print_fn(component.clone(), has_separate_name); + let into_comp_generics = component.create_into_component_generics(); + let component_attrs = component.filter_attrs_for_component_struct(); + let component_impl_attrs = component.filter_attrs_for_component_impl(); + let phantom_generics = component.phantom_generics(); + let component_name = component.component_name(); + let fn_name = component.inner_fn_ident(); let FunctionComponent { props_type, generics, vis, - name: function_name, .. } = component; - let component_name = component_name.unwrap_or_else(|| function_name.clone()); - let provider_name = format_ident!( - "{}FunctionProvider", - component_name, - span = Span::mixed_site() - ); let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let fn_generics = ty_generics.as_turbofish(); - if has_separate_name && function_name == component_name { - return Err(syn::Error::new_spanned( - component_name, - "the component must not have the same name as the function", - )); - } - - let phantom_generics = generics - .type_params() - .map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds - .collect::>(); - - let provider_props = Ident::new("props", Span::mixed_site()); + let component_props = Ident::new("props", Span::mixed_site()); + let ctx_ident = Ident::new("ctx", Span::mixed_site()); - let fn_generics = ty_generics.as_turbofish(); + let into_comp_impl = { + let (impl_generics, ty_generics, where_clause) = into_comp_generics.split_for_impl(); - let fn_name = if has_separate_name { - function_name - } else { - Ident::new("inner", Span::mixed_site()) + quote! { + impl #impl_generics ::yew::html::IntoComponent for #component_name #ty_generics #where_clause { + type Properties = #props_type; + type Component = ::yew::functional::FunctionComponent; + } + } }; - let ctx_ident = Ident::new("ctx", Span::mixed_site()); - let quoted = quote! { - #[doc(hidden)] - #[allow(non_camel_case_types)] + #(#component_attrs)* #[allow(unused_parens)] - #vis struct #provider_name #ty_generics { + #vis struct #component_name #generics #where_clause { _marker: ::std::marker::PhantomData<(#phantom_generics)>, } - #[automatically_derived] - impl #impl_generics ::yew::functional::FunctionProvider for #provider_name #ty_generics #where_clause { - type TProps = #props_type; + // we cannot disable any lints here because it will be applied to the function body + // as well. + #(#component_impl_attrs)* + impl #impl_generics ::yew::functional::FunctionProvider for #component_name #ty_generics #where_clause { + type Properties = #props_type; - fn run(#ctx_ident: &mut ::yew::functional::HookContext, #provider_props: &Self::TProps) -> ::yew::html::HtmlResult { + fn run(#ctx_ident: &mut ::yew::functional::HookContext, #component_props: &Self::Properties) -> ::yew::html::HtmlResult { #func - ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #provider_props)) + ::yew::html::IntoHtmlResult::into_html_result(#fn_name #fn_generics (#ctx_ident, #component_props)) } } - #[allow(type_alias_bounds)] - #vis type #component_name #generics = ::yew::functional::FunctionComponent<#provider_name #ty_generics>; + #into_comp_impl }; Ok(quoted) diff --git a/packages/yew-macro/src/hook/body.rs b/packages/yew-macro/src/hook/body.rs index ce24934ed20..13ea1dbdd5d 100644 --- a/packages/yew-macro/src/hook/body.rs +++ b/packages/yew-macro/src/hook/body.rs @@ -1,4 +1,3 @@ -use proc_macro2::Span; use proc_macro_error::emit_error; use std::sync::{Arc, Mutex}; use syn::spanned::Spanned; @@ -8,12 +7,20 @@ use syn::{ ExprMatch, ExprWhile, Ident, Item, }; -#[derive(Debug, Default)] +#[derive(Debug)] pub struct BodyRewriter { branch_lock: Arc>, + ctx_ident: Ident, } impl BodyRewriter { + pub fn new(ctx_ident: Ident) -> Self { + Self { + branch_lock: Arc::default(), + ctx_ident, + } + } + fn is_branched(&self) -> bool { self.branch_lock.try_lock().is_err() } @@ -30,7 +37,7 @@ impl BodyRewriter { impl VisitMut for BodyRewriter { fn visit_expr_call_mut(&mut self, i: &mut ExprCall) { - let ctx_ident = Ident::new("ctx", Span::mixed_site()); + let ctx_ident = &self.ctx_ident; // Only rewrite hook calls. if let Expr::Path(ref m) = &*i.func { @@ -55,6 +62,32 @@ impl VisitMut for BodyRewriter { visit_mut::visit_expr_call_mut(self, i); } + fn visit_expr_mut(&mut self, i: &mut Expr) { + let ctx_ident = &self.ctx_ident; + + match &mut *i { + Expr::Macro(m) => { + if let Some(ident) = m.mac.path.segments.last().as_ref().map(|m| &m.ident) { + if ident.to_string().starts_with("use_") { + if self.is_branched() { + emit_error!( + ident, + "hooks cannot be called at this position."; + help = "move hooks to the top-level of your function."; + note = "see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks" + ); + } else { + *i = parse_quote_spanned! { i.span() => ::yew::functional::Hook::run(#i, #ctx_ident) }; + } + } else { + visit_mut::visit_expr_macro_mut(self, m); + } + } + } + _ => visit_mut::visit_expr_mut(self, i), + } + } + fn visit_expr_closure_mut(&mut self, i: &mut ExprClosure) { self.with_branch(move |m| visit_mut::visit_expr_closure_mut(m, i)) } diff --git a/packages/yew-macro/src/hook/mod.rs b/packages/yew-macro/src/hook/mod.rs index 2e8a90223d5..79b094b663c 100644 --- a/packages/yew-macro/src/hook/mod.rs +++ b/packages/yew-macro/src/hook/mod.rs @@ -1,9 +1,10 @@ use proc_macro2::{Span, TokenStream}; use proc_macro_error::emit_error; -use quote::{quote, ToTokens}; +use quote::quote; use syn::parse::{Parse, ParseStream}; -use syn::visit_mut; -use syn::{parse_file, GenericParam, Ident, ItemFn, LitStr, ReturnType, Signature}; +use syn::{ + parse_file, parse_quote, visit_mut, Attribute, Ident, ItemFn, LitStr, ReturnType, Signature, +}; mod body; mod lifetime; @@ -47,27 +48,22 @@ impl Parse for HookFn { } } -pub fn hook_impl(component: HookFn) -> syn::Result { - let HookFn { inner: original_fn } = component; +impl HookFn { + fn doc_attr(&self) -> Attribute { + let vis = &self.inner.vis; + let sig = &self.inner.sig; - let ItemFn { - vis, - sig, - mut block, - attrs, - } = original_fn.clone(); - - let sig_s = quote! { #vis #sig { - __yew_macro_dummy_function_body__ - } } - .to_string(); - - let sig_file = parse_file(&sig_s).unwrap(); - let sig_formatted = prettyplease::unparse(&sig_file); - - let doc_text = LitStr::new( - &format!( - r#" + let sig_s = quote! { #vis #sig { + __yew_macro_dummy_function_body__ + } } + .to_string(); + + let sig_file = parse_file(&sig_s).unwrap(); + let sig_formatted = prettyplease::unparse(&sig_file); + + let literal = LitStr::new( + &format!( + r#" # Note When used in function components and hooks, this hook is equivalent to: @@ -76,15 +72,32 @@ When used in function components and hooks, this hook is equivalent to: {} ``` "#, - sig_formatted.replace( - "__yew_macro_dummy_function_body__", - "/* implementation omitted */" - ) - ), - Span::mixed_site(), - ); + sig_formatted.replace( + "__yew_macro_dummy_function_body__", + "/* implementation omitted */" + ) + ), + Span::mixed_site(), + ); + + parse_quote!(#[doc = #literal]) + } +} - let hook_sig = HookSignature::rewrite(&sig); +pub fn hook_impl(hook: HookFn) -> syn::Result { + let doc_attr = hook.doc_attr(); + + let HookFn { inner: original_fn } = hook; + + let ItemFn { + ref vis, + ref sig, + ref block, + ref attrs, + } = original_fn; + let mut block = *block.clone(); + + let hook_sig = HookSignature::rewrite(sig); let Signature { ref fn_token, @@ -98,24 +111,13 @@ When used in function components and hooks, this hook is equivalent to: let output_type = &hook_sig.output_type; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - let call_generics = { - let mut generics = generics.clone(); - - // We need to filter out lifetimes. - generics.params = generics - .params - .into_iter() - .filter(|m| !matches!(m, GenericParam::Lifetime(_))) - .collect(); - - let (_impl_generics, ty_generics, _where_clause) = generics.split_for_impl(); - ty_generics.as_turbofish().to_token_stream() - }; + let call_generics = hook_sig.call_generics(); - let ctx_ident = Ident::new("ctx", Span::mixed_site()); + // We use _ctx so that if a hook does not use other hooks, it will not trigger unused_vars. + let ctx_ident = Ident::new("_ctx", Span::mixed_site()); - let mut body_rewriter = BodyRewriter::default(); - visit_mut::visit_block_mut(&mut body_rewriter, &mut *block); + let mut body_rewriter = BodyRewriter::new(ctx_ident.clone()); + visit_mut::visit_block_mut(&mut body_rewriter, &mut block); let inner_fn_ident = Ident::new("inner_fn", Span::mixed_site()); let input_args = hook_sig.input_args(); @@ -188,7 +190,7 @@ When used in function components and hooks, this hook is equivalent to: let output = quote! { #[cfg(not(doctest))] #(#attrs)* - #[doc = #doc_text] + #doc_attr #vis #fn_token #ident #generics (#inputs) #hook_return_type #where_clause { #inner_fn diff --git a/packages/yew-macro/src/hook/signature.rs b/packages/yew-macro/src/hook/signature.rs index a2031585e2f..62f3e8ee2cb 100644 --- a/packages/yew-macro/src/hook/signature.rs +++ b/packages/yew-macro/src/hook/signature.rs @@ -1,11 +1,11 @@ -use proc_macro2::Span; +use proc_macro2::{Span, TokenStream}; use proc_macro_error::emit_error; -use quote::quote; +use quote::{quote, ToTokens}; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; use syn::{ - parse_quote, parse_quote_spanned, token, visit_mut, FnArg, Ident, Lifetime, Pat, Receiver, - ReturnType, Signature, Type, TypeImplTrait, TypeReference, WhereClause, + parse_quote, parse_quote_spanned, token, visit_mut, FnArg, GenericParam, Ident, Lifetime, Pat, + Receiver, ReturnType, Signature, Type, TypeImplTrait, TypeReference, WhereClause, }; use super::lifetime; @@ -180,4 +180,18 @@ impl HookSignature { }) .collect() } + + pub fn call_generics(&self) -> TokenStream { + let mut generics = self.sig.generics.clone(); + + // We need to filter out lifetimes. + generics.params = generics + .params + .into_iter() + .filter(|m| !matches!(m, GenericParam::Lifetime(_))) + .collect(); + + let (_impl_generics, ty_generics, _where_clause) = generics.split_for_impl(); + ty_generics.as_turbofish().to_token_stream() + } } diff --git a/packages/yew-macro/src/html_tree/html_component.rs b/packages/yew-macro/src/html_tree/html_component.rs index 53a97236fdf..20758495f73 100644 --- a/packages/yew-macro/src/html_tree/html_component.rs +++ b/packages/yew-macro/src/html_tree/html_component.rs @@ -92,7 +92,7 @@ impl ToTokens for HtmlComponent { children, } = self; - let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::BaseComponent>::Properties); + let props_ty = quote_spanned!(ty.span()=> <#ty as ::yew::html::IntoComponent>::Properties); let children_renderer = if children.is_empty() { None } else { diff --git a/packages/yew-macro/tests/function_component_attr/generic-pass.rs b/packages/yew-macro/tests/function_component_attr/generic-pass.rs index 3672a4475cb..29df3007d46 100644 --- a/packages/yew-macro/tests/function_component_attr/generic-pass.rs +++ b/packages/yew-macro/tests/function_component_attr/generic-pass.rs @@ -58,21 +58,20 @@ fn comp1(_props: &()) -> ::yew::Html { } } -// no longer possible? -// #[::yew::function_component(ConstGenerics)] -// fn const_generics() -> ::yew::Html { -// ::yew::html! { -//
-// { N } -//
-// } -// } +#[::yew::function_component(ConstGenerics)] +fn const_generics() -> ::yew::Html { + ::yew::html! { +
+ { N } +
+ } +} fn compile_pass() { ::yew::html! { a=10 /> }; ::yew::html! { /> }; - // ::yew::html! { /> }; + ::yew::html! { /> }; } fn main() {} diff --git a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr index aea648dd1f2..46be45e6d7b 100644 --- a/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr +++ b/packages/yew-macro/tests/function_component_attr/generic-props-fail.stderr @@ -19,54 +19,49 @@ error[E0599]: no method named `build` found for struct `PropsBuilder` -error[E0277]: the trait bound `FunctionComponent>: BaseComponent` is not satisfied +error[E0277]: the trait bound `Comp: IntoComponent` is not satisfied --> tests/function_component_attr/generic-props-fail.rs:27:14 | 27 | html! { /> }; - | ^^^^ the trait `BaseComponent` is not implemented for `FunctionComponent>` + | ^^^^ the trait `IntoComponent` is not implemented for `Comp` | = help: the following implementations were found: - as BaseComponent> + as IntoComponent> -error[E0599]: the function or associated item `new` exists for struct `VChild>>`, but its trait bounds were not satisfied - --> tests/function_component_attr/generic-props-fail.rs:27:14 - | -27 | html! { /> }; - | ^^^^ function or associated item cannot be called on `VChild>>` due to unsatisfied trait bounds - | - ::: $WORKSPACE/packages/yew/src/functional/mod.rs - | - | pub struct FunctionComponent { - | ----------------------------------------------------------- doesn't satisfy `_: BaseComponent` - | - = note: the following trait bounds were not satisfied: - `FunctionComponent>: BaseComponent` +error[E0599]: the function or associated item `new` exists for struct `VChild>`, but its trait bounds were not satisfied + --> tests/function_component_attr/generic-props-fail.rs:27:14 + | +8 | #[function_component(Comp)] + | --------------------------- doesn't satisfy `Comp: IntoComponent` +... +27 | html! { /> }; + | ^^^^ function or associated item cannot be called on `VChild>` due to unsatisfied trait bounds + | + = note: the following trait bounds were not satisfied: + `Comp: IntoComponent` error[E0277]: the trait bound `MissingTypeBounds: yew::Properties` is not satisfied - --> tests/function_component_attr/generic-props-fail.rs:27:14 - | -27 | html! { /> }; - | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` - | -note: required because of the requirements on the impl of `FunctionProvider` for `CompFunctionProvider` - --> tests/function_component_attr/generic-props-fail.rs:8:1 - | -8 | #[function_component(Comp)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ -note: required by a bound in `FunctionComponent` - --> $WORKSPACE/packages/yew/src/functional/mod.rs - | - | pub struct FunctionComponent { - | ^^^^^^^^^^^^^^^^ required by this bound in `FunctionComponent` - = note: this error originates in the attribute macro `function_component` (in Nightly builds, run with -Z macro-backtrace for more info) + --> tests/function_component_attr/generic-props-fail.rs:27:14 + | +27 | html! { /> }; + | ^^^^ the trait `yew::Properties` is not implemented for `MissingTypeBounds` + | +note: required by a bound in `Comp` + --> tests/function_component_attr/generic-props-fail.rs:11:8 + | +8 | #[function_component(Comp)] + | ---- required by a bound in this +... +11 | P: Properties + PartialEq, + | ^^^^^^^^^^ required by this bound in `Comp` -error[E0107]: missing generics for type alias `Comp` +error[E0107]: missing generics for struct `Comp` --> tests/function_component_attr/generic-props-fail.rs:30:14 | 30 | html! { }; | ^^^^ expected 1 generic argument | -note: type alias defined here, with 1 generic parameter: `P` +note: struct defined here, with 1 generic parameter: `P` --> tests/function_component_attr/generic-props-fail.rs:8:22 | 8 | #[function_component(Comp)] diff --git a/packages/yew-macro/tests/hook_attr/hook_macro-fail.rs b/packages/yew-macro/tests/hook_attr/hook_macro-fail.rs new file mode 100644 index 00000000000..7180602f539 --- /dev/null +++ b/packages/yew-macro/tests/hook_attr/hook_macro-fail.rs @@ -0,0 +1,30 @@ +use yew::prelude::*; + +#[hook] +pub fn use_some_macro_inner(val: &str) -> String { + use_state(|| val.to_owned()).to_string() +} + +macro_rules! use_some_macro { + () => { + use_some_macro_inner("default str") + }; + ($t: tt) => { + use_some_macro_inner($t) + }; +} + +#[function_component] +fn Comp() -> Html { + let content = if true { + use_some_macro!() + } else { + use_some_macro!("b") + }; + + html! { +
{content}
+ } +} + +fn main() {} diff --git a/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr b/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr new file mode 100644 index 00000000000..ca7561ea43c --- /dev/null +++ b/packages/yew-macro/tests/hook_attr/hook_macro-fail.stderr @@ -0,0 +1,33 @@ +error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + + --> tests/hook_attr/hook_macro-fail.rs:20:9 + | +20 | use_some_macro!() + | ^^^^^^^^^^^^^^ + +error: hooks cannot be called at this position. + + = help: move hooks to the top-level of your function. + = note: see: https://yew.rs/docs/next/concepts/function-components/introduction#hooks + + --> tests/hook_attr/hook_macro-fail.rs:22:9 + | +22 | use_some_macro!("b") + | ^^^^^^^^^^^^^^ + +warning: unused macro definition + --> tests/hook_attr/hook_macro-fail.rs:8:1 + | +8 | / macro_rules! use_some_macro { +9 | | () => { +10 | | use_some_macro_inner("default str") +11 | | }; +... | +14 | | }; +15 | | } + | |_^ + | + = note: `#[warn(unused_macros)]` on by default diff --git a/packages/yew-macro/tests/hook_attr/hook_macro-pass.rs b/packages/yew-macro/tests/hook_attr/hook_macro-pass.rs new file mode 100644 index 00000000000..9a053d353bb --- /dev/null +++ b/packages/yew-macro/tests/hook_attr/hook_macro-pass.rs @@ -0,0 +1,27 @@ +use yew::prelude::*; + +#[hook] +pub fn use_some_macro_inner(val: &str) -> String { + use_state(|| val.to_owned()).to_string() +} + +macro_rules! use_some_macro { + () => { + use_some_macro_inner("default str") + }; + ($t: tt) => { + use_some_macro_inner($t) + }; +} + +#[function_component] +fn Comp() -> Html { + let a = use_some_macro!(); + let b = use_some_macro!("b"); + + html! { +
{a}{b}
+ } +} + +fn main() {} diff --git a/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr b/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr index 4b062ceeda5..ed4828b53b3 100644 --- a/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-unimplemented-fail.stderr @@ -10,10 +10,10 @@ error[E0599]: the function or associated item `new` exists for struct `VChild tests/html_macro/component-unimplemented-fail.rs:6:14 | 3 | struct Unimplemented; - | --------------------- doesn't satisfy `Unimplemented: BaseComponent` + | --------------------- doesn't satisfy `Unimplemented: IntoComponent` ... 6 | html! { }; | ^^^^^^^^^^^^^ function or associated item cannot be called on `VChild` due to unsatisfied trait bounds | = note: the following trait bounds were not satisfied: - `Unimplemented: BaseComponent` + `Unimplemented: IntoComponent` diff --git a/packages/yew/src/dom_bundle/app_handle.rs b/packages/yew/src/dom_bundle/app_handle.rs index 771eb1b9d8e..7cd49e94ee1 100644 --- a/packages/yew/src/dom_bundle/app_handle.rs +++ b/packages/yew/src/dom_bundle/app_handle.rs @@ -1,27 +1,27 @@ //! [AppHandle] contains the state Yew keeps to bootstrap a component in an isolated scope. use super::{ComponentRenderState, Scoped}; -use crate::html::{BaseComponent, Scope}; -use crate::NodeRef; -use std::{ops::Deref, rc::Rc}; +use crate::html::{IntoComponent, NodeRef, Scope}; +use std::ops::Deref; +use std::rc::Rc; use web_sys::Element; /// An instance of an application. #[derive(Debug)] -pub struct AppHandle { +pub struct AppHandle { /// `Scope` holder - scope: Scope, + pub(crate) scope: Scope<::Component>, } -impl AppHandle +impl AppHandle where - COMP: BaseComponent, + ICOMP: IntoComponent, { /// The main entry point of a Yew program which also allows passing properties. It works /// similarly to the `program` function in Elm. You should provide an initial model, `update` /// function which will update the state of the model and a `view` function which /// will render the model to a virtual DOM tree. - pub(crate) fn mount_with_props(element: Element, props: Rc) -> Self { + pub(crate) fn mount_with_props(element: Element, props: Rc) -> Self { clear_element(&element); let app = Self { scope: Scope::new(None), @@ -41,11 +41,11 @@ where } } -impl Deref for AppHandle +impl Deref for AppHandle where - COMP: BaseComponent, + ICOMP: IntoComponent, { - type Target = Scope; + type Target = Scope<::Component>; fn deref(&self) -> &Self::Target { &self.scope diff --git a/packages/yew/src/dom_bundle/bcomp.rs b/packages/yew/src/dom_bundle/bcomp.rs index fe2d1ccabf0..c68db83af9a 100644 --- a/packages/yew/src/dom_bundle/bcomp.rs +++ b/packages/yew/src/dom_bundle/bcomp.rs @@ -175,7 +175,7 @@ impl Mountable for PropsWrapper { } fn reuse(self: Box, node_ref: NodeRef, scope: &dyn Scoped, next_sibling: NodeRef) { - let scope: Scope = scope.to_any().downcast(); + let scope: Scope = scope.to_any().downcast::(); scope.reuse(self.props, node_ref, next_sibling); } diff --git a/packages/yew/src/functional/mod.rs b/packages/yew/src/functional/mod.rs index 447e74e32ff..35226a536d0 100644 --- a/packages/yew/src/functional/mod.rs +++ b/packages/yew/src/functional/mod.rs @@ -31,7 +31,7 @@ pub use hooks::*; use crate::html::Context; -use crate::html::SealedBaseComponent; +use crate::html::sealed::SealedBaseComponent; /// This attribute creates a function component from a normal Rust function. /// @@ -131,21 +131,27 @@ impl fmt::Debug for HookContext { /// Trait that allows a struct to act as Function Component. pub trait FunctionProvider { /// Properties for the Function Component. - type TProps: Properties + PartialEq; + type Properties: Properties + PartialEq; /// Render the component. This function returns the [`Html`](crate::Html) to be rendered for the component. /// /// Equivalent of [`Component::view`](crate::html::Component::view). - fn run(ctx: &mut HookContext, props: &Self::TProps) -> HtmlResult; + fn run(ctx: &mut HookContext, props: &Self::Properties) -> HtmlResult; } /// Wrapper that allows a struct implementing [`FunctionProvider`] to be consumed as a component. -pub struct FunctionComponent { +pub struct FunctionComponent +where + T: FunctionProvider + 'static, +{ _never: std::marker::PhantomData, hook_ctx: RefCell, } -impl fmt::Debug for FunctionComponent { +impl fmt::Debug for FunctionComponent +where + T: FunctionProvider + 'static, +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("FunctionComponent<_>") } @@ -156,7 +162,7 @@ where T: FunctionProvider + 'static, { type Message = (); - type Properties = T::TProps; + type Properties = T::Properties; fn create(ctx: &Context) -> Self { let scope = AnyScope::from(ctx.link().clone()); diff --git a/packages/yew/src/html/component/mod.rs b/packages/yew/src/html/component/mod.rs index 74443781962..b6efe5cc8b9 100644 --- a/packages/yew/src/html/component/mod.rs +++ b/packages/yew/src/html/component/mod.rs @@ -70,9 +70,11 @@ impl Context { } } -/// A Sealed trait that prevents direct implementation of -/// [BaseComponent]. -pub trait SealedBaseComponent {} +pub(crate) mod sealed { + /// A Sealed trait that prevents direct implementation of + /// [BaseComponent]. + pub trait SealedBaseComponent {} +} /// The common base of both function components and struct components. /// @@ -80,11 +82,11 @@ pub trait SealedBaseComponent {} /// [`#[function_component]`](crate::functional::function_component). /// /// We provide a blanket implementation of this trait for every member that implements [`Component`]. -pub trait BaseComponent: SealedBaseComponent + Sized + 'static { +pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { /// The Component's Message. type Message: 'static; - /// The Component's properties. + /// The Component's Properties. type Properties: Properties; /// Creates a component. @@ -201,4 +203,24 @@ where } } -impl SealedBaseComponent for T where T: Sized + Component + 'static {} +impl sealed::SealedBaseComponent for T where T: Sized + Component + 'static {} + +/// A trait that indicates a type is able to be converted into a component. +/// +/// You may want to use this trait if you want to accept both function components and struct +/// components as a generic parameter. +pub trait IntoComponent { + /// The Component's Properties. + type Properties: Properties; + + /// The Component Type. + type Component: BaseComponent + 'static; +} + +impl IntoComponent for T +where + T: BaseComponent + 'static, +{ + type Properties = T::Properties; + type Component = T; +} diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 89529c9d1e2..02557af8fbf 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -10,9 +10,10 @@ use super::{ use crate::callback::Callback; use crate::context::{ContextHandle, ContextProvider}; use crate::dom_bundle::{ComponentRenderState, Scoped}; +use crate::html::IntoComponent; use crate::html::NodeRef; use crate::scheduler::{self, Shared}; -use std::any::TypeId; +use std::any::{Any, TypeId}; use std::cell::{Ref, RefCell}; use std::marker::PhantomData; use std::ops::Deref; @@ -63,7 +64,7 @@ impl Clone for MsgQueue { pub struct AnyScope { type_id: TypeId, parent: Option>, - state: Shared>, + typed_scope: Rc, } impl fmt::Debug for AnyScope { @@ -76,8 +77,8 @@ impl From> for AnyScope { fn from(scope: Scope) -> Self { AnyScope { type_id: TypeId::of::(), - parent: scope.parent, - state: scope.state, + parent: scope.parent.clone(), + typed_scope: Rc::new(scope), } } } @@ -88,7 +89,7 @@ impl AnyScope { Self { type_id: TypeId::of::<()>(), parent: None, - state: Rc::new(RefCell::new(None)), + typed_scope: Rc::new(()), } } @@ -107,37 +108,25 @@ impl AnyScope { /// # Panics /// /// If the self value can't be cast into the target type. - pub fn downcast(self) -> Scope { - self.try_downcast::().unwrap() + pub fn downcast(&self) -> Scope { + self.try_downcast::().unwrap() } /// Attempts to downcast into a typed scope /// /// Returns [`None`] if the self value can't be cast into the target type. - pub fn try_downcast(self) -> Option> { - let state = self.state.borrow(); - - state.as_ref().map(|m| { - m.inner - .as_any() - .downcast_ref::>() - .unwrap() - .context - .link() - .clone() - }) + pub fn try_downcast(&self) -> Option> { + self.typed_scope + .downcast_ref::>() + .cloned() } /// Attempts to find a parent scope of a certain type /// /// Returns [`None`] if no parent scope with the specified type was found. - pub fn find_parent_scope(&self) -> Option> { - let expected_type_id = TypeId::of::(); + pub fn find_parent_scope(&self) -> Option> { iter::successors(Some(self), |scope| scope.get_parent()) - .filter(|scope| scope.get_type_id() == &expected_type_id) - .cloned() - .map(AnyScope::downcast::) - .next() + .find_map(AnyScope::try_downcast::) } /// Accesses a value provided by a parent `ContextProvider` component of the diff --git a/packages/yew/src/html/mod.rs b/packages/yew/src/html/mod.rs index ae290b1e67f..7422cb9eca9 100644 --- a/packages/yew/src/html/mod.rs +++ b/packages/yew/src/html/mod.rs @@ -7,6 +7,7 @@ mod error; mod listener; pub use classes::*; +pub(crate) use component::sealed; pub use component::*; pub use conversion::*; pub use error::*; diff --git a/packages/yew/src/lib.rs b/packages/yew/src/lib.rs index e614ff2a77e..9c3be9f316b 100644 --- a/packages/yew/src/lib.rs +++ b/packages/yew/src/lib.rs @@ -300,7 +300,7 @@ pub mod events { pub use crate::dom_bundle::AppHandle; use web_sys::Element; -use crate::html::BaseComponent; +use crate::html::IntoComponent; thread_local! { static PANIC_HOOK_IS_SET: Cell = Cell::new(false); @@ -322,44 +322,44 @@ fn set_default_panic_hook() { /// The main entry point of a Yew application. /// If you would like to pass props, use the `start_app_with_props_in_element` method. -pub fn start_app_in_element(element: Element) -> AppHandle +pub fn start_app_in_element(element: Element) -> AppHandle where - COMP: BaseComponent, - COMP::Properties: Default, + ICOMP: IntoComponent, + ICOMP::Properties: Default, { - start_app_with_props_in_element::(element, COMP::Properties::default()) + start_app_with_props_in_element(element, ICOMP::Properties::default()) } /// Starts an yew app mounted to the body of the document. /// Alias to start_app_in_element(Body) -pub fn start_app() -> AppHandle +pub fn start_app() -> AppHandle where - COMP: BaseComponent, - COMP::Properties: Default, + ICOMP: IntoComponent, + ICOMP::Properties: Default, { - start_app_with_props::(COMP::Properties::default()) + start_app_with_props(ICOMP::Properties::default()) } /// The main entry point of a Yew application. This function does the /// same as `start_app_in_element(...)` but allows to start an Yew application with properties. -pub fn start_app_with_props_in_element( +pub fn start_app_with_props_in_element( element: Element, - props: COMP::Properties, -) -> AppHandle + props: ICOMP::Properties, +) -> AppHandle where - COMP: BaseComponent, + ICOMP: IntoComponent, { set_default_panic_hook(); - AppHandle::::mount_with_props(element, Rc::new(props)) + AppHandle::::mount_with_props(element, Rc::new(props)) } /// The main entry point of a Yew application. /// This function does the same as `start_app(...)` but allows to start an Yew application with properties. -pub fn start_app_with_props(props: COMP::Properties) -> AppHandle +pub fn start_app_with_props(props: ICOMP::Properties) -> AppHandle where - COMP: BaseComponent, + ICOMP: IntoComponent, { - start_app_with_props_in_element::( + start_app_with_props_in_element( gloo_utils::document() .body() .expect("no body node found") @@ -383,10 +383,11 @@ pub mod prelude { pub use crate::events::*; pub use crate::html::{ create_portal, BaseComponent, Children, ChildrenWithProps, Classes, Component, Context, - Html, HtmlResult, NodeRef, Properties, + Html, HtmlResult, IntoComponent, NodeRef, Properties, }; pub use crate::macros::{classes, html, html_nested}; pub use crate::suspense::Suspense; + pub use crate::virtual_dom::AttrValue; pub use crate::functional::*; } diff --git a/packages/yew/src/server_renderer.rs b/packages/yew/src/server_renderer.rs index 91bf4c95212..b7277340693 100644 --- a/packages/yew/src/server_renderer.rs +++ b/packages/yew/src/server_renderer.rs @@ -3,29 +3,29 @@ use super::*; use crate::html::Scope; /// A Yew Server-side Renderer. -#[derive(Debug)] #[cfg_attr(documenting, doc(cfg(feature = "ssr")))] -pub struct ServerRenderer +#[derive(Debug)] +pub struct ServerRenderer where - COMP: BaseComponent, + ICOMP: IntoComponent, { - props: COMP::Properties, + props: ICOMP::Properties, } -impl Default for ServerRenderer +impl Default for ServerRenderer where - COMP: BaseComponent, - COMP::Properties: Default, + ICOMP: IntoComponent, + ICOMP::Properties: Default, { fn default() -> Self { - Self::with_props(COMP::Properties::default()) + Self::with_props(ICOMP::Properties::default()) } } -impl ServerRenderer +impl ServerRenderer where - COMP: BaseComponent, - COMP::Properties: Default, + ICOMP: IntoComponent, + ICOMP::Properties: Default, { /// Creates a [ServerRenderer] with default properties. pub fn new() -> Self { @@ -33,12 +33,12 @@ where } } -impl ServerRenderer +impl ServerRenderer where - COMP: BaseComponent, + ICOMP: IntoComponent, { /// Creates a [ServerRenderer] with custom properties. - pub fn with_props(props: COMP::Properties) -> Self { + pub fn with_props(props: ICOMP::Properties) -> Self { Self { props } } @@ -53,7 +53,7 @@ where /// Renders Yew Application to a String. pub async fn render_to_string(self, w: &mut String) { - let scope = Scope::::new(None); + let scope = Scope::<::Component>::new(None); scope.render_to_string(w, self.props.into()).await; } } diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index 76b098523a2..76d1e4f3a05 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -2,7 +2,7 @@ use super::Key; use crate::dom_bundle::{Mountable, PropsWrapper}; -use crate::html::{BaseComponent, NodeRef}; +use crate::html::{BaseComponent, IntoComponent, NodeRef}; use std::any::TypeId; use std::fmt; use std::rc::Rc; @@ -41,15 +41,15 @@ impl Clone for VComp { } /// A virtual child component. -pub struct VChild { +pub struct VChild { /// The component properties - pub props: Rc, + pub props: Rc, /// Reference to the mounted node node_ref: NodeRef, key: Option, } -impl Clone for VChild { +impl Clone for VChild { fn clone(&self) -> Self { VChild { props: Rc::clone(&self.props), @@ -59,21 +59,21 @@ impl Clone for VChild { } } -impl PartialEq for VChild +impl PartialEq for VChild where - COMP::Properties: PartialEq, + ICOMP::Properties: PartialEq, { - fn eq(&self, other: &VChild) -> bool { + fn eq(&self, other: &VChild) -> bool { self.props == other.props } } -impl VChild +impl VChild where - COMP: BaseComponent, + ICOMP: IntoComponent, { /// Creates a child component that can be accessed and modified by its parent. - pub fn new(props: COMP::Properties, node_ref: NodeRef, key: Option) -> Self { + pub fn new(props: ICOMP::Properties, node_ref: NodeRef, key: Option) -> Self { Self { props: Rc::new(props), node_ref, @@ -82,25 +82,25 @@ where } } -impl From> for VComp +impl From> for VComp where - COMP: BaseComponent, + ICOMP: IntoComponent, { - fn from(vchild: VChild) -> Self { - VComp::new::(vchild.props, vchild.node_ref, vchild.key) + fn from(vchild: VChild) -> Self { + VComp::new::(vchild.props, vchild.node_ref, vchild.key) } } impl VComp { /// Creates a new `VComp` instance. - pub fn new(props: Rc, node_ref: NodeRef, key: Option) -> Self + pub fn new(props: Rc, node_ref: NodeRef, key: Option) -> Self where - COMP: BaseComponent, + ICOMP: IntoComponent, { VComp { - type_id: TypeId::of::(), + type_id: TypeId::of::(), node_ref, - mountable: Box::new(PropsWrapper::::new(props)), + mountable: Box::new(PropsWrapper::::new(props)), key, } } diff --git a/packages/yew/src/virtual_dom/vnode.rs b/packages/yew/src/virtual_dom/vnode.rs index 3b98a0e7ae4..975f3cb84db 100644 --- a/packages/yew/src/virtual_dom/vnode.rs +++ b/packages/yew/src/virtual_dom/vnode.rs @@ -1,7 +1,7 @@ //! This module contains the implementation of abstract virtual node. use super::{Key, VChild, VComp, VList, VPortal, VSuspense, VTag, VText}; -use crate::html::BaseComponent; +use crate::html::IntoComponent; use std::cmp::PartialEq; use std::fmt; use std::iter::FromIterator; @@ -93,11 +93,11 @@ impl From for VNode { } } -impl From> for VNode +impl From> for VNode where - COMP: BaseComponent, + ICOMP: IntoComponent, { - fn from(vchild: VChild) -> Self { + fn from(vchild: VChild) -> Self { VNode::VComp(VComp::from(vchild)) } } diff --git a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr index 82e0f9a46f8..563ac8e341e 100644 --- a/packages/yew/tests/failed_tests/base_component_impl-fail.stderr +++ b/packages/yew/tests/failed_tests/base_component_impl-fail.stderr @@ -4,9 +4,9 @@ error[E0277]: the trait bound `Comp: yew::Component` is not satisfied 6 | impl BaseComponent for Comp { | ^^^^^^^^^^^^^ the trait `yew::Component` is not implemented for `Comp` | - = note: required because of the requirements on the impl of `SealedBaseComponent` for `Comp` + = note: required because of the requirements on the impl of `html::component::sealed::SealedBaseComponent` for `Comp` note: required by a bound in `BaseComponent` --> src/html/component/mod.rs | - | pub trait BaseComponent: SealedBaseComponent + Sized + 'static { - | ^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` + | pub trait BaseComponent: sealed::SealedBaseComponent + Sized + 'static { + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `BaseComponent` diff --git a/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.rs b/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.rs index 9127dec8719..dd7399b146a 100644 --- a/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.rs +++ b/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.rs @@ -23,6 +23,6 @@ impl BaseComponent for Comp { fn destroy(&mut self, _ctx: &Context) {} } -impl yew::html::component::SealedBaseComponent for Comp {} +impl yew::html::component::sealed::SealedBaseComponent for Comp {} fn main() {} diff --git a/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.stderr b/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.stderr index 322cc88b4dd..4bdf97cf6a6 100644 --- a/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.stderr +++ b/packages/yew/tests/failed_tests/sealed_base_component_impl-fail.stderr @@ -1,7 +1,7 @@ error[E0603]: module `component` is private --> tests/failed_tests/sealed_base_component_impl-fail.rs:26:17 | -26 | impl yew::html::component::SealedBaseComponent for Comp {} +26 | impl yew::html::component::sealed::SealedBaseComponent for Comp {} | ^^^^^^^^^ private module | note: the module `component` is defined here diff --git a/tools/process-benchmark-results/src/main.rs b/tools/process-benchmark-results/src/main.rs index 586eaa8c618..286caa920b3 100644 --- a/tools/process-benchmark-results/src/main.rs +++ b/tools/process-benchmark-results/src/main.rs @@ -20,7 +20,7 @@ fn main() -> Result<()> { let transformed_benchmarks: Vec = input_json .into_iter() .map(|v| GhActionBenchmark { - name: format!("{} {}", v["framework"], v["benchmark"]).replace('\"', ""), + name: format!("{} {}", v["framework"], v["benchmark"]).replace('"', ""), unit: String::default(), value: v["median"].to_string(), })