Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with tuples in closing tag #2886

Merged
merged 5 commits into from Oct 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
205 changes: 53 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,58 @@ 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() {
let cursor = input.cursor();
let _ = cursor
.punct()
.and_then(|(_, cursor)| cursor.punct())
.and_then(|(_, cursor)| cursor.ident())
.ok_or_else(|| {
syn::Error::new(
Span::call_site(),
"expected a valid closing tag (e.g.: </Component>)",
)
})?;

let fork = input.fork();
hamza1311 marked this conversation as resolved.
Show resolved Hide resolved
let lt = fork.parse::<Token![<]>()?;
let div = Some(fork.parse::<Token![/]>()?);
let ty = fork.parse::<Type>()?;
if ty != open.ty {
fn format_token_stream(ts: impl ToTokens) -> String {
let string = ts.to_token_stream().to_string();
// remove unnecessary spaces
string.replace(' ', "")
}
let open_ty = open.ty;
return 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 gt = fork.parse::<Token![>]>()?;
let close = HtmlComponentClose {
tag: TagTokens { lt, div, gt },
ty,
};
input.advance_to(&fork);
break 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 +155,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 +170,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 +199,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 (e.g.: </Component>)
--> tests/html_macro/component-fail.rs:178:13
|
178 | let _ = html! { <A></> };
| ^^^^^^^^^^^^^^^^
hamza1311 marked this conversation as resolved.
Show resolved Hide resolved
|
= note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info)

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 (e.g.: </Component>)
--> tests/html_macro/generic-component-fail.rs:51:5
|
51 | html! { <Generic<String>></> };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the macro `html` (in Nightly builds, run with -Z macro-backtrace for more info)
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