From f3e639a2f61b4a32cb1af9a80f499ab5d4109e1a Mon Sep 17 00:00:00 2001 From: "Matthew \"strager\" Glazar" Date: Sun, 31 Dec 2023 19:06:51 -0500 Subject: [PATCH] fix(typescript): parse 'aria-label' as single token after generic component If a generic JSX component tag name, such as 'Foo' in ' />', is followed by a hyphenated attribute name, the hyphen is not included in the token. In other words, tokenization is broken: aria-label="baz" /> ^ less ^^^ identifier ^ less ^^^ identifier ^ greater ^^^^ identifier ^ minus Fix this by calling skip_in_jsx() instead of skip() when skipping the '>' in 'Foo'. --- docs/CHANGELOG.md | 3 +++ src/quick-lint-js/fe/parse-class.cpp | 2 +- src/quick-lint-js/fe/parse-expression.cpp | 6 +++--- src/quick-lint-js/fe/parse-type.cpp | 23 +++++++++++++++-------- src/quick-lint-js/fe/parse.h | 8 ++++++-- test/test-parse-typescript-generic.cpp | 7 +++++++ 6 files changed, 35 insertions(+), 14 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 035e6cdee8..de8812cc4f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -17,6 +17,9 @@ Semantic Versioning. such as `type`. * Writing `++x` inside `? :` no longer falsely reports [E0254][] ("unexpected ':' in expression; did you mean 'as'?"). + * Hypthenated JSX attribute names following generic JSX component names, such + as in ` aria-label="..." />`, now parse correctly and no + longer report [E0054][] ("unexpected token"). ## 2.19.0 (2023-12-30) diff --git a/src/quick-lint-js/fe/parse-class.cpp b/src/quick-lint-js/fe/parse-class.cpp index b847d4e8d7..0265c55a9f 100644 --- a/src/quick-lint-js/fe/parse-class.cpp +++ b/src/quick-lint-js/fe/parse-class.cpp @@ -2238,7 +2238,7 @@ void Parser::parse_and_visit_typescript_interface_reference( .context = context, }); } - this->parse_and_visit_typescript_generic_arguments(v); + this->parse_and_visit_typescript_generic_arguments(v, /*in_jsx=*/false); } } diff --git a/src/quick-lint-js/fe/parse-expression.cpp b/src/quick-lint-js/fe/parse-expression.cpp index c6d54981d2..9d61c066f2 100644 --- a/src/quick-lint-js/fe/parse-expression.cpp +++ b/src/quick-lint-js/fe/parse-expression.cpp @@ -1537,7 +1537,7 @@ Expression* Parser::parse_expression_remainder(Parse_Visitor_Base& v, bool parsed_without_fatal_error = this->catch_fatal_parse_errors( [this, &generic_arguments_visits] { this->parse_and_visit_typescript_generic_arguments( - generic_arguments_visits.visitor()); + generic_arguments_visits.visitor(), /*in_jsx=*/false); }); if (!parsed_without_fatal_error) { return false; @@ -1850,7 +1850,7 @@ Expression* Parser::parse_expression_remainder(Parse_Visitor_Base& v, .opening_less = Source_Code_Span(less_begin, less_begin + 1), }); } - this->parse_and_visit_typescript_generic_arguments(v); + this->parse_and_visit_typescript_generic_arguments(v, /*in_jsx=*/false); binary_builder.replace_last(this->parse_call_expression_remainder( v, binary_builder.last_expression())); goto next; @@ -3810,7 +3810,7 @@ Expression* Parser::parse_jsx_element_or_fragment(Parse_Visitor_Base& v, // /> // TypeScript only. if (this->peek().type == Token_Type::less || this->peek().type == Token_Type::less_less) { - this->parse_and_visit_typescript_generic_arguments(v); + this->parse_and_visit_typescript_generic_arguments(v, /*in_jsx=*/true); } bool is_intrinsic = diff --git a/src/quick-lint-js/fe/parse-type.cpp b/src/quick-lint-js/fe/parse-type.cpp index c7a041fe66..cb82ff9e47 100644 --- a/src/quick-lint-js/fe/parse-type.cpp +++ b/src/quick-lint-js/fe/parse-type.cpp @@ -313,7 +313,8 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope( this->peek().begin + 1), }); } - this->parse_and_visit_typescript_generic_arguments_no_scope(v); + this->parse_and_visit_typescript_generic_arguments_no_scope( + v, /*in_jsx=*/false); } } @@ -741,7 +742,8 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope( .context = Statement_Kind::typeof_type, }); } - this->parse_and_visit_typescript_generic_arguments_no_scope(v); + this->parse_and_visit_typescript_generic_arguments_no_scope( + v, /*in_jsx=*/false); } maybe_parse_dots_after_generic_arguments(); break; @@ -781,7 +783,8 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope( if (!this->peek().has_leading_newline && (this->peek().type == Token_Type::less || this->peek().type == Token_Type::less_less)) { - this->parse_and_visit_typescript_generic_arguments_no_scope(v); + this->parse_and_visit_typescript_generic_arguments_no_scope( + v, /*in_jsx=*/false); } maybe_parse_dots_after_generic_arguments(); break; @@ -1567,15 +1570,15 @@ void Parser::parse_and_visit_typescript_tuple_type_expression( } } -void Parser::parse_and_visit_typescript_generic_arguments( - Parse_Visitor_Base &v) { +void Parser::parse_and_visit_typescript_generic_arguments(Parse_Visitor_Base &v, + bool in_jsx) { v.visit_enter_type_scope(); - this->parse_and_visit_typescript_generic_arguments_no_scope(v); + this->parse_and_visit_typescript_generic_arguments_no_scope(v, in_jsx); v.visit_exit_type_scope(); } void Parser::parse_and_visit_typescript_generic_arguments_no_scope( - Parse_Visitor_Base &v) { + Parse_Visitor_Base &v, bool in_jsx) { QLJS_ASSERT(this->peek().type == Token_Type::less || this->peek().type == Token_Type::less_less); if (this->peek().type == Token_Type::less_less) { @@ -1595,7 +1598,11 @@ void Parser::parse_and_visit_typescript_generic_arguments_no_scope( switch (this->peek().type) { case Token_Type::greater: - this->skip(); + if (in_jsx) { + this->lexer_.skip_in_jsx(); + } else { + this->lexer_.skip(); + } break; case Token_Type::greater_equal: case Token_Type::greater_greater: diff --git a/src/quick-lint-js/fe/parse.h b/src/quick-lint-js/fe/parse.h index baf75ab5ea..b9744d53b2 100644 --- a/src/quick-lint-js/fe/parse.h +++ b/src/quick-lint-js/fe/parse.h @@ -295,9 +295,13 @@ class Parser { void maybe_visit_assignment(Expression *ast, Parse_Visitor_Base &v, Variable_Assignment_Flags flags); - void parse_and_visit_typescript_generic_arguments(Parse_Visitor_Base &v); + // If in_jsx is true, then the token after '>' is parsed as a JSX token. + // otherwise, the token after '>' is parsed as a normal JavaScript/TypeScript + // token. + void parse_and_visit_typescript_generic_arguments(Parse_Visitor_Base &v, + bool in_jsx); void parse_and_visit_typescript_generic_arguments_no_scope( - Parse_Visitor_Base &v); + Parse_Visitor_Base &v, bool in_jsx); public: // For testing only. void parse_and_visit_typescript_generic_parameters(Parse_Visitor_Base &v); diff --git a/test/test-parse-typescript-generic.cpp b/test/test-parse-typescript-generic.cpp index 37dbc1e44f..c7f9e4af16 100644 --- a/test/test-parse-typescript-generic.cpp +++ b/test/test-parse-typescript-generic.cpp @@ -915,6 +915,13 @@ TEST_F(Test_Parse_TypeScript_Generic, jsx_element) { Expression* ast = p.parse_expression(); EXPECT_EQ(summarize(ast), "jsxelement(C, var value)"); } + + { + Test_Parser p(u8" aria-label={label} />"_sv, + typescript_jsx_options); + Expression* ast = p.parse_expression(); + EXPECT_EQ(summarize(ast), "jsxelement(MyComponent, var label)"); + } } TEST_F(Test_Parse_TypeScript_Generic,