Skip to content

Commit

Permalink
fix(typescript): parse 'aria-label' as single token after generic com…
Browse files Browse the repository at this point in the history
…ponent

If a generic JSX component tag name, such as 'Foo<Bar>' in
'<Foo<Bar> />', is followed by a hyphenated attribute name, the hyphen
is not included in the token. In other words, tokenization is broken:

    <Foo<Bar> 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<Bar>'.
  • Loading branch information
strager committed Jan 1, 2024
1 parent 9f317a9 commit f3e639a
Show file tree
Hide file tree
Showing 6 changed files with 35 additions and 14 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<MyComponent<T> aria-label="..." />`, now parse correctly and no
longer report [E0054][] ("unexpected token").

## 2.19.0 (2023-12-30)

Expand Down
2 changes: 1 addition & 1 deletion src/quick-lint-js/fe/parse-class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/quick-lint-js/fe/parse-expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -3810,7 +3810,7 @@ Expression* Parser::parse_jsx_element_or_fragment(Parse_Visitor_Base& v,
// <Component<T> /> // 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 =
Expand Down
23 changes: 15 additions & 8 deletions src/quick-lint-js/fe/parse-type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions src/quick-lint-js/fe/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions test/test-parse-typescript-generic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"<MyComponent<T> 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,
Expand Down

0 comments on commit f3e639a

Please sign in to comment.