Skip to content

Commit

Permalink
Port yew-autoprops to yew-macro
Browse files Browse the repository at this point in the history
  • Loading branch information
cecton committed Nov 1, 2023
1 parent ddb77bd commit 6866d0d
Show file tree
Hide file tree
Showing 9 changed files with 521 additions and 12 deletions.
218 changes: 218 additions & 0 deletions packages/yew-macro/src/autoprops.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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::<FunctionComponentName>(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::<Vec<_>>();
let args = sig
.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(syn::PatType { pat, .. }) => Some(quote! { #pat }),
_ => None,
})
.collect::<Vec<_>>();
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::<Vec<_>>();

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::<Vec<_>>();

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<syn::Ident>,
}

impl syn::parse::Parse for AutopropsArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
if input.is_empty() {
return Ok(Self {
properties_name: None,
});
}

let properties_name = input.parse()?;

Ok(Self {
properties_name: Some(properties_name),
})
}
}
34 changes: 31 additions & 3 deletions packages/yew-macro/src/function_component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand All @@ -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]?",
));
}

Expand Down Expand Up @@ -393,7 +421,7 @@ impl FunctionComponent {
}

pub struct FunctionComponentName {
component_name: Option<Ident>,
pub component_name: Option<Ident>,
}

impl Parse for FunctionComponentName {
Expand Down
12 changes: 12 additions & 0 deletions packages/yew-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
66 changes: 66 additions & 0 deletions packages/yew-macro/tests/function_component_attr/autoprops-fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use yew::prelude::*;

#[autoprops]
#[function_component]
fn CantAcceptReceiver(&self, b: bool) -> Html {
html! {
<p>{b}</p>
}
}

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

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

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

#[derive(PartialEq)]
struct NotClonable(u32);

#[autoprops]
#[function_component]
fn TypeIsNotClone(stuff: NotClonable) -> Html {
drop(stuff);
html! {
<p></p>
}
}

#[derive(Clone)]
struct NotPartialEq(u32);

#[autoprops]
#[function_component]
fn TypeIsNotPartialEq(stuff: NotPartialEq) -> Html {
drop(stuff);
html! {
<p></p>
}
}

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

fn main() {}
Loading

0 comments on commit 6866d0d

Please sign in to comment.