Skip to content

Commit

Permalink
Merge port of yew-autopros in Yew to yew-autoprops
Browse files Browse the repository at this point in the history
  • Loading branch information
cecton committed Nov 5, 2023
1 parent 584c416 commit 859672a
Show file tree
Hide file tree
Showing 18 changed files with 551 additions and 372 deletions.
18 changes: 10 additions & 8 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@ name: Rust

on:
push:
branches: [ "master" ]
branches: [ "main" ]
pull_request:
branches: [ "master" ]
branches: [ "main" ]

env:
CARGO_TERM_COLOR: always

jobs:
build:

strategy:
matrix:
rust:
- stable
- 1.64.0
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- run: rustup default ${{ matrix.rust }}
- run: cargo build --verbose
- run: cargo test --verbose
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@


[dev-dependencies]
yew = { version = "0.21" }
trybuild = "1"
yew = { version = "0.21" }
trybuild = "1"
rustversion = "1"
258 changes: 258 additions & 0 deletions src/autoprops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
use crate::function_component::FunctionComponentName;
use quote::{quote, quote_spanned};
use syn::spanned::Spanned;

#[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 placed *before* #[function_component])",
)
})?;

for input in &sig.inputs {
if let syn::FnArg::Typed(syn::PatType { pat, .. }) = input {
if let syn::Pat::Wild(wild) = pat.as_ref() {
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 self.needs_a_properties_struct() {
// 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<_>>();
let phantom = sig.generics.type_params().next().is_some().then(|| {
quote! {
, phantom: _
}
});
quote! {
#(#receivers,)* #properties_name {
#(#args),*
#phantom
}: &#properties_name #type_generics
}
} else {
quote! {}
};
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 !self.needs_a_properties_struct() {
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<_>>();
let type_params = sig
.generics
.type_params()
.map(|param| &param.ident)
.collect::<Vec<_>>();
let phantom = (!type_params.is_empty()).then(|| {
quote! {
#[prop_or_default]
phantom: ::std::marker::PhantomData <( #(#type_params),* )>,
}
});
let fields_eq = sig
.inputs
.iter()
.filter_map(|arg| match arg {
syn::FnArg::Typed(syn::PatType { pat, ty, .. }) => Some(quote_spanned! {
ty.span() => self.#pat == rhs.#pat &&
}),
_ => None,
})
.collect::<Vec<_>>();

quote! {
#[derive(::yew::Properties)]
#vis struct #properties_name #impl_generics #where_clause {
#(#fields)*
#phantom
}

impl #impl_generics ::std::cmp::PartialEq for #properties_name #type_generics
#where_clause {
fn eq(&self, rhs: &Self) -> ::std::primitive::bool {
#(#fields_eq)* true
}
}
}
}

fn needs_a_properties_struct(&self) -> bool {
let syn::ItemFn { sig, .. } = &self.item_fn;
!sig.inputs.is_empty() || sig.generics.type_params().next().is_some()
}
}

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),
})
}
}
24 changes: 24 additions & 0 deletions src/function_component.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// copy-pasted from yew-macro because proc-macro crates cannot export any items

use syn::parse::{Parse, ParseStream};
use syn::Ident;

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

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

let component_name = input.parse()?;

Ok(Self {
component_name: Some(component_name),
})
}
}
Loading

0 comments on commit 859672a

Please sign in to comment.