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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 50 additions & 152 deletions packages/yew-macro/src/html_tree/html_component.rs
Original file line number Diff line number Diff line change
@@ -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();
ranile marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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