Skip to content

Commit

Permalink
Fix issues with tuples in closing tag (#2886)
Browse files Browse the repository at this point in the history
* Add test case for #2225

* Fix issues with tuples in closing tag

* Improve diagnostics

* fmt

* better diagnostics
  • Loading branch information
hamza1311 committed Oct 8, 2022
1 parent bb71b5d commit 7ada344
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 162 deletions.
202 changes: 50 additions & 152 deletions 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,
Expand All @@ -21,17 +15,16 @@ pub struct HtmlComponent {
close: Option<HtmlComponentClose>,
}

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<Self> {
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::<HtmlComponentClose>() {
Ok(close) => Err(syn::Error::new_spanned(
close.to_spanned(),
Expand All @@ -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::<HtmlComponentClose>()?;
};

if !children.is_empty() {
if let Some(children_prop) = open.props.children() {
Expand Down Expand Up @@ -127,108 +152,6 @@ impl ToTokens for HtmlComponent {
}
}

impl HtmlComponent {
fn double_colon(mut cursor: Cursor) -> Option<Cursor> {
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,
Expand All @@ -244,15 +167,6 @@ impl HtmlComponentOpen {
}
}

impl PeekValue<Type> for HtmlComponentOpen {
fn peek(cursor: Cursor) -> Option<Type> {
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<Self> {
TagTokens::parse_start_content(input, |input, tag| {
Expand Down Expand Up @@ -282,22 +196,6 @@ impl HtmlComponentClose {
}
}

impl PeekValue<Type> for HtmlComponentClose {
fn peek(cursor: Cursor) -> Option<Type> {
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<Self> {
TagTokens::parse_end_content(input, |input, tag| {
Expand Down
30 changes: 30 additions & 0 deletions packages/yew-macro/tests/html_macro/component-fail.rs
Expand Up @@ -148,4 +148,34 @@ fn not_expressions() {
html! { <HtmlInProps header={format!("ending with semi");} /> };
}

fn mismatch_closing_tags() {
pub struct A;
impl Component for A {
type Message = ();
type Properties = ();

fn create(_ctx: &Context<Self>) -> Self {
unimplemented!()
}
fn view(&self, _ctx: &Context<Self>) -> Html {
unimplemented!()
}
}

pub struct B;
impl Component for B {
type Message = ();
type Properties = ();

fn create(_ctx: &Context<Self>) -> Self {
unimplemented!()
}
fn view(&self, _ctx: &Context<Self>) -> Html {
unimplemented!()
}
}
let _ = html! { <A></B> };
let _ = html! { <A></> };
}

fn main() {}
14 changes: 14 additions & 0 deletions packages/yew-macro/tests/html_macro/component-fail.stderr
Expand Up @@ -386,6 +386,20 @@ error: only an expression may be assigned as a property. Consider removing this
148 | html! { <HtmlInProps header={format!("ending with semi");} /> };
| ^

error: mismatched closing tags: expected `A`, found `B`
--> tests/html_macro/component-fail.rs:177:22
|
177 | let _ = html! { <A></B> };
| ^^^^^

error: expected a valid closing tag for component
note: found opening tag `<A>`
help: try `</A>`
--> tests/html_macro/component-fail.rs:178:24
|
178 | let _ = html! { <A></> };
| ^^^

error[E0425]: cannot find value `blah` in this scope
--> tests/html_macro/component-fail.rs:82:22
|
Expand Down
6 changes: 6 additions & 0 deletions packages/yew-macro/tests/html_macro/generic-component-fail.rs
Expand Up @@ -40,9 +40,15 @@ where
}}

fn compile_fail() {
#[allow(unused_imports)]
use std::path::Path;

html! { <Generic<String>> };
html! { <Generic<String>></Generic> };
html! { <Generic<String>></Generic<Vec<String>>> };

html! { <Generic<String>></Generic<Path>> };
html! { <Generic<String>></> };
}

fn main() {}
34 changes: 24 additions & 10 deletions 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! { <Generic<String>> };
46 | html! { <Generic<String>> };
| ^^^^^^^^^^^^^^^^^

error: this closing tag has no corresponding opening tag
--> $DIR/generic-component-fail.rs:44:30
error: mismatched closing tags: expected `Generic<String>`, found `Generic`
--> tests/html_macro/generic-component-fail.rs:47:14
|
44 | html! { <Generic<String>></Generic> };
| ^^^^^^^^^^
47 | html! { <Generic<String>></Generic> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^

error: this closing tag has no corresponding opening tag
--> $DIR/generic-component-fail.rs:45:30
error: mismatched closing tags: expected `Generic<String>`, found `Generic<Vec<String>>`
--> tests/html_macro/generic-component-fail.rs:48:14
|
45 | html! { <Generic<String>></Generic<Vec<String>>> };
| ^^^^^^^^^^^^^^^^^^^^^^^
48 | html! { <Generic<String>></Generic<Vec<String>>> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: mismatched closing tags: expected `Generic<String>`, found `Generic<Path>`
--> tests/html_macro/generic-component-fail.rs:50:14
|
50 | html! { <Generic<String>></Generic<Path>> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: expected a valid closing tag for component
note: found opening tag `<Generic<String>>`
help: try `</Generic<String>>`
--> tests/html_macro/generic-component-fail.rs:51:30
|
51 | html! { <Generic<String>></> };
| ^^^
2 changes: 2 additions & 0 deletions packages/yew-macro/tests/html_macro/generic-component-pass.rs
Expand Up @@ -77,6 +77,8 @@ where

fn compile_pass() {
::yew::html! { <Generic<::std::string::String> /> };
::yew::html! { <Generic<(u8, bool)> /> };
::yew::html! { <Generic<(u8, bool)> ></Generic<(u8, bool)>> };
::yew::html! { <Generic<::std::string::String> ></Generic<::std::string::String>> };

::yew::html! { <Generic<::std::vec::Vec<::std::string::String>> /> };
Expand Down

0 comments on commit 7ada344

Please sign in to comment.