Skip to content

Commit

Permalink
fix(typescript): parse 'import type * from ...'
Browse files Browse the repository at this point in the history
  • Loading branch information
strager committed Dec 30, 2023
1 parent f0ef6e1 commit 82f50c7
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 3 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Semantic Versioning.
parsed correctly.
* `import("modulename")` in a type is now allowed and no longer falsely
reports a diagnostic.
* `import type * from 'othermodule';` no longer crashes quick-lint-js with an
assertion failure.
* Generic call signatures are now parsed correctly when using a semicolon-free
coding style.
* Interface index signatures and computed property names in interfaces are now
Expand Down
21 changes: 18 additions & 3 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1182,6 +1182,7 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,

// export * from "module";
// export * as name from "module";
export_star:
case Token_Type::star:
// Do not set is_current_typescript_namespace_non_empty_. See
// NOTE[ambiguous-ambient-statement-in-namespace].
Expand Down Expand Up @@ -1412,8 +1413,9 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
// Do not set is_current_typescript_namespace_non_empty_.
Source_Code_Span type_keyword = this->peek().span();
this->skip();
if (this->peek().type == Token_Type::left_curly) {
// export type {A, B as C};
switch (this->peek().type) {
// export type {A, B as C};
case Token_Type::left_curly:
// Do not set is_current_typescript_namespace_non_empty_. See
// NOTE[ambiguous-ambient-statement-in-namespace].
typescript_type_only_keyword = type_keyword;
Expand All @@ -1424,10 +1426,23 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
});
}
goto named_export_list;
} else {

// export type * from 'othermod';
case Token_Type::star:
typescript_type_only_keyword = type_keyword;
if (!this->options_.typescript) {
this->diag_reporter_->report(
Diag_TypeScript_Type_Export_Not_Allowed_In_JavaScript{
.type_keyword = type_keyword,
});
}
goto export_star;

default:
// export type A = B;
// Do not set is_current_typescript_namespace_non_empty_.
this->parse_and_visit_typescript_type_alias(v, type_keyword);
break;
}
break;
}
Expand Down
105 changes: 105 additions & 0 deletions test/test-parse-typescript-module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <vector>

using ::testing::ElementsAreArray;
using ::testing::IsEmpty;

namespace quick_lint_js {
namespace {
Expand Down Expand Up @@ -334,6 +335,22 @@ TEST_F(Test_Parse_TypeScript_Module,
"visit_end_of_module",
}));
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {T} from 'othermod';"_sv, //
u8" ^^^^ Diag_TypeScript_Type_Export_Not_Allowed_In_JavaScript"_diag, //
javascript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type * from 'othermod';"_sv, //
u8" ^^^^ Diag_TypeScript_Type_Export_Not_Allowed_In_JavaScript"_diag, //
javascript_options);
EXPECT_THAT(p.visits, IsEmpty());
}
}

TEST_F(Test_Parse_TypeScript_Module, inline_type_export) {
Expand Down Expand Up @@ -409,6 +426,94 @@ TEST_F(Test_Parse_TypeScript_Module, mixed_inline_type_and_type_only_export) {
}
}

TEST_F(Test_Parse_TypeScript_Module, export_type_from) {
{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type * from 'other';"_sv, no_diags, typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type * as mother from 'other';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type * as 'mother' from 'other';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {} from 'other';"_sv, no_diags, typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {util1, util2, util3} from 'other';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {readFileSync as readFile} from 'fs';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {promises as default} from 'fs';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

for (String8 keyword : keywords) {
Padded_String code(
concat(u8"export type {"_sv, keyword, u8"} from 'other';"_sv));
SCOPED_TRACE(code);
Spy_Visitor p = test_parse_and_visit_statement(code.string_view(), no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
// Keywords are legal, even if Unicode-escaped.
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {\\u{76}ar} from 'fs';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
// Keywords are legal, even if Unicode-escaped.
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {\\u{76}ar as \\u{69}f} from 'fs';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {'name'} from 'other';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export type {'name' as 'othername'} from 'other';"_sv, no_diags,
typescript_options);
EXPECT_THAT(p.visits, IsEmpty());
}
}

TEST_F(Test_Parse_TypeScript_Module, import_require) {
{
Spy_Visitor p = test_parse_and_visit_module(
Expand Down

0 comments on commit 82f50c7

Please sign in to comment.