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
+ let trying_to_close = || {
+ let lt = input.peek(Token![<]);
+ let div = input.peek2(Token![/]);
+ lt && div
+ };
+
+ if trying_to_close() {
return match input.parse::() {
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! { > /> };