Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 137 additions & 63 deletions packages/yew-macro/src/function_component.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,6 +21,8 @@ pub struct FunctionComponent {
name: Ident,
return_type: Box<Type>,
fn_token: Fn,

component_name: Option<Ident>,
}

impl Parse for FunctionComponent {
Expand Down Expand Up @@ -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<Attribute> {
self.attrs
.iter()
.filter_map(|m| {
m.path
.get_ident()
.and_then(|ident| match ident.to_string().as_str() {
"doc" | "allow" => Some(m.clone()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should pass deny and warn. if we're passing along allow, or does it not matter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably shouldn't. This makes users to be able to apply lints on generated code.

Lints are usually suppressed on generated code.

_ => None,
})
})
.collect()
}

/// Filters attributes that should be copied to the component impl block.
fn filter_attrs_for_component_impl(&self) -> Vec<Attribute> {
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<Ident, Comma> {
self.generics
.type_params()
.map(|ty_param| ty_param.ident.clone()) // create a new Punctuated sequence without any type bounds
.collect::<Punctuated<_, Comma>>()
}

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<Ident>,
}
Expand All @@ -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
Expand All @@ -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<TokenStream> {
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::<Punctuated<_, Comma>>();

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<Self>;
}
}
};

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)
Expand Down
39 changes: 36 additions & 3 deletions packages/yew-macro/src/hook/body.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use proc_macro2::Span;
use proc_macro_error::emit_error;
use std::sync::{Arc, Mutex};
use syn::spanned::Spanned;
Expand All @@ -8,12 +7,20 @@ use syn::{
ExprMatch, ExprWhile, Ident, Item,
};

#[derive(Debug, Default)]
#[derive(Debug)]
pub struct BodyRewriter {
branch_lock: Arc<Mutex<()>>,
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()
}
Expand All @@ -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 {
Expand All @@ -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))
}
Expand Down
Loading