diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 47922a7b4c..d1b9c37ba8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/src/quick-lint-js/fe/parse-statement.cpp b/src/quick-lint-js/fe/parse-statement.cpp index 88c3630f94..bbadb0268f 100644 --- a/src/quick-lint-js/fe/parse-statement.cpp +++ b/src/quick-lint-js/fe/parse-statement.cpp @@ -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() && @@ -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 () => {} @@ -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; + } } } @@ -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}; diff --git a/src/quick-lint-js/fe/parse.h b/src/quick-lint-js/fe/parse.h index bfe19fe522..7b58484667 100644 --- a/src/quick-lint-js/fe/parse.h +++ b/src/quick-lint-js/fe/parse.h @@ -568,7 +568,8 @@ class Parser { std::optional typescript_type_only_keyword, Vector *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, diff --git a/test/test-parse-module.cpp b/test/test-parse-module.cpp index 453def2348..645255d973 100644 --- a/test/test-parse-module.cpp +++ b/test/test-parse-module.cpp @@ -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 variable_names =