Skip to content

Commit f3e639a

Browse files
committed
fix(typescript): parse 'aria-label' as single token after generic component
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>'.
1 parent 9f317a9 commit f3e639a

File tree

6 files changed

+35
-14
lines changed

6 files changed

+35
-14
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Semantic Versioning.
1717
such as `type`.
1818
* Writing `++x` inside `? :` no longer falsely reports [E0254][] ("unexpected
1919
':' in expression; did you mean 'as'?").
20+
* Hypthenated JSX attribute names following generic JSX component names, such
21+
as in `<MyComponent<T> aria-label="..." />`, now parse correctly and no
22+
longer report [E0054][] ("unexpected token").
2023

2124
## 2.19.0 (2023-12-30)
2225

src/quick-lint-js/fe/parse-class.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2238,7 +2238,7 @@ void Parser::parse_and_visit_typescript_interface_reference(
22382238
.context = context,
22392239
});
22402240
}
2241-
this->parse_and_visit_typescript_generic_arguments(v);
2241+
this->parse_and_visit_typescript_generic_arguments(v, /*in_jsx=*/false);
22422242
}
22432243
}
22442244

src/quick-lint-js/fe/parse-expression.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,7 +1537,7 @@ Expression* Parser::parse_expression_remainder(Parse_Visitor_Base& v,
15371537
bool parsed_without_fatal_error = this->catch_fatal_parse_errors(
15381538
[this, &generic_arguments_visits] {
15391539
this->parse_and_visit_typescript_generic_arguments(
1540-
generic_arguments_visits.visitor());
1540+
generic_arguments_visits.visitor(), /*in_jsx=*/false);
15411541
});
15421542
if (!parsed_without_fatal_error) {
15431543
return false;
@@ -1850,7 +1850,7 @@ Expression* Parser::parse_expression_remainder(Parse_Visitor_Base& v,
18501850
.opening_less = Source_Code_Span(less_begin, less_begin + 1),
18511851
});
18521852
}
1853-
this->parse_and_visit_typescript_generic_arguments(v);
1853+
this->parse_and_visit_typescript_generic_arguments(v, /*in_jsx=*/false);
18541854
binary_builder.replace_last(this->parse_call_expression_remainder(
18551855
v, binary_builder.last_expression()));
18561856
goto next;
@@ -3810,7 +3810,7 @@ Expression* Parser::parse_jsx_element_or_fragment(Parse_Visitor_Base& v,
38103810
// <Component<T> /> // TypeScript only.
38113811
if (this->peek().type == Token_Type::less ||
38123812
this->peek().type == Token_Type::less_less) {
3813-
this->parse_and_visit_typescript_generic_arguments(v);
3813+
this->parse_and_visit_typescript_generic_arguments(v, /*in_jsx=*/true);
38143814
}
38153815

38163816
bool is_intrinsic =

src/quick-lint-js/fe/parse-type.cpp

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,8 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope(
313313
this->peek().begin + 1),
314314
});
315315
}
316-
this->parse_and_visit_typescript_generic_arguments_no_scope(v);
316+
this->parse_and_visit_typescript_generic_arguments_no_scope(
317+
v, /*in_jsx=*/false);
317318
}
318319
}
319320

@@ -741,7 +742,8 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope(
741742
.context = Statement_Kind::typeof_type,
742743
});
743744
}
744-
this->parse_and_visit_typescript_generic_arguments_no_scope(v);
745+
this->parse_and_visit_typescript_generic_arguments_no_scope(
746+
v, /*in_jsx=*/false);
745747
}
746748
maybe_parse_dots_after_generic_arguments();
747749
break;
@@ -781,7 +783,8 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope(
781783
if (!this->peek().has_leading_newline &&
782784
(this->peek().type == Token_Type::less ||
783785
this->peek().type == Token_Type::less_less)) {
784-
this->parse_and_visit_typescript_generic_arguments_no_scope(v);
786+
this->parse_and_visit_typescript_generic_arguments_no_scope(
787+
v, /*in_jsx=*/false);
785788
}
786789
maybe_parse_dots_after_generic_arguments();
787790
break;
@@ -1567,15 +1570,15 @@ void Parser::parse_and_visit_typescript_tuple_type_expression(
15671570
}
15681571
}
15691572

1570-
void Parser::parse_and_visit_typescript_generic_arguments(
1571-
Parse_Visitor_Base &v) {
1573+
void Parser::parse_and_visit_typescript_generic_arguments(Parse_Visitor_Base &v,
1574+
bool in_jsx) {
15721575
v.visit_enter_type_scope();
1573-
this->parse_and_visit_typescript_generic_arguments_no_scope(v);
1576+
this->parse_and_visit_typescript_generic_arguments_no_scope(v, in_jsx);
15741577
v.visit_exit_type_scope();
15751578
}
15761579

15771580
void Parser::parse_and_visit_typescript_generic_arguments_no_scope(
1578-
Parse_Visitor_Base &v) {
1581+
Parse_Visitor_Base &v, bool in_jsx) {
15791582
QLJS_ASSERT(this->peek().type == Token_Type::less ||
15801583
this->peek().type == Token_Type::less_less);
15811584
if (this->peek().type == Token_Type::less_less) {
@@ -1595,7 +1598,11 @@ void Parser::parse_and_visit_typescript_generic_arguments_no_scope(
15951598

15961599
switch (this->peek().type) {
15971600
case Token_Type::greater:
1598-
this->skip();
1601+
if (in_jsx) {
1602+
this->lexer_.skip_in_jsx();
1603+
} else {
1604+
this->lexer_.skip();
1605+
}
15991606
break;
16001607
case Token_Type::greater_equal:
16011608
case Token_Type::greater_greater:

src/quick-lint-js/fe/parse.h

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,9 +295,13 @@ class Parser {
295295
void maybe_visit_assignment(Expression *ast, Parse_Visitor_Base &v,
296296
Variable_Assignment_Flags flags);
297297

298-
void parse_and_visit_typescript_generic_arguments(Parse_Visitor_Base &v);
298+
// If in_jsx is true, then the token after '>' is parsed as a JSX token.
299+
// otherwise, the token after '>' is parsed as a normal JavaScript/TypeScript
300+
// token.
301+
void parse_and_visit_typescript_generic_arguments(Parse_Visitor_Base &v,
302+
bool in_jsx);
299303
void parse_and_visit_typescript_generic_arguments_no_scope(
300-
Parse_Visitor_Base &v);
304+
Parse_Visitor_Base &v, bool in_jsx);
301305

302306
public: // For testing only.
303307
void parse_and_visit_typescript_generic_parameters(Parse_Visitor_Base &v);

test/test-parse-typescript-generic.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,13 @@ TEST_F(Test_Parse_TypeScript_Generic, jsx_element) {
915915
Expression* ast = p.parse_expression();
916916
EXPECT_EQ(summarize(ast), "jsxelement(C, var value)");
917917
}
918+
919+
{
920+
Test_Parser p(u8"<MyComponent<T> aria-label={label} />"_sv,
921+
typescript_jsx_options);
922+
Expression* ast = p.parse_expression();
923+
EXPECT_EQ(summarize(ast), "jsxelement(MyComponent, var label)");
924+
}
918925
}
919926

920927
TEST_F(Test_Parse_TypeScript_Generic,

0 commit comments

Comments
 (0)