Skip to content

Commit

Permalink
fix(typescript): merge 'export default interface' with 'export defaul…
Browse files Browse the repository at this point in the history
…t class'

TypeScript performs declaration merging on default exports. Have us do
the same to avoid falsely reporting duplicate export errors.
  • Loading branch information
strager committed Nov 21, 2023
1 parent e303fde commit 589a7fb
Show file tree
Hide file tree
Showing 4 changed files with 51 additions and 10 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.
* Detection of multiple `export default` statements ([E0715][]) now also applies
to `export {... as default};` statements.
* TypeScript support (still experimental):
* `export default` with a class and an interface (triggering declaration
merging) no longer fasely reports [E0715][] ("cannot use multiple `export
default` statements in one module").
* `export as namespace` statements are now parsed.
* Assertion signatures (`function f(param): asserts param`) are now parsed.
* `case await x:` no longer treats `:` as if it was a type annotation colon in
Expand Down
37 changes: 28 additions & 9 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,6 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
// export default class C {}
case Token_Type::kw_default: {
Source_Code_Span default_keyword = this->peek().span();
this->found_default_export(default_keyword);

this->is_current_typescript_namespace_non_empty_ = true;
if (this->in_typescript_namespace_or_module_.has_value() &&
Expand All @@ -1027,6 +1026,11 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
});
}
this->skip();

this->found_default_export(default_keyword,
/*is_mergeable_interface=*/this->peek().type ==
Token_Type::kw_interface);

switch (this->peek().type) {
// export default async function f() {}
// export default async () => {}
Expand Down Expand Up @@ -1574,15 +1578,29 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
}
}

void Parser::found_default_export(Source_Code_Span default_keyword) {
void Parser::found_default_export(Source_Code_Span default_keyword,
bool is_mergeable_interface) {
if (this->first_export_default_statement_default_keyword_.has_value()) {
this->diag_reporter_->report(Diag_Multiple_Export_Defaults{
.second_export_default = default_keyword,
.first_export_default =
*this->first_export_default_statement_default_keyword_,
});
if (is_mergeable_interface) {
// export default class {} export default interface I {}
} else {
// export default class {} export default class {} // Invalid.
this->diag_reporter_->report(Diag_Multiple_Export_Defaults{
.second_export_default = default_keyword,
.first_export_default =
*this->first_export_default_statement_default_keyword_,
});
}
} else {
this->first_export_default_statement_default_keyword_ = default_keyword;
if (is_mergeable_interface) {
// export default interface I {} ...
// export default interface I {} export default class {} // Merged.
// Allow an interface to merge with any other default export.
// TODO(#1107): An interface is not always mergeable with anything.
} else {
// export default class {} ...
this->first_export_default_statement_default_keyword_ = default_keyword;
}
}
}

Expand Down Expand Up @@ -4726,7 +4744,8 @@ void Parser::parse_and_visit_named_exports(
v.visit_variable_type_use(left_name);
} else if (right_token.type == Token_Type::kw_default) {
// export {C as default};
this->found_default_export(/*default_keyword=*/right_token.span());
this->found_default_export(/*default_keyword=*/right_token.span(),
/*is_mergeable_interface=*/false);
v.visit_variable_export_default_use(left_name);
} else {
// export {C};
Expand Down
3 changes: 2 additions & 1 deletion src/quick-lint-js/fe/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,8 @@ class Parser {
std::optional<Source_Code_Span> typescript_type_only_keyword,
Vector<Token> *out_exported_bad_tokens);

void found_default_export(Source_Code_Span default_keyword);
void found_default_export(Source_Code_Span default_keyword,
bool is_mergeable_interface);

void parse_and_visit_variable_declaration_statement(
Parse_Visitor_Base &v,
Expand Down
18 changes: 18 additions & 0 deletions test/test-parse-module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,24 @@ TEST_F(Test_Parse_Module, multiple_default_exports) {
}
}

TEST_F(Test_Parse_Module,
default_exports_can_be_typescript_declaration_merged) {
{
Spy_Visitor p = test_parse_and_visit_module(
u8"export default class C {} export default interface I {}"_sv,
no_diags, typescript_options);
}

{
Spy_Visitor p = test_parse_and_visit_module(
u8"export default interface I {} export default class C {}"_sv,
no_diags, typescript_options);
}

// TODO(#1107): There are some cases where declaration merging does not occur
// between classes and interfaces.
}

TEST_F(Test_Parse_Module,
export_default_with_contextual_keyword_variable_expression) {
Dirty_Set<String8> variable_names =
Expand Down

0 comments on commit 589a7fb

Please sign in to comment.