From 43f0d574effc9500ac78bb3e866aee8c7e24e0ca Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Thu, 24 Sep 2020 23:03:26 +0200 Subject: [PATCH 01/39] remove renamed imports from yew-macro They add a lot of cognitive overhead and don't provide much benefit in this case. --- yew-macro/src/html_tree/html_block.rs | 4 ++-- yew-macro/src/html_tree/html_component.rs | 10 +++++----- yew-macro/src/html_tree/html_dashed_name.rs | 4 ++-- yew-macro/src/html_tree/html_iterable.rs | 4 ++-- yew-macro/src/html_tree/html_list.rs | 10 +++++----- yew-macro/src/html_tree/html_prop.rs | 14 +++++++------- yew-macro/src/html_tree/html_tag/mod.rs | 16 +++++++--------- .../src/html_tree/html_tag/tag_attributes.rs | 4 ++-- 8 files changed, 32 insertions(+), 34 deletions(-) diff --git a/yew-macro/src/html_tree/html_block.rs b/yew-macro/src/html_tree/html_block.rs index 2f89d92baea..b7109380924 100644 --- a/yew-macro/src/html_tree/html_block.rs +++ b/yew-macro/src/html_tree/html_block.rs @@ -6,7 +6,7 @@ use proc_macro2::Delimiter; use quote::{quote, quote_spanned, ToTokens}; use syn::braced; use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::token; pub struct HtmlBlock { @@ -26,7 +26,7 @@ impl PeekValue<()> for HtmlBlock { } impl Parse for HtmlBlock { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let content; let brace = braced!(content in input); let content = if HtmlIterable::peek(content.cursor()).is_some() { diff --git a/yew-macro/src/html_tree/html_component.rs b/yew-macro/src/html_tree/html_component.rs index 24b9de05e06..860a62a3b86 100644 --- a/yew-macro/src/html_tree/html_component.rs +++ b/yew-macro/src/html_tree/html_component.rs @@ -7,7 +7,7 @@ use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; use std::cmp::Ordering; use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; use syn::{ @@ -30,7 +30,7 @@ impl PeekValue<()> for HtmlComponent { } impl Parse for HtmlComponent { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { if HtmlComponentClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( @@ -277,7 +277,7 @@ impl PeekValue for HtmlComponentOpen { } impl Parse for HtmlComponentOpen { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let lt = input.parse::()?; let ty = input.parse()?; // backwards compat @@ -326,7 +326,7 @@ impl PeekValue for HtmlComponentClose { } } impl Parse for HtmlComponentClose { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { Ok(HtmlComponentClose { lt: input.parse()?, div: input.parse()?, @@ -358,7 +358,7 @@ struct Props { const COLLISION_MSG: &str = "Using the `with props` syntax in combination with named props is not allowed (note: this does not apply to the `ref` prop)."; impl Parse for Props { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let mut props = Props { node_ref: None, key: None, diff --git a/yew-macro/src/html_tree/html_dashed_name.rs b/yew-macro/src/html_tree/html_dashed_name.rs index ed72bbb8a0b..9d918b9bdbf 100644 --- a/yew-macro/src/html_tree/html_dashed_name.rs +++ b/yew-macro/src/html_tree/html_dashed_name.rs @@ -6,7 +6,7 @@ use quote::{quote, ToTokens}; use std::fmt; use syn::buffer::Cursor; use syn::ext::IdentExt; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::{spanned::Spanned, LitStr, Token}; #[derive(Clone, PartialEq)] @@ -68,7 +68,7 @@ impl Peek<'_, Self> for HtmlDashedName { } impl Parse for HtmlDashedName { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let name = input.call(Ident::parse_any)?; let mut extended = Vec::new(); while input.peek(Token![-]) { diff --git a/yew-macro/src/html_tree/html_iterable.rs b/yew-macro/src/html_tree/html_iterable.rs index 265123e8c12..700d28ab2ad 100644 --- a/yew-macro/src/html_tree/html_iterable.rs +++ b/yew-macro/src/html_tree/html_iterable.rs @@ -4,7 +4,7 @@ use boolinator::Boolinator; use proc_macro2::TokenStream; use quote::{quote_spanned, ToTokens}; use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Expr, Token}; @@ -18,7 +18,7 @@ impl PeekValue<()> for HtmlIterable { } impl Parse for HtmlIterable { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let for_token = input.parse::()?; match input.parse() { diff --git a/yew-macro/src/html_tree/html_list.rs b/yew-macro/src/html_tree/html_list.rs index 6303bd1dd30..436163dfff1 100644 --- a/yew-macro/src/html_tree/html_list.rs +++ b/yew-macro/src/html_tree/html_list.rs @@ -4,7 +4,7 @@ use crate::{Peek, PeekValue}; use boolinator::Boolinator; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Expr, Token}; @@ -23,7 +23,7 @@ impl PeekValue<()> for HtmlList { } impl Parse for HtmlList { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { if HtmlListClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( @@ -101,7 +101,7 @@ impl PeekValue<()> for HtmlListOpen { } impl Parse for HtmlListOpen { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let lt = input.parse()?; let HtmlPropSuffix { stream, gt, .. } = input.parse()?; let props = syn::parse2(stream)?; @@ -120,7 +120,7 @@ struct HtmlListProps { key: Option, } impl Parse for HtmlListProps { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let key = if input.is_empty() { None } else { @@ -164,7 +164,7 @@ impl PeekValue<()> for HtmlListClose { } impl Parse for HtmlListClose { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { Ok(HtmlListClose { lt: input.parse()?, div: input.parse()?, diff --git a/yew-macro/src/html_tree/html_prop.rs b/yew-macro/src/html_tree/html_prop.rs index 020cbd725ba..02ed4ffba99 100644 --- a/yew-macro/src/html_tree/html_prop.rs +++ b/yew-macro/src/html_tree/html_prop.rs @@ -1,12 +1,12 @@ -use crate::html_tree::HtmlDashedName as HtmlPropLabel; +use crate::html_tree::HtmlDashedName; use crate::{Peek, PeekValue}; use proc_macro2::{TokenStream, TokenTree}; use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::{Expr, Token}; pub struct HtmlProp { - pub label: HtmlPropLabel, + pub label: HtmlDashedName, pub question_mark: Option, pub value: Expr, } @@ -28,13 +28,13 @@ impl HtmlProp { impl PeekValue<()> for HtmlProp { fn peek(cursor: Cursor) -> Option<()> { - HtmlPropLabel::peek(cursor).map(|_| ()) + HtmlDashedName::peek(cursor).map(|_| ()) } } impl Parse for HtmlProp { - fn parse(input: ParseStream) -> ParseResult { - let label = input.parse::()?; + fn parse(input: ParseStream) -> syn::Result { + let label = input.parse::()?; let question_mark = input.parse::().ok(); let equals = input .parse::() @@ -63,7 +63,7 @@ pub struct HtmlPropSuffix { } impl Parse for HtmlPropSuffix { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let mut trees: Vec = vec![]; let mut div: Option = None; let mut angle_count = 1; diff --git a/yew-macro/src/html_tree/html_tag/mod.rs b/yew-macro/src/html_tree/html_tag/mod.rs index 97e6154e9f4..a2ab9efed41 100644 --- a/yew-macro/src/html_tree/html_tag/mod.rs +++ b/yew-macro/src/html_tree/html_tag/mod.rs @@ -1,8 +1,6 @@ mod tag_attributes; -use super::{ - HtmlChildrenTree, HtmlDashedName, HtmlProp as TagAttribute, HtmlPropSuffix as TagSuffix, -}; +use super::{HtmlChildrenTree, HtmlDashedName, HtmlProp, HtmlPropSuffix}; use crate::stringify::Stringify; use crate::{non_capitalized_ascii, stringify, Peek, PeekValue}; use boolinator::Boolinator; @@ -188,7 +186,7 @@ impl ToTokens for HtmlTag { None } else { let attrs = attributes.iter().map( - |TagAttribute { + |HtmlProp { label, question_mark, value, @@ -217,7 +215,7 @@ impl ToTokens for HtmlTag { } else { let tokens = booleans .iter() - .map(|TagAttribute { label, value, .. }| { + .map(|HtmlProp { label, value, .. }| { let label_str = label.to_lit_str(); let sr = label.stringify(); quote_spanned! {value.span()=> { @@ -279,7 +277,7 @@ impl ToTokens for HtmlTag { let add_listeners = listeners .iter() .map( - |TagAttribute { + |HtmlProp { label, question_mark, value, @@ -311,7 +309,7 @@ impl ToTokens for HtmlTag { } else { let listeners_it = listeners .iter() - .map(|TagAttribute { label, value, .. }| to_wrapped_listener(&label.name, value)); + .map(|HtmlProp { label, value, .. }| to_wrapped_listener(&label.name, value)); Some(quote! { #vtag.add_listeners(::std::vec![#(#listeners_it),*]); @@ -518,7 +516,7 @@ impl Parse for HtmlTagOpen { fn parse(input: ParseStream) -> ParseResult { let lt = input.parse::()?; let tag_name = input.parse::()?; - let TagSuffix { stream, div, gt } = input.parse()?; + let HtmlPropSuffix { stream, div, gt } = input.parse()?; let mut attributes: TagAttributes = syn::parse2(stream)?; match &tag_name { @@ -529,7 +527,7 @@ impl Parse for HtmlTagOpen { "input" | "textarea" => {} _ => { if let Some(attr) = attributes.value.take() { - attributes.attributes.push(TagAttribute { + attributes.attributes.push(HtmlProp { label: HtmlDashedName::new(Ident::new("value", Span::call_site())), question_mark: attr.question_mark, value: attr.value, diff --git a/yew-macro/src/html_tree/html_tag/tag_attributes.rs b/yew-macro/src/html_tree/html_tag/tag_attributes.rs index 6ecc2f91081..6d49c896a2e 100644 --- a/yew-macro/src/html_tree/html_tag/tag_attributes.rs +++ b/yew-macro/src/html_tree/html_tag/tag_attributes.rs @@ -3,7 +3,7 @@ use crate::PeekValue; use lazy_static::lazy_static; use std::collections::HashSet; use std::iter::FromIterator; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::{Expr, ExprTuple}; pub struct TagAttributes { @@ -272,7 +272,7 @@ impl TagAttributes { } impl Parse for TagAttributes { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let mut attributes: Vec = Vec::new(); while TagAttribute::peek(input.cursor()).is_some() { attributes.push(input.parse::()?); From 273d7a0589bedd3845bdb5b00b051ee2782df865 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Thu, 24 Sep 2020 23:57:32 +0200 Subject: [PATCH 02/39] just a prototype --- yew-macro/src/html_tree/mod.rs | 2 +- yew-macro/src/lib.rs | 9 +++- yew-macro/src/props/component.rs | 45 ++++++++++++++++++++ yew-macro/src/props/mod.rs | 4 ++ yew-macro/src/props/prop.rs | 70 ++++++++++++++++++++++++++++++++ yew-macro/tests/props.rs | 18 ++++++++ yew/src/lib.rs | 2 + 7 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 yew-macro/src/props/component.rs create mode 100644 yew-macro/src/props/mod.rs create mode 100644 yew-macro/src/props/prop.rs create mode 100644 yew-macro/tests/props.rs diff --git a/yew-macro/src/html_tree/mod.rs b/yew-macro/src/html_tree/mod.rs index f118fc2e682..a014ce5eab7 100644 --- a/yew-macro/src/html_tree/mod.rs +++ b/yew-macro/src/html_tree/mod.rs @@ -10,7 +10,7 @@ mod html_tag; use crate::PeekValue; use html_block::HtmlBlock; use html_component::HtmlComponent; -use html_dashed_name::HtmlDashedName; +pub use html_dashed_name::HtmlDashedName; use html_iterable::HtmlIterable; use html_list::HtmlList; use html_node::HtmlNode; diff --git a/yew-macro/src/lib.rs b/yew-macro/src/lib.rs index efd74022572..012bdce771e 100644 --- a/yew-macro/src/lib.rs +++ b/yew-macro/src/lib.rs @@ -55,10 +55,9 @@ //! //! Please refer to [https://github.com/yewstack/yew](https://github.com/yewstack/yew) for how to set this up. -#![recursion_limit = "128"] - mod derive_props; mod html_tree; +mod props; mod stringify; use derive_props::DerivePropsInput; @@ -103,3 +102,9 @@ pub fn html(input: TokenStream) -> TokenStream { let root = parse_macro_input!(input as HtmlRootVNode); TokenStream::from(quote! {#root}) } + +#[proc_macro] +pub fn props(input: TokenStream) -> TokenStream { + let root = parse_macro_input!(input as props::ComponentProps); + TokenStream::from(quote! {#root}) +} diff --git a/yew-macro/src/props/component.rs b/yew-macro/src/props/component.rs new file mode 100644 index 00000000000..cc33653901a --- /dev/null +++ b/yew-macro/src/props/component.rs @@ -0,0 +1,45 @@ +use super::{HtmlProp, HtmlPropList}; +use proc_macro2::TokenStream; +use quote::{quote_spanned, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Type, +}; + +pub struct ComponentProps { + ty: Type, + props: HtmlPropList, +} +impl Parse for ComponentProps { + fn parse(input: ParseStream) -> syn::Result { + let ty = input.parse()?; + let props: HtmlPropList = input.parse()?; + for prop in props.iter() { + if prop.question_mark.is_some() { + return Err(syn::Error::new_spanned( + &prop.label, + "optional attributes are only supported on HTML tags. Yew components can use `Option` properties to accomplish the same thing.", + )); + } + } + + Ok(Self { ty, props }) + } +} +impl ToTokens for ComponentProps { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { ty, props } = self; + let set_props = props.iter().map(|HtmlProp { label, value, .. }| { + quote_spanned! {value.span()=> + .#label(<::yew::virtual_dom::VComp as ::yew::virtual_dom::Transformer<_, _>>::transform(#value)) + } + }); + + tokens.extend(quote_spanned! {ty.span()=> + <#ty as ::yew::html::Properties>::builder() + #(#set_props)* + .build() + }) + } +} diff --git a/yew-macro/src/props/mod.rs b/yew-macro/src/props/mod.rs new file mode 100644 index 00000000000..1420154e573 --- /dev/null +++ b/yew-macro/src/props/mod.rs @@ -0,0 +1,4 @@ +mod component; +pub use component::ComponentProps; +mod prop; +use prop::*; diff --git a/yew-macro/src/props/prop.rs b/yew-macro/src/props/prop.rs new file mode 100644 index 00000000000..ebf298acff0 --- /dev/null +++ b/yew-macro/src/props/prop.rs @@ -0,0 +1,70 @@ +use crate::html_tree::HtmlDashedName; +use syn::parse::{Parse, ParseStream}; +use syn::{Expr, Token}; + +pub struct HtmlProp { + pub label: HtmlDashedName, + pub question_mark: Option, + pub value: Expr, +} +impl HtmlProp { + /// Checks if the prop uses the optional attribute syntax. + /// If it does, an error is returned. + pub fn ensure_not_optional(&self) -> syn::Result<()> { + if self.question_mark.is_some() { + let msg = format!( + "the `{}` attribute does not support being used as an optional attribute", + self.label + ); + Err(syn::Error::new_spanned(&self.label, msg)) + } else { + Ok(()) + } + } +} +impl Parse for HtmlProp { + fn parse(input: ParseStream) -> syn::Result { + let label = input.parse::()?; + let question_mark = input.parse::().ok(); + let equals = input.parse::().map_err(|_| { + syn::Error::new_spanned( + &label, + "this prop doesn't have a value. \ + In case of boolean attributes, set the value to `true` or `false` \ + to control whether or not it will be present", + ) + })?; + if input.is_empty() { + return Err(syn::Error::new_spanned( + equals, + "expected an expression following this equals sign", + )); + } + let value = input.parse::()?; + Ok(Self { + label, + question_mark, + value, + }) + } +} + +pub struct HtmlPropList(Vec); +impl HtmlPropList { + pub fn iter(&self) -> std::slice::Iter { + self.0.iter() + } +} +impl Parse for HtmlPropList { + fn parse(input: ParseStream) -> syn::Result { + let mut props: Vec = Vec::new(); + + while !input.is_empty() { + props.push(input.parse()?); + } + + props.sort_by(|a, b| a.label.to_string().cmp(&b.label.to_string())); + + Ok(Self(props)) + } +} diff --git a/yew-macro/tests/props.rs b/yew-macro/tests/props.rs new file mode 100644 index 00000000000..c87513d92d4 --- /dev/null +++ b/yew-macro/tests/props.rs @@ -0,0 +1,18 @@ +use yew::prelude::*; + +#[derive(Clone, Properties, Debug, Default, PartialEq)] +pub struct ChildProperties { + #[prop_or_default] + pub string: String, + pub int: i32, + #[prop_or_default] + pub opt_str: Option, + #[prop_or_default] + pub vec: Vec, + #[prop_or_default] + pub optional_callback: Option>, +} + +fn main() { + let props = yew::props!(ChildProperties int=5 opt_str="Hello World!"); +} diff --git a/yew/src/lib.rs b/yew/src/lib.rs index d4d65664f70..26ec04af672 100644 --- a/yew/src/lib.rs +++ b/yew/src/lib.rs @@ -194,6 +194,8 @@ pub use yew_macro::html; /// [`ChildrenRenderer`]: ./html/struct.ChildrenRenderer.html pub use yew_macro::html_nested; +pub use yew_macro::props; + /// This module contains macros which implements html! macro and JSX-like templates pub mod macros { pub use crate::html; From 9ce788fafc21bce1241875f85cebf469a5c68f20 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Thu, 1 Oct 2020 17:55:19 +0200 Subject: [PATCH 03/39] cleanup --- yew-macro/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yew-macro/src/lib.rs b/yew-macro/src/lib.rs index 012bdce771e..3ec172c43e5 100644 --- a/yew-macro/src/lib.rs +++ b/yew-macro/src/lib.rs @@ -63,7 +63,7 @@ mod stringify; use derive_props::DerivePropsInput; use html_tree::{HtmlRoot, HtmlRootVNode}; use proc_macro::TokenStream; -use quote::{quote, ToTokens}; +use quote::ToTokens; use syn::buffer::Cursor; use syn::parse_macro_input; @@ -94,17 +94,17 @@ pub fn derive_props(input: TokenStream) -> TokenStream { #[proc_macro] pub fn html_nested(input: TokenStream) -> TokenStream { let root = parse_macro_input!(input as HtmlRoot); - TokenStream::from(quote! {#root}) + TokenStream::from(root.into_token_stream()) } #[proc_macro] pub fn html(input: TokenStream) -> TokenStream { let root = parse_macro_input!(input as HtmlRootVNode); - TokenStream::from(quote! {#root}) + TokenStream::from(root.into_token_stream()) } #[proc_macro] pub fn props(input: TokenStream) -> TokenStream { let root = parse_macro_input!(input as props::ComponentProps); - TokenStream::from(quote! {#root}) + TokenStream::from(root.into_token_stream()) } From eefa6eed5a98a8afa1218008d0a0505e48b2e506 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Fri, 2 Oct 2020 16:23:18 +0200 Subject: [PATCH 04/39] add prop type resolver --- yew-macro/src/lib.rs | 4 +- yew-macro/src/props/component.rs | 46 +++++++++++++++++-- yew-macro/tests/macro/test_component.rs | 29 ------------ yew-macro/tests/props.rs | 18 -------- .../tests/props_macro/resolve-prop-fail.rs | 43 +++++++++++++++++ .../props_macro/resolve-prop-fail.stderr | 17 +++++++ .../tests/props_macro/resolve-prop-pass.rs | 38 +++++++++++++++ yew-macro/tests/props_macro_test.rs | 7 +++ 8 files changed, 150 insertions(+), 52 deletions(-) delete mode 100644 yew-macro/tests/macro/test_component.rs delete mode 100644 yew-macro/tests/props.rs create mode 100644 yew-macro/tests/props_macro/resolve-prop-fail.rs create mode 100644 yew-macro/tests/props_macro/resolve-prop-fail.stderr create mode 100644 yew-macro/tests/props_macro/resolve-prop-pass.rs create mode 100644 yew-macro/tests/props_macro_test.rs diff --git a/yew-macro/src/lib.rs b/yew-macro/src/lib.rs index 3ec172c43e5..06314b30533 100644 --- a/yew-macro/src/lib.rs +++ b/yew-macro/src/lib.rs @@ -105,6 +105,6 @@ pub fn html(input: TokenStream) -> TokenStream { #[proc_macro] pub fn props(input: TokenStream) -> TokenStream { - let root = parse_macro_input!(input as props::ComponentProps); - TokenStream::from(root.into_token_stream()) + let props = parse_macro_input!(input as props::ComponentProps); + TokenStream::from(props.into_token_stream()) } diff --git a/yew-macro/src/props/component.rs b/yew-macro/src/props/component.rs index cc33653901a..45629c7e351 100644 --- a/yew-macro/src/props/component.rs +++ b/yew-macro/src/props/component.rs @@ -3,17 +3,57 @@ use proc_macro2::TokenStream; use quote::{quote_spanned, ToTokens}; use syn::{ parse::{Parse, ParseStream}, + punctuated::Punctuated, spanned::Spanned, - Type, + TypePath, }; +/// Pop from `Punctuated` without leaving it in a state where it has trailing punctuation. +fn pop_last_punctuated(punctuated: &mut Punctuated) -> Option { + let value = punctuated.pop().map(|pair| pair.into_value()); + // remove the 2nd last value and push it right back to remove the trailing punctuation + if let Some(pair) = punctuated.pop() { + punctuated.push_value(pair.into_value()); + } + value +} + +/// Check if the given type path looks like an associated type. +fn is_associated_properties(ty: &TypePath) -> bool { + let mut segments_it = ty.path.segments.iter(); + if let Some(seg) = segments_it.next_back() { + // if the last segment is `Properties` ... + if seg.ident == "Properties" { + if let Some(seg) = segments_it.next_back() { + // ... and we can be reasonably sure that the previous segment is a component ... + if !crate::non_capitalized_ascii(&seg.ident.to_string()) { + // ... then we assume that this is an associated type like `Component::Properties` + return true; + } + } + } + } + + false +} + pub struct ComponentProps { - ty: Type, + ty: TypePath, props: HtmlPropList, } impl Parse for ComponentProps { fn parse(input: ParseStream) -> syn::Result { - let ty = input.parse()?; + let mut ty: TypePath = input.parse()?; + + // if the type isn't already qualified (``) and it's an associated type (`MyComp::Properties`) ... + if ty.qself.is_none() && is_associated_properties(&ty) { + pop_last_punctuated(&mut ty.path.segments); + // .. transform it into a "qualified-self" type + ty = syn::parse2(quote_spanned! {ty.span()=> + <#ty as ::yew::html::Component>::Properties + })?; + } + let props: HtmlPropList = input.parse()?; for prop in props.iter() { if prop.question_mark.is_some() { diff --git a/yew-macro/tests/macro/test_component.rs b/yew-macro/tests/macro/test_component.rs deleted file mode 100644 index b4afc83b6ca..00000000000 --- a/yew-macro/tests/macro/test_component.rs +++ /dev/null @@ -1,29 +0,0 @@ -use yew::prelude::*; - -#[derive(Clone, Properties, PartialEq)] -pub struct TestProperties { - pub string: String, - pub int: i32, -} - -pub struct TestComponent; -impl Component for TestComponent { - type Message = (); - type Properties = TestProperties; - - fn create(_: Self::Properties, _: ComponentLink) -> Self { - TestComponent - } - - fn update(&mut self, _: Self::Message) -> ShouldRender { - unimplemented!() - } - - fn change(&mut self, _: Self::Properties) -> ShouldRender { - unimplemented!() - } - - fn view(&self) -> Html { - unimplemented!() - } -} diff --git a/yew-macro/tests/props.rs b/yew-macro/tests/props.rs deleted file mode 100644 index c87513d92d4..00000000000 --- a/yew-macro/tests/props.rs +++ /dev/null @@ -1,18 +0,0 @@ -use yew::prelude::*; - -#[derive(Clone, Properties, Debug, Default, PartialEq)] -pub struct ChildProperties { - #[prop_or_default] - pub string: String, - pub int: i32, - #[prop_or_default] - pub opt_str: Option, - #[prop_or_default] - pub vec: Vec, - #[prop_or_default] - pub optional_callback: Option>, -} - -fn main() { - let props = yew::props!(ChildProperties int=5 opt_str="Hello World!"); -} diff --git a/yew-macro/tests/props_macro/resolve-prop-fail.rs b/yew-macro/tests/props_macro/resolve-prop-fail.rs new file mode 100644 index 00000000000..dd5e0fbf1ba --- /dev/null +++ b/yew-macro/tests/props_macro/resolve-prop-fail.rs @@ -0,0 +1,43 @@ +use yew::prelude::*; + +#[derive(Clone, Properties)] +struct Props {} + +struct MyComp; +impl Component for MyComp { + type Message = (); + type Properties = Props; + + fn create(_props: Self::Properties, _link: ComponentLink) -> Self { + unimplemented!() + } + + fn update(&mut self, _msg: Self::Message) -> ShouldRender { + unimplemented!() + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + unimplemented!() + } + + fn view(&self) -> Html { + unimplemented!() + } +} + +trait NotAComponent { + type Properties; +} + +struct MyNotAComponent; +impl NotAComponent for MyNotAComponent { + type Properties = (); +} + +fn compile_fail() { + yew::props!(Vec<_>); + yew::props!(MyComp); + yew::props!(MyNotAComponent::Properties); +} + +fn main() {} diff --git a/yew-macro/tests/props_macro/resolve-prop-fail.stderr b/yew-macro/tests/props_macro/resolve-prop-fail.stderr new file mode 100644 index 00000000000..97e4303fda4 --- /dev/null +++ b/yew-macro/tests/props_macro/resolve-prop-fail.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `std::vec::Vec<_>: yew::html::Properties` is not satisfied + --> $DIR/resolve-prop-fail.rs:38:17 + | +38 | yew::props!(Vec<_>); + | ^^^ the trait `yew::html::Properties` is not implemented for `std::vec::Vec<_>` + +error[E0277]: the trait bound `MyComp: yew::html::Properties` is not satisfied + --> $DIR/resolve-prop-fail.rs:39:17 + | +39 | yew::props!(MyComp); + | ^^^^^^ the trait `yew::html::Properties` is not implemented for `MyComp` + +error[E0277]: the trait bound `MyNotAComponent: yew::html::Component` is not satisfied + --> $DIR/resolve-prop-fail.rs:40:17 + | +40 | yew::props!(MyNotAComponent::Properties); + | ^^^^^^^^^^^^^^^ the trait `yew::html::Component` is not implemented for `MyNotAComponent` diff --git a/yew-macro/tests/props_macro/resolve-prop-pass.rs b/yew-macro/tests/props_macro/resolve-prop-pass.rs new file mode 100644 index 00000000000..d7fb0241c41 --- /dev/null +++ b/yew-macro/tests/props_macro/resolve-prop-pass.rs @@ -0,0 +1,38 @@ +use yew::prelude::*; + +#[derive(Clone, Properties)] +struct Props { + n: i32, +} + +struct MyComp; +impl Component for MyComp { + type Message = (); + type Properties = Props; + + fn create(_props: Self::Properties, _link: ComponentLink) -> Self { + unimplemented!() + } + + fn update(&mut self, _msg: Self::Message) -> ShouldRender { + unimplemented!() + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + unimplemented!() + } + + fn view(&self) -> Html { + unimplemented!() + } +} + +fn compile_pass() { + yew::props!(Props n=1); + yew::props!(self::Props n=1); + yew::props!(MyComp::Properties n=2); + yew::props!(self::MyComp::Properties n=3); + yew::props!(::Properties n=5); +} + +fn main() {} diff --git a/yew-macro/tests/props_macro_test.rs b/yew-macro/tests/props_macro_test.rs new file mode 100644 index 00000000000..a5c7ee2bb03 --- /dev/null +++ b/yew-macro/tests/props_macro_test.rs @@ -0,0 +1,7 @@ +#[rustversion::attr(stable(1.45), test)] +fn tests() { + let t = trybuild::TestCases::new(); + + t.pass("tests/props_macro/resolve-prop-pass.rs"); + t.compile_fail("tests/props_macro/resolve-prop-fail.rs"); +} From 7914e15a001f4740283dab5fb76ac35cd59ed6fa Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Sat, 3 Oct 2020 03:20:19 +0200 Subject: [PATCH 05/39] use new props for tags --- yew-macro/src/html_tree/html_block.rs | 7 +-- yew-macro/src/html_tree/html_list.rs | 5 +- yew-macro/src/html_tree/html_tag/mod.rs | 19 +++---- .../src/html_tree/html_tag/tag_attributes.rs | 50 ++++++++----------- yew-macro/src/props/component.rs | 27 ++++++---- yew-macro/src/props/mod.rs | 2 +- yew-macro/src/props/prop.rs | 4 ++ 7 files changed, 56 insertions(+), 58 deletions(-) diff --git a/yew-macro/src/html_tree/html_block.rs b/yew-macro/src/html_tree/html_block.rs index b7109380924..07bbab0414d 100644 --- a/yew-macro/src/html_tree/html_block.rs +++ b/yew-macro/src/html_tree/html_block.rs @@ -1,13 +1,10 @@ -use super::html_iterable::HtmlIterable; -use super::html_node::HtmlNode; -use super::ToNodeIterator; +use super::{HtmlIterable, HtmlNode, ToNodeIterator}; use crate::PeekValue; use proc_macro2::Delimiter; use quote::{quote, quote_spanned, ToTokens}; -use syn::braced; use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream}; -use syn::token; +use syn::{braced, token}; pub struct HtmlBlock { content: BlockContent, diff --git a/yew-macro/src/html_tree/html_list.rs b/yew-macro/src/html_tree/html_list.rs index 436163dfff1..38bcd6b53a3 100644 --- a/yew-macro/src/html_tree/html_list.rs +++ b/yew-macro/src/html_tree/html_list.rs @@ -1,6 +1,5 @@ -use super::{html_dashed_name::HtmlDashedName, HtmlChildrenTree}; -use crate::html_tree::{HtmlProp, HtmlPropSuffix}; -use crate::{Peek, PeekValue}; +use super::{html_dashed_name::HtmlDashedName, HtmlChildrenTree, HtmlPropSuffix}; +use crate::{props::HtmlProp, Peek, PeekValue}; use boolinator::Boolinator; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; diff --git a/yew-macro/src/html_tree/html_tag/mod.rs b/yew-macro/src/html_tree/html_tag/mod.rs index a2ab9efed41..157f164ab37 100644 --- a/yew-macro/src/html_tree/html_tag/mod.rs +++ b/yew-macro/src/html_tree/html_tag/mod.rs @@ -1,15 +1,16 @@ -mod tag_attributes; - -use super::{HtmlChildrenTree, HtmlDashedName, HtmlProp, HtmlPropSuffix}; +use super::{HtmlChildrenTree, HtmlDashedName, HtmlPropSuffix}; +use crate::props::HtmlProp; use crate::stringify::Stringify; use crate::{non_capitalized_ascii, stringify, Peek, PeekValue}; use boolinator::Boolinator; use proc_macro2::{Delimiter, Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result as ParseResult}; +use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; use syn::{Block, Ident, Token}; + +mod tag_attributes; use tag_attributes::{ClassesForm, TagAttributes}; pub struct HtmlTag { @@ -27,7 +28,7 @@ impl PeekValue<()> for HtmlTag { } impl Parse for HtmlTag { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { if HtmlTagClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( @@ -414,7 +415,7 @@ impl Peek<'_, ()> for DynamicName { } impl Parse for DynamicName { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let at = input.parse()?; // the expression block is optional, closing tags don't have it. let expr = if input.cursor().group(Delimiter::Brace).is_some() { @@ -465,7 +466,7 @@ impl Peek<'_, TagKey> for TagName { } impl Parse for TagName { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { if DynamicName::peek(input.cursor()).is_some() { DynamicName::parse(input).map(Self::Expr) } else { @@ -513,7 +514,7 @@ impl PeekValue for HtmlTagOpen { } impl Parse for HtmlTagOpen { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let lt = input.parse::()?; let tag_name = input.parse::()?; let HtmlPropSuffix { stream, div, gt } = input.parse()?; @@ -591,7 +592,7 @@ impl PeekValue for HtmlTagClose { } impl Parse for HtmlTagClose { - fn parse(input: ParseStream) -> ParseResult { + fn parse(input: ParseStream) -> syn::Result { let lt = input.parse()?; let div = input.parse()?; let tag_name = input.parse()?; diff --git a/yew-macro/src/html_tree/html_tag/tag_attributes.rs b/yew-macro/src/html_tree/html_tag/tag_attributes.rs index 6d49c896a2e..d1f030b2f47 100644 --- a/yew-macro/src/html_tree/html_tag/tag_attributes.rs +++ b/yew-macro/src/html_tree/html_tag/tag_attributes.rs @@ -1,5 +1,4 @@ -use crate::html_tree::HtmlProp as TagAttribute; -use crate::PeekValue; +use crate::props::{HtmlProp, HtmlPropList}; use lazy_static::lazy_static; use std::collections::HashSet; use std::iter::FromIterator; @@ -7,12 +6,12 @@ use syn::parse::{Parse, ParseStream}; use syn::{Expr, ExprTuple}; pub struct TagAttributes { - pub attributes: Vec, - pub listeners: Vec, + pub attributes: Vec, + pub listeners: Vec, pub classes: Option, - pub booleans: Vec, - pub value: Option, - pub kind: Option, + pub booleans: Vec, + pub value: Option, + pub kind: Option, pub checked: Option, pub node_ref: Option, pub key: Option, @@ -213,7 +212,7 @@ lazy_static! { } impl TagAttributes { - fn drain_listeners(attrs: &mut Vec) -> Vec { + fn drain_listeners(attrs: &mut Vec) -> Vec { let mut i = 0; let mut drained = Vec::new(); while i < attrs.len() { @@ -227,7 +226,7 @@ impl TagAttributes { drained } - fn drain_boolean(attrs: &mut Vec) -> Vec { + fn drain_boolean(attrs: &mut Vec) -> Vec { let mut i = 0; let mut drained = Vec::new(); while i < attrs.len() { @@ -241,7 +240,7 @@ impl TagAttributes { drained } - fn remove_attr(attrs: &mut Vec, name: &str) -> Option { + fn remove_attr(attrs: &mut Vec, name: &str) -> Option { let mut i = 0; while i < attrs.len() { if attrs[i].label.to_string() == name { @@ -254,9 +253,9 @@ impl TagAttributes { } fn remove_attr_nonoptional( - attrs: &mut Vec, + attrs: &mut Vec, name: &str, - ) -> syn::Result> { + ) -> syn::Result> { match Self::remove_attr(attrs, name) { Some(attr) => attr.ensure_not_optional().map(|_| Some(attr)), None => Ok(None), @@ -273,10 +272,7 @@ impl TagAttributes { impl Parse for TagAttributes { fn parse(input: ParseStream) -> syn::Result { - let mut attributes: Vec = Vec::new(); - while TagAttribute::peek(input.cursor()).is_some() { - attributes.push(input.parse::()?); - } + let mut attributes = input.parse::()?.into_inner(); let mut listeners = Vec::new(); for listener in Self::drain_listeners(&mut attributes) { @@ -298,23 +294,19 @@ impl Parse for TagAttributes { } // Multiple listener attributes are allowed, but no others - attributes.sort_by(|a, b| { - a.label - .to_string() - .partial_cmp(&b.label.to_string()) - .unwrap() - }); - let mut i = 0; - while i + 1 < attributes.len() { - if attributes[i].label.to_string() == attributes[i + 1].label.to_string() { - let label = &attributes[i + 1].label; + for pair in attributes.windows(2) { + let (label_a, label_b) = (pair[0].label.to_lit_str(), pair[1].label.to_lit_str()); + if label_a == label_b { return Err(syn::Error::new_spanned( - label, - format!("the attribute `{}` can only be specified once", label), + label_b, + format!( + "the attribute `{}` can only be specified once", + label_b.value() + ), )); } - i += 1; } + let booleans = Self::drain_boolean(&mut attributes); for attr in &booleans { if attr.question_mark.is_some() { diff --git a/yew-macro/src/props/component.rs b/yew-macro/src/props/component.rs index 45629c7e351..db31e8736db 100644 --- a/yew-macro/src/props/component.rs +++ b/yew-macro/src/props/component.rs @@ -41,6 +41,21 @@ pub struct ComponentProps { ty: TypePath, props: HtmlPropList, } +impl ComponentProps { + pub fn parse_for_properties(input: ParseStream, ty: TypePath) -> syn::Result { + let props: HtmlPropList = input.parse()?; + for prop in props.iter() { + if prop.question_mark.is_some() { + return Err(syn::Error::new_spanned( + &prop.label, + "optional attributes are only supported on HTML tags. Yew components can use `Option` properties to accomplish the same thing.", + )); + } + } + + Ok(Self { ty, props }) + } +} impl Parse for ComponentProps { fn parse(input: ParseStream) -> syn::Result { let mut ty: TypePath = input.parse()?; @@ -54,17 +69,7 @@ impl Parse for ComponentProps { })?; } - let props: HtmlPropList = input.parse()?; - for prop in props.iter() { - if prop.question_mark.is_some() { - return Err(syn::Error::new_spanned( - &prop.label, - "optional attributes are only supported on HTML tags. Yew components can use `Option` properties to accomplish the same thing.", - )); - } - } - - Ok(Self { ty, props }) + Self::parse_for_properties(input, ty) } } impl ToTokens for ComponentProps { diff --git a/yew-macro/src/props/mod.rs b/yew-macro/src/props/mod.rs index 1420154e573..7923ccec748 100644 --- a/yew-macro/src/props/mod.rs +++ b/yew-macro/src/props/mod.rs @@ -1,4 +1,4 @@ mod component; pub use component::ComponentProps; mod prop; -use prop::*; +pub use prop::*; diff --git a/yew-macro/src/props/prop.rs b/yew-macro/src/props/prop.rs index ebf298acff0..3b8c56f005a 100644 --- a/yew-macro/src/props/prop.rs +++ b/yew-macro/src/props/prop.rs @@ -51,6 +51,10 @@ impl Parse for HtmlProp { pub struct HtmlPropList(Vec); impl HtmlPropList { + pub fn into_inner(self) -> Vec { + self.0 + } + pub fn iter(&self) -> std::slice::Iter { self.0.iter() } From f3b8717648fe4bec3f485ff1bea59d42d2996c70 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Tue, 6 Oct 2020 17:59:43 +0200 Subject: [PATCH 06/39] silence clippy --- yew/src/services/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/yew/src/services/mod.rs b/yew/src/services/mod.rs index ae5c36d3f3c..d983f3fc916 100644 --- a/yew/src/services/mod.rs +++ b/yew/src/services/mod.rs @@ -41,6 +41,7 @@ use std::time::Duration; /// /// All tasks must be handled when they are cancelled, which is why the `Drop` trait is required. /// Tasks should cancel themselves in their implementation of the `Drop` trait. +#[allow(clippy::drop_bounds)] pub trait Task: Drop { /// Returns `true` if task is active. fn is_active(&self) -> bool; From a587c41ffc6c11ce5e0a2417e4c34c9684aee876 Mon Sep 17 00:00:00 2001 From: Simon Berger Date: Tue, 6 Oct 2020 18:00:02 +0200 Subject: [PATCH 07/39] simplify tag parsing --- yew-macro/src/html_tree/html_component.rs | 87 +++++------ yew-macro/src/html_tree/html_list.rs | 77 ++++------ yew-macro/src/html_tree/html_prop.rs | 106 -------------- yew-macro/src/html_tree/html_tag/mod.rs | 138 +++++++++--------- .../src/html_tree/html_tag/tag_attributes.rs | 2 +- yew-macro/src/html_tree/mod.rs | 18 +-- yew-macro/src/html_tree/tag.rs | 89 +++++++++++ yew-macro/tests/props_macro_test.rs | 1 + yew-router/src/components/router_button.rs | 6 +- yew-router/src/components/router_link.rs | 8 +- 10 files changed, 241 insertions(+), 291 deletions(-) delete mode 100644 yew-macro/src/html_tree/html_prop.rs create mode 100644 yew-macro/src/html_tree/tag.rs diff --git a/yew-macro/src/html_tree/html_component.rs b/yew-macro/src/html_tree/html_component.rs index 860a62a3b86..2659ea338bf 100644 --- a/yew-macro/src/html_tree/html_component.rs +++ b/yew-macro/src/html_tree/html_component.rs @@ -1,15 +1,13 @@ -use super::HtmlChildrenTree; -use super::HtmlProp; -use super::HtmlPropSuffix; -use crate::PeekValue; +use super::{HtmlChildrenTree, TagTokens}; +use crate::{props::HtmlProp, PeekValue}; use boolinator::Boolinator; use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; use std::cmp::Ordering; -use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; use syn::spanned::Spanned; +use syn::{buffer::Cursor, parse::Parser}; use syn::{ AngleBracketedGenericArguments, Expr, GenericArgument, Ident, Path, PathArguments, PathSegment, Token, Type, TypePath, @@ -34,7 +32,7 @@ impl Parse for HtmlComponent { if HtmlComponentClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( - close, + close.to_spanned(), "this closing tag has no corresponding opening tag", )), Err(err) => Err(err), @@ -43,7 +41,7 @@ impl Parse for HtmlComponent { let open = input.parse::()?; // Return early if it's a self-closing tag - if open.div.is_some() { + if open.is_self_closing() { return Ok(HtmlComponent { ty: open.ty, props: open.props, @@ -55,7 +53,7 @@ impl Parse for HtmlComponent { loop { if input.is_empty() { return Err(syn::Error::new_spanned( - open, + open.to_spanned(), "this opening tag has no corresponding closing tag", )); } @@ -260,11 +258,18 @@ impl HtmlComponent { } struct HtmlComponentOpen { - lt: Token![<], + tokens: TagTokens, ty: Type, props: Props, - div: Option, - gt: Token![>], +} +impl HtmlComponentOpen { + fn is_self_closing(&self) -> bool { + self.tokens.div.is_some() + } + + fn to_spanned(&self) -> impl ToTokens { + self.tokens.to_spanned() + } } impl PeekValue for HtmlComponentOpen { @@ -278,35 +283,27 @@ impl PeekValue for HtmlComponentOpen { impl Parse for HtmlComponentOpen { fn parse(input: ParseStream) -> syn::Result { - let lt = input.parse::()?; - let ty = input.parse()?; - // backwards compat - let _ = input.parse::(); - let HtmlPropSuffix { stream, div, gt } = input.parse()?; - let props = syn::parse2(stream)?; - - Ok(HtmlComponentOpen { - lt, - ty, - props, - div, - gt, - }) - } -} + let (tokens, content) = TagTokens::parse_start(input)?; -impl ToTokens for HtmlComponentOpen { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let HtmlComponentOpen { lt, gt, .. } = self; - tokens.extend(quote! {#lt#gt}); + let content_parser = |input: ParseStream| -> syn::Result { + let ty = input.parse()?; + let props = input.parse()?; + + Ok(Self { tokens, ty, props }) + }; + + content_parser.parse2(content) } } struct HtmlComponentClose { - lt: Token![<], - div: Token![/], - ty: Type, - gt: Token![>], + tokens: TagTokens, + _ty: Type, +} +impl HtmlComponentClose { + fn to_spanned(&self) -> impl ToTokens { + self.tokens.to_spanned() + } } impl PeekValue for HtmlComponentClose { @@ -327,19 +324,9 @@ impl PeekValue for HtmlComponentClose { } impl Parse for HtmlComponentClose { fn parse(input: ParseStream) -> syn::Result { - Ok(HtmlComponentClose { - lt: input.parse()?, - div: input.parse()?, - ty: input.parse()?, - gt: input.parse()?, - }) - } -} - -impl ToTokens for HtmlComponentClose { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let HtmlComponentClose { lt, div, ty, gt } = self; - tokens.extend(quote! {#lt#div#ty#gt}); + let (tokens, content) = TagTokens::parse_end(input)?; + let ty = syn::parse2(content)?; + Ok(Self { tokens, _ty: ty }) } } @@ -359,7 +346,7 @@ const COLLISION_MSG: &str = "Using the `with props` syntax in combination with n impl Parse for Props { fn parse(input: ParseStream) -> syn::Result { - let mut props = Props { + let mut props = Self { node_ref: None, key: None, prop_type: PropType::None, @@ -382,7 +369,7 @@ impl Parse for Props { continue; } - if (HtmlProp::peek(input.cursor())).is_none() { + if input.is_empty() { break; } diff --git a/yew-macro/src/html_tree/html_list.rs b/yew-macro/src/html_tree/html_list.rs index 38bcd6b53a3..1d92314f6a6 100644 --- a/yew-macro/src/html_tree/html_list.rs +++ b/yew-macro/src/html_tree/html_list.rs @@ -1,16 +1,16 @@ -use super::{html_dashed_name::HtmlDashedName, HtmlChildrenTree, HtmlPropSuffix}; +use super::{html_dashed_name::HtmlDashedName, HtmlChildrenTree, TagTokens}; use crate::{props::HtmlProp, Peek, PeekValue}; use boolinator::Boolinator; use quote::{quote, quote_spanned, ToTokens}; use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; -use syn::{Expr, Token}; +use syn::Expr; pub struct HtmlList { open: HtmlListOpen, children: HtmlChildrenTree, - close: HtmlListClose, + _close: HtmlListClose, } impl PeekValue<()> for HtmlList { @@ -26,7 +26,7 @@ impl Parse for HtmlList { if HtmlListClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( - close, + close.to_spanned(), "this closing fragment has no corresponding opening fragment", )), Err(err) => Err(err), @@ -39,7 +39,7 @@ impl Parse for HtmlList { children.parse_child(input)?; if input.is_empty() { return Err(syn::Error::new_spanned( - open, + open.to_spanned(), "this opening fragment has no corresponding closing fragment", )); } @@ -47,21 +47,17 @@ impl Parse for HtmlList { let close = input.parse::()?; - Ok(HtmlList { + Ok(Self { open, children, - close, + _close: close, }) } } impl ToTokens for HtmlList { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let Self { - open, - children, - close, - } = &self; + let Self { open, children, .. } = &self; let key = if let Some(key) = &open.props.key { quote_spanned! {key.span()=> Some(::std::convert::Into::<::yew::virtual_dom::Key>::into(#key))} @@ -69,8 +65,7 @@ impl ToTokens for HtmlList { quote! {None} }; - let open_close_tokens = quote! {#open#close}; - tokens.extend(quote_spanned! {open_close_tokens.span()=> + tokens.extend(quote_spanned! {children.span()=> ::yew::virtual_dom::VNode::VList( ::yew::virtual_dom::VList::new_with_children(#children, #key) ) @@ -79,9 +74,13 @@ impl ToTokens for HtmlList { } struct HtmlListOpen { - lt: Token![<], + tokens: TagTokens, props: HtmlListProps, - gt: Token![>], +} +impl HtmlListOpen { + fn to_spanned(&self) -> impl ToTokens { + self.tokens.to_spanned() + } } impl PeekValue<()> for HtmlListOpen { @@ -101,17 +100,9 @@ impl PeekValue<()> for HtmlListOpen { impl Parse for HtmlListOpen { fn parse(input: ParseStream) -> syn::Result { - let lt = input.parse()?; - let HtmlPropSuffix { stream, gt, .. } = input.parse()?; - let props = syn::parse2(stream)?; - Ok(Self { lt, props, gt }) - } -} - -impl ToTokens for HtmlListOpen { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let HtmlListOpen { lt, gt, .. } = self; - tokens.extend(quote! {#lt#gt}); + let (tokens, content) = TagTokens::parse_start(input)?; + let props = syn::parse2(content)?; + Ok(Self { tokens, props }) } } @@ -144,12 +135,12 @@ impl Parse for HtmlListProps { } } -struct HtmlListClose { - lt: Token![<], - div: Token![/], - gt: Token![>], +struct HtmlListClose(TagTokens); +impl HtmlListClose { + fn to_spanned(&self) -> impl ToTokens { + self.0.to_spanned() + } } - impl PeekValue<()> for HtmlListClose { fn peek(cursor: Cursor) -> Option<()> { let (punct, cursor) = cursor.punct()?; @@ -161,20 +152,16 @@ impl PeekValue<()> for HtmlListClose { (punct.as_char() == '>').as_option() } } - impl Parse for HtmlListClose { fn parse(input: ParseStream) -> syn::Result { - Ok(HtmlListClose { - lt: input.parse()?, - div: input.parse()?, - gt: input.parse()?, - }) - } -} - -impl ToTokens for HtmlListClose { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let HtmlListClose { lt, div, gt } = self; - tokens.extend(quote! {#lt#div#gt}); + let (tokens, content) = TagTokens::parse_end(input)?; + if !content.is_empty() { + Err(syn::Error::new_spanned( + content, + "unexpected content in list close", + )) + } else { + Ok(Self(tokens)) + } } } diff --git a/yew-macro/src/html_tree/html_prop.rs b/yew-macro/src/html_tree/html_prop.rs deleted file mode 100644 index 02ed4ffba99..00000000000 --- a/yew-macro/src/html_tree/html_prop.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::html_tree::HtmlDashedName; -use crate::{Peek, PeekValue}; -use proc_macro2::{TokenStream, TokenTree}; -use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream}; -use syn::{Expr, Token}; - -pub struct HtmlProp { - pub label: HtmlDashedName, - pub question_mark: Option, - pub value: Expr, -} -impl HtmlProp { - /// Checks if the prop uses the optional attribute syntax. - /// If it does, an error is returned. - pub fn ensure_not_optional(&self) -> syn::Result<()> { - if self.question_mark.is_some() { - let msg = format!( - "the `{}` attribute does not support being used as an optional attribute", - self.label - ); - Err(syn::Error::new_spanned(&self.label, msg)) - } else { - Ok(()) - } - } -} - -impl PeekValue<()> for HtmlProp { - fn peek(cursor: Cursor) -> Option<()> { - HtmlDashedName::peek(cursor).map(|_| ()) - } -} - -impl Parse for HtmlProp { - fn parse(input: ParseStream) -> syn::Result { - let label = input.parse::()?; - let question_mark = input.parse::().ok(); - let equals = input - .parse::() - .map_err(|_| syn::Error::new_spanned(&label, "this prop doesn't have a value"))?; - if input.is_empty() { - return Err(syn::Error::new_spanned( - equals, - "expected an expression following this equals sign", - )); - } - let value = input.parse::()?; - // backwards compat - let _ = input.parse::(); - Ok(Self { - label, - question_mark, - value, - }) - } -} - -pub struct HtmlPropSuffix { - pub stream: TokenStream, - pub div: Option, - pub gt: Token![>], -} - -impl Parse for HtmlPropSuffix { - fn parse(input: ParseStream) -> syn::Result { - let mut trees: Vec = vec![]; - let mut div: Option = None; - let mut angle_count = 1; - let gt: Option]>; - - loop { - let next = input.parse()?; - if let TokenTree::Punct(punct) = &next { - match punct.as_char() { - '>' => { - angle_count -= 1; - if angle_count == 0 { - gt = Some(syn::token::Gt { - spans: [punct.span()], - }); - break; - } - } - '<' => angle_count += 1, - '/' => { - if angle_count == 1 && input.peek(Token![>]) { - div = Some(syn::token::Div { - spans: [punct.span()], - }); - gt = Some(input.parse()?); - break; - } - } - _ => {} - }; - } - trees.push(next); - } - - let gt: Token![>] = gt.ok_or_else(|| input.error("missing tag close"))?; - let stream: TokenStream = trees.into_iter().collect(); - - Ok(HtmlPropSuffix { stream, div, gt }) - } -} diff --git a/yew-macro/src/html_tree/html_tag/mod.rs b/yew-macro/src/html_tree/html_tag/mod.rs index 157f164ab37..388be15c2dd 100644 --- a/yew-macro/src/html_tree/html_tag/mod.rs +++ b/yew-macro/src/html_tree/html_tag/mod.rs @@ -1,13 +1,13 @@ -use super::{HtmlChildrenTree, HtmlDashedName, HtmlPropSuffix}; +use super::{HtmlChildrenTree, HtmlDashedName, TagTokens}; use crate::props::HtmlProp; use crate::stringify::Stringify; use crate::{non_capitalized_ascii, stringify, Peek, PeekValue}; use boolinator::Boolinator; use proc_macro2::{Delimiter, Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; -use syn::buffer::Cursor; use syn::parse::{Parse, ParseStream}; use syn::spanned::Spanned; +use syn::{buffer::Cursor, parse::Parser}; use syn::{Block, Ident, Token}; mod tag_attributes; @@ -32,7 +32,7 @@ impl Parse for HtmlTag { if HtmlTagClose::peek(input.cursor()).is_some() { return match input.parse::() { Ok(close) => Err(syn::Error::new_spanned( - close, + close.to_spanned(), "this closing tag has no corresponding opening tag", )), Err(err) => Err(err), @@ -41,7 +41,7 @@ impl Parse for HtmlTag { let open = input.parse::()?; // Return early if it's a self-closing tag - if open.div.is_some() { + if open.is_self_closing() { return Ok(HtmlTag { tag_name: open.tag_name, attributes: open.attributes, @@ -57,7 +57,7 @@ impl Parse for HtmlTag { match name.to_ascii_lowercase_string().as_str() { "area" | "base" | "br" | "col" | "embed" | "hr" | "img" | "input" | "link" | "meta" | "param" | "source" | "track" | "wbr" => { - return Err(syn::Error::new_spanned(&open, format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", name))); + return Err(syn::Error::new_spanned(open.to_spanned(), format!("the tag `<{}>` is a void element and cannot have children (hint: rewrite this as `<{0}/>`)", name))); } _ => {} } @@ -68,7 +68,7 @@ impl Parse for HtmlTag { loop { if input.is_empty() { return Err(syn::Error::new_spanned( - open, + open.to_spanned(), "this opening tag has no corresponding closing tag", )); } @@ -485,11 +485,18 @@ impl ToTokens for TagName { } struct HtmlTagOpen { - lt: Token![<], + tokens: TagTokens, tag_name: TagName, attributes: TagAttributes, - div: Option, - gt: Token![>], +} +impl HtmlTagOpen { + fn is_self_closing(&self) -> bool { + self.tokens.div.is_some() + } + + fn to_spanned(&self) -> impl ToTokens { + self.tokens.to_spanned() + } } impl PeekValue for HtmlTagOpen { @@ -515,60 +522,61 @@ impl PeekValue for HtmlTagOpen { impl Parse for HtmlTagOpen { fn parse(input: ParseStream) -> syn::Result { - let lt = input.parse::()?; - let tag_name = input.parse::()?; - let HtmlPropSuffix { stream, div, gt } = input.parse()?; - let mut attributes: TagAttributes = syn::parse2(stream)?; - - match &tag_name { - TagName::Lit(name) => { - // Don't treat value as special for non input / textarea fields - // For dynamic tags this is done at runtime! - match name.to_ascii_lowercase_string().as_str() { - "input" | "textarea" => {} - _ => { - if let Some(attr) = attributes.value.take() { - attributes.attributes.push(HtmlProp { - label: HtmlDashedName::new(Ident::new("value", Span::call_site())), - question_mark: attr.question_mark, - value: attr.value, - }); + let (tokens, content) = TagTokens::parse_start(input)?; + + let content_parser = |input: ParseStream| -> syn::Result { + let tag_name = input.parse::()?; + let mut attributes = input.parse::()?; + + match &tag_name { + TagName::Lit(name) => { + // Don't treat value as special for non input / textarea fields + // For dynamic tags this is done at runtime! + match name.to_ascii_lowercase_string().as_str() { + "input" | "textarea" => {} + _ => { + if let Some(attr) = attributes.value.take() { + attributes.attributes.push(HtmlProp { + label: HtmlDashedName::new(Ident::new( + "value", + Span::call_site(), + )), + question_mark: attr.question_mark, + value: attr.value, + }); + } } } } - } - TagName::Expr(name) => { - if name.expr.is_none() { - return Err(syn::Error::new_spanned( - tag_name, - "this dynamic tag is missing an expression block defining its value", - )); + TagName::Expr(name) => { + if name.expr.is_none() { + return Err(syn::Error::new_spanned( + tag_name, + "this dynamic tag is missing an expression block defining its value", + )); + } } } - } - Ok(HtmlTagOpen { - lt, - tag_name, - attributes, - div, - gt, - }) - } -} + Ok(Self { + tokens, + tag_name, + attributes, + }) + }; -impl ToTokens for HtmlTagOpen { - fn to_tokens(&self, tokens: &mut TokenStream) { - let HtmlTagOpen { lt, gt, .. } = self; - tokens.extend(quote! {#lt#gt}); + content_parser.parse2(content) } } struct HtmlTagClose { - lt: Token![<], - div: Option, - tag_name: TagName, - gt: Token![>], + tokens: TagTokens, + _tag_name: TagName, +} +impl HtmlTagClose { + fn to_spanned(&self) -> impl ToTokens { + self.tokens.to_spanned() + } } impl PeekValue for HtmlTagClose { @@ -593,10 +601,8 @@ impl PeekValue for HtmlTagClose { impl Parse for HtmlTagClose { fn parse(input: ParseStream) -> syn::Result { - let lt = input.parse()?; - let div = input.parse()?; - let tag_name = input.parse()?; - let gt = input.parse()?; + let (tokens, content) = TagTokens::parse_end(input)?; + let tag_name = syn::parse2(content)?; if let TagName::Expr(name) = &tag_name { if let Some(expr) = &name.expr { @@ -607,23 +613,9 @@ impl Parse for HtmlTagClose { } } - Ok(HtmlTagClose { - lt, - div, - tag_name, - gt, + Ok(Self { + tokens, + _tag_name: tag_name, }) } } - -impl ToTokens for HtmlTagClose { - fn to_tokens(&self, tokens: &mut TokenStream) { - let HtmlTagClose { - lt, - div, - tag_name, - gt, - } = self; - tokens.extend(quote! {#lt#div#tag_name#gt}); - } -} diff --git a/yew-macro/src/html_tree/html_tag/tag_attributes.rs b/yew-macro/src/html_tree/html_tag/tag_attributes.rs index d1f030b2f47..e97163fe395 100644 --- a/yew-macro/src/html_tree/html_tag/tag_attributes.rs +++ b/yew-macro/src/html_tree/html_tag/tag_attributes.rs @@ -298,7 +298,7 @@ impl Parse for TagAttributes { let (label_a, label_b) = (pair[0].label.to_lit_str(), pair[1].label.to_lit_str()); if label_a == label_b { return Err(syn::Error::new_spanned( - label_b, + &label_b, format!( "the attribute `{}` can only be specified once", label_b.value() diff --git a/yew-macro/src/html_tree/mod.rs b/yew-macro/src/html_tree/mod.rs index a014ce5eab7..f6c24c141d4 100644 --- a/yew-macro/src/html_tree/mod.rs +++ b/yew-macro/src/html_tree/mod.rs @@ -1,27 +1,27 @@ +use crate::PeekValue; +use proc_macro2::{Ident, Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::buffer::Cursor; +use syn::parse::{Parse, ParseStream, Result}; +use syn::spanned::Spanned; + mod html_block; mod html_component; mod html_dashed_name; mod html_iterable; mod html_list; mod html_node; -mod html_prop; mod html_tag; +mod tag; +use tag::TagTokens; -use crate::PeekValue; use html_block::HtmlBlock; use html_component::HtmlComponent; pub use html_dashed_name::HtmlDashedName; use html_iterable::HtmlIterable; use html_list::HtmlList; use html_node::HtmlNode; -use html_prop::HtmlProp; -use html_prop::HtmlPropSuffix; use html_tag::HtmlTag; -use proc_macro2::{Ident, Span, TokenStream}; -use quote::{quote, quote_spanned, ToTokens}; -use syn::buffer::Cursor; -use syn::parse::{Parse, ParseStream, Result}; -use syn::spanned::Spanned; pub enum HtmlType { Block, diff --git a/yew-macro/src/html_tree/tag.rs b/yew-macro/src/html_tree/tag.rs new file mode 100644 index 00000000000..f98d4ca3f5f --- /dev/null +++ b/yew-macro/src/html_tree/tag.rs @@ -0,0 +1,89 @@ +use proc_macro2::{TokenStream, TokenTree}; +use quote::{quote, ToTokens}; +use syn::parse::ParseStream; +use syn::Token; + +pub struct TagTokens { + pub lt: Token![<], + pub div: Option, + pub gt: Token![>], +} +impl TagTokens { + /// Parse a start tag + pub fn parse_start(input: ParseStream) -> syn::Result<(Self, TokenStream)> { + let lt = input.parse()?; + let (content, div, gt) = Self::parse_until_end(input)?; + + Ok((Self { lt, div, gt }, content)) + } + + /// Parse an end tag. + /// `div` will always be `Some` for end tags. + pub fn parse_end(input: ParseStream) -> syn::Result<(Self, TokenStream)> { + let lt = input.parse()?; + let div = Some(input.parse()?); + + let (content, end_div, gt) = Self::parse_until_end(input)?; + if end_div.is_some() { + return Err(syn::Error::new_spanned( + end_div, + "unexpected `/` in this end tag", + )); + } + + Ok((Self { lt, div, gt }, content)) + } + + fn parse_until_end( + input: ParseStream, + ) -> syn::Result<(TokenStream, Option, Token![>])> { + let mut trees = Vec::new(); + let mut angle_count: usize = 1; + let mut div: Option = None; + let gt: Token![>]; + + loop { + let next = input.parse()?; + if let TokenTree::Punct(punct) = &next { + match punct.as_char() { + '/' => { + if angle_count == 1 && input.peek(Token![>]) { + div = Some(syn::token::Div { + spans: [punct.span()], + }); + gt = input.parse()?; + break; + } + } + '>' => { + angle_count = angle_count.checked_sub(1).ok_or_else(|| { + syn::Error::new_spanned( + punct, + "this tag close has no corresponding tag open", + ) + })?; + if angle_count == 0 { + gt = syn::token::Gt { + spans: [punct.span()], + }; + break; + } + } + '<' => angle_count += 1, + _ => {} + }; + } + + trees.push(next); + } + + Ok((trees.into_iter().collect(), div, gt)) + } + + /// Generate tokens which can be used in `syn::Error::new_spanned` to span the entire tag. + /// This is to work around limitation of being unable to manually join spans which exists in stable Rust. + pub fn to_spanned(&self) -> impl ToTokens { + let Self { lt, gt, .. } = self; + quote! {#lt#gt} + } +} diff --git a/yew-macro/tests/props_macro_test.rs b/yew-macro/tests/props_macro_test.rs index a5c7ee2bb03..8f9b893d063 100644 --- a/yew-macro/tests/props_macro_test.rs +++ b/yew-macro/tests/props_macro_test.rs @@ -1,3 +1,4 @@ +#[allow(dead_code)] #[rustversion::attr(stable(1.45), test)] fn tests() { let t = trybuild::TestCases::new(); diff --git a/yew-router/src/components/router_button.rs b/yew-router/src/components/router_button.rs index 9bfc71836c0..14e5f2e9345 100644 --- a/yew-router/src/components/router_button.rs +++ b/yew-router/src/components/router_button.rs @@ -59,9 +59,9 @@ impl Component for RouterButto }); html! {