diff --git a/packages/yew-macro/src/html_tree/html_component.rs b/packages/yew-macro/src/html_tree/html_component.rs index 1a13b16351d..4f557134c36 100644 --- a/packages/yew-macro/src/html_tree/html_component.rs +++ b/packages/yew-macro/src/html_tree/html_component.rs @@ -1,18 +1,12 @@ -use boolinator::Boolinator; use proc_macro2::Span; use quote::{quote, quote_spanned, ToTokens}; -use syn::buffer::Cursor; +use syn::parse::discouraged::Speculative; use syn::parse::{Parse, ParseStream}; -use syn::punctuated::Punctuated; use syn::spanned::Spanned; -use syn::{ - AngleBracketedGenericArguments, GenericArgument, Path, PathArguments, PathSegment, Token, Type, - TypePath, -}; +use syn::{Token, Type}; use super::{HtmlChildrenTree, TagTokens}; use crate::props::ComponentProps; -use crate::PeekValue; pub struct HtmlComponent { ty: Type, @@ -21,17 +15,16 @@ pub struct HtmlComponent { close: Option, } -impl PeekValue<()> for HtmlComponent { - fn peek(cursor: Cursor) -> Option<()> { - HtmlComponentOpen::peek(cursor) - .or_else(|| HtmlComponentClose::peek(cursor)) - .map(|_| ()) - } -} - impl Parse for HtmlComponent { fn parse(input: ParseStream) -> syn::Result { - if HtmlComponentClose::peek(input.cursor()).is_some() { + // check if the next tokens are () { Ok(close) => Err(syn::Error::new_spanned( close.to_spanned(), @@ -53,23 +46,55 @@ impl Parse for HtmlComponent { } let mut children = HtmlChildrenTree::new(); - loop { + let close = loop { if input.is_empty() { return Err(syn::Error::new_spanned( open.to_spanned(), "this opening tag has no corresponding closing tag", )); } - if let Some(ty) = HtmlComponentClose::peek(input.cursor()) { - if open.ty == ty { - break; + + if trying_to_close() { + fn format_token_stream(ts: impl ToTokens) -> String { + let string = ts.to_token_stream().to_string(); + // remove unnecessary spaces + string.replace(' ', "") } - } + let fork = input.fork(); + break TagTokens::parse_end_content(&fork, |i_fork, tag| { + let ty = i_fork.parse().map_err(|e| { + syn::Error::new( + e.span(), + format!( + "expected a valid closing tag for component\nnote: found opening \ + tag `{lt}{0}{gt}`\nhelp: try `{lt}/{0}{gt}`", + format_token_stream(&open.ty), + lt = open.tag.lt.to_token_stream(), + gt = open.tag.gt.to_token_stream(), + ), + ) + })?; + + if ty != open.ty { + let open_ty = &open.ty; + Err(syn::Error::new_spanned( + quote!(#open_ty #ty), + format!( + "mismatched closing tags: expected `{}`, found `{}`", + format_token_stream(open_ty), + format_token_stream(ty) + ), + )) + } else { + let close = HtmlComponentClose { tag, ty }; + input.advance_to(&fork); + Ok(close) + } + })?; + } children.parse_child(input)?; - } - - let close = input.parse::()?; + }; if !children.is_empty() { if let Some(children_prop) = open.props.children() { @@ -127,108 +152,6 @@ impl ToTokens for HtmlComponent { } } -impl HtmlComponent { - fn double_colon(mut cursor: Cursor) -> Option { - for _ in 0..2 { - let (punct, c) = cursor.punct()?; - (punct.as_char() == ':').as_option()?; - cursor = c; - } - - Some(cursor) - } - - /// Refer to the [`syn::parse::Parse`] implementation for [`AngleBracketedGenericArguments`]. - fn path_arguments(mut cursor: Cursor) -> Option<(PathArguments, Cursor)> { - let (punct, c) = cursor.punct()?; - cursor = c; - (punct.as_char() == '<').as_option()?; - - let mut args = Punctuated::new(); - - loop { - let punct = cursor.punct(); - if let Some((punct, c)) = punct { - if punct.as_char() == '>' { - cursor = c; - break; - } - } - - let (ty, c) = Self::peek_type(cursor); - cursor = c; - - args.push_value(GenericArgument::Type(ty)); - - let punct = cursor.punct(); - if let Some((punct, c)) = punct { - cursor = c; - if punct.as_char() == '>' { - break; - } else if punct.as_char() == ',' { - args.push_punct(Token![,](Span::mixed_site())) - } - } - } - - Some(( - PathArguments::AngleBracketed(AngleBracketedGenericArguments { - colon2_token: None, - lt_token: Token![<](Span::mixed_site()), - args, - gt_token: Token![>](Span::mixed_site()), - }), - cursor, - )) - } - - fn peek_type(mut cursor: Cursor) -> (Type, Cursor) { - let mut colons_optional = true; - let mut leading_colon = None; - let mut segments = Punctuated::new(); - - loop { - let mut post_colons_cursor = cursor; - if let Some(c) = Self::double_colon(post_colons_cursor) { - if colons_optional { - leading_colon = Some(Token![::](Span::mixed_site())); - } - post_colons_cursor = c; - } else if !colons_optional { - break; - } - - if let Some((ident, c)) = post_colons_cursor.ident() { - cursor = c; - let arguments = if let Some((args, c)) = Self::path_arguments(cursor) { - cursor = c; - args - } else { - PathArguments::None - }; - - segments.push(PathSegment { ident, arguments }); - } else { - break; - } - - // only first `::` is optional - colons_optional = false; - } - - ( - Type::Path(TypePath { - qself: None, - path: Path { - leading_colon, - segments, - }, - }), - cursor, - ) - } -} - struct HtmlComponentOpen { tag: TagTokens, ty: Type, @@ -244,15 +167,6 @@ impl HtmlComponentOpen { } } -impl PeekValue for HtmlComponentOpen { - fn peek(cursor: Cursor) -> Option { - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '<').as_option()?; - let (typ, _) = HtmlComponent::peek_type(cursor); - Some(typ) - } -} - impl Parse for HtmlComponentOpen { fn parse(input: ParseStream) -> syn::Result { TagTokens::parse_start_content(input, |input, tag| { @@ -282,22 +196,6 @@ impl HtmlComponentClose { } } -impl PeekValue for HtmlComponentClose { - fn peek(cursor: Cursor) -> Option { - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '<').as_option()?; - - let (punct, cursor) = cursor.punct()?; - (punct.as_char() == '/').as_option()?; - - let (typ, cursor) = HtmlComponent::peek_type(cursor); - - let (punct, _) = cursor.punct()?; - (punct.as_char() == '>').as_option()?; - - Some(typ) - } -} impl Parse for HtmlComponentClose { fn parse(input: ParseStream) -> syn::Result { TagTokens::parse_end_content(input, |input, tag| { diff --git a/packages/yew-macro/tests/html_macro/component-fail.rs b/packages/yew-macro/tests/html_macro/component-fail.rs index 5da187d2f5e..cdf9ccb34d2 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.rs +++ b/packages/yew-macro/tests/html_macro/component-fail.rs @@ -148,4 +148,34 @@ fn not_expressions() { html! { }; } +fn mismatch_closing_tags() { + pub struct A; + impl Component for A { + type Message = (); + type Properties = (); + + fn create(_ctx: &Context) -> Self { + unimplemented!() + } + fn view(&self, _ctx: &Context) -> Html { + unimplemented!() + } + } + + pub struct B; + impl Component for B { + type Message = (); + type Properties = (); + + fn create(_ctx: &Context) -> Self { + unimplemented!() + } + fn view(&self, _ctx: &Context) -> Html { + unimplemented!() + } + } + let _ = html! { }; + let _ = html! { }; +} + fn main() {} diff --git a/packages/yew-macro/tests/html_macro/component-fail.stderr b/packages/yew-macro/tests/html_macro/component-fail.stderr index 33f60e1a6a3..15229044d0e 100644 --- a/packages/yew-macro/tests/html_macro/component-fail.stderr +++ b/packages/yew-macro/tests/html_macro/component-fail.stderr @@ -386,6 +386,20 @@ error: only an expression may be assigned as a property. Consider removing this 148 | html! { }; | ^ +error: mismatched closing tags: expected `A`, found `B` + --> tests/html_macro/component-fail.rs:177:22 + | +177 | let _ = html! { }; + | ^^^^^ + +error: expected a valid closing tag for component + note: found opening tag `` + help: try `` + --> tests/html_macro/component-fail.rs:178:24 + | +178 | let _ = html! { }; + | ^^^ + error[E0425]: cannot find value `blah` in this scope --> tests/html_macro/component-fail.rs:82:22 | diff --git a/packages/yew-macro/tests/html_macro/generic-component-fail.rs b/packages/yew-macro/tests/html_macro/generic-component-fail.rs index ed2163bcfc5..7d06180caab 100644 --- a/packages/yew-macro/tests/html_macro/generic-component-fail.rs +++ b/packages/yew-macro/tests/html_macro/generic-component-fail.rs @@ -40,9 +40,15 @@ where }} fn compile_fail() { + #[allow(unused_imports)] + use std::path::Path; + html! { > }; html! { > }; html! { >>> }; + + html! { >> }; + html! { > }; } fn main() {} diff --git a/packages/yew-macro/tests/html_macro/generic-component-fail.stderr b/packages/yew-macro/tests/html_macro/generic-component-fail.stderr index 33312601115..a0071a87b44 100644 --- a/packages/yew-macro/tests/html_macro/generic-component-fail.stderr +++ b/packages/yew-macro/tests/html_macro/generic-component-fail.stderr @@ -1,17 +1,31 @@ error: this opening tag has no corresponding closing tag - --> $DIR/generic-component-fail.rs:43:13 + --> tests/html_macro/generic-component-fail.rs:46:13 | -43 | html! { > }; +46 | html! { > }; | ^^^^^^^^^^^^^^^^^ -error: this closing tag has no corresponding opening tag - --> $DIR/generic-component-fail.rs:44:30 +error: mismatched closing tags: expected `Generic`, found `Generic` + --> tests/html_macro/generic-component-fail.rs:47:14 | -44 | html! { > }; - | ^^^^^^^^^^ +47 | html! { > }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ -error: this closing tag has no corresponding opening tag - --> $DIR/generic-component-fail.rs:45:30 +error: mismatched closing tags: expected `Generic`, found `Generic>` + --> tests/html_macro/generic-component-fail.rs:48:14 | -45 | html! { >>> }; - | ^^^^^^^^^^^^^^^^^^^^^^^ +48 | html! { >>> }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: mismatched closing tags: expected `Generic`, found `Generic` + --> tests/html_macro/generic-component-fail.rs:50:14 + | +50 | html! { >> }; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected a valid closing tag for component + note: found opening tag `>` + help: try `>` + --> tests/html_macro/generic-component-fail.rs:51:30 + | +51 | html! { > }; + | ^^^ diff --git a/packages/yew-macro/tests/html_macro/generic-component-pass.rs b/packages/yew-macro/tests/html_macro/generic-component-pass.rs index 49d88cb56b8..4aca8a05457 100644 --- a/packages/yew-macro/tests/html_macro/generic-component-pass.rs +++ b/packages/yew-macro/tests/html_macro/generic-component-pass.rs @@ -77,6 +77,8 @@ where fn compile_pass() { ::yew::html! { /> }; + ::yew::html! { /> }; + ::yew::html! { >> }; ::yew::html! { >> }; ::yew::html! { > /> };