Skip to content

Commit

Permalink
feat(fe): warn on duplicate export when using 'export {X as default};…
Browse files Browse the repository at this point in the history
…' syntax

quick-lint-js reports duplicate default exports but only when using
the 'export default ...;' syntax. Run the duplicate export logic when
using 'export {X as default};' syntax too.
  • Loading branch information
strager committed Nov 21, 2023
1 parent 38cdcaa commit b3448d5
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 28 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Semantic Versioning.
clause in `switch` statement now reports [E0427][] ("missing 'break;' or '//
fallthrough' comment between statement and 'case'"). (Implemented by [Yash
Masani][].)
* Detection of multiple `export default` statements ([E0715][]) now also applies
to `export {... as default};` statements.
* TypeScript support (still experimental):
* `export as namespace` statements are now parsed.
* Assertion signatures (`function f(param): asserts param`) are now parsed.
Expand Down
24 changes: 14 additions & 10 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1014,16 +1014,7 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
switch (this->peek().type) {
// export default class C {}
case Token_Type::kw_default:
if (this->first_export_default_statement_default_keyword_.has_value()) {
this->diag_reporter_->report(Diag_Multiple_Export_Defaults{
.second_export_default = this->peek().span(),
.first_export_default =
*this->first_export_default_statement_default_keyword_,
});
} else {
this->first_export_default_statement_default_keyword_ =
this->peek().span();
}
this->found_default_export(this->peek().span());

this->is_current_typescript_namespace_non_empty_ = true;
if (this->in_typescript_namespace_or_module_.has_value() &&
Expand Down Expand Up @@ -1581,6 +1572,18 @@ void Parser::parse_and_visit_export(Parse_Visitor_Base &v,
}
}

void Parser::found_default_export(Source_Code_Span default_keyword) {
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_,
});
} else {
this->first_export_default_statement_default_keyword_ = default_keyword;
}
}

void Parser::parse_and_visit_typescript_generic_parameters(
Parse_Visitor_Base &v) {
QLJS_ASSERT(this->peek().type == Token_Type::less);
Expand Down Expand Up @@ -4721,6 +4724,7 @@ 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());
v.visit_variable_export_default_use(left_name);
} else {
// export {C};
Expand Down
2 changes: 2 additions & 0 deletions src/quick-lint-js/fe/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +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 parse_and_visit_variable_declaration_statement(
Parse_Visitor_Base &v,
bool is_top_level_typescript_definition_without_declare_or_export =
Expand Down
48 changes: 30 additions & 18 deletions test/test-parse-module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,24 +172,6 @@ TEST_F(Test_Parse_Module, export_default) {
}));
}

{
Spy_Visitor p = test_parse_and_visit_module(
u8"export default class A {} export default class B {}"_sv, //
u8" ^^^^^^^ Diag_Multiple_Export_Defaults.first_export_default\n"
u8" ^^^^^^^ .second_export_default"_diag);
EXPECT_THAT(p.visits, ElementsAreArray({
"visit_enter_class_scope", // A
"visit_enter_class_scope_body", //
"visit_exit_class_scope", //
"visit_variable_declaration", //
"visit_enter_class_scope", // B
"visit_enter_class_scope_body", //
"visit_exit_class_scope", //
"visit_variable_declaration", //
"visit_end_of_module",
}));
}

{
Spy_Visitor p = test_parse_and_visit_statement(
u8"export default async (a) => b;"_sv, no_diags, javascript_options);
Expand All @@ -203,6 +185,36 @@ TEST_F(Test_Parse_Module, export_default) {
}
}

TEST_F(Test_Parse_Module, multiple_default_exports) {
{
Spy_Visitor p = test_parse_and_visit_module(
u8"export default class A {} export default class B {}"_sv, //
u8" ^^^^^^^ Diag_Multiple_Export_Defaults.second_export_default\n"_diag
u8" ^^^^^^^ .first_export_default"_diag);
}

{
Spy_Visitor p = test_parse_and_visit_module(
u8"export default class A {} export {B as default};"_sv, //
u8" ^^^^^^^ Diag_Multiple_Export_Defaults.second_export_default\n"_diag
u8" ^^^^^^^ .first_export_default"_diag);
}

{
Spy_Visitor p = test_parse_and_visit_module(
u8"export {A as default}; export default class B {}"_sv, //
u8" ^^^^^^^ Diag_Multiple_Export_Defaults.second_export_default\n"_diag
u8" ^^^^^^^ .first_export_default"_diag);
}

{
Spy_Visitor p = test_parse_and_visit_module(
u8"export {A as default}; export {B as default};"_sv, //
u8" ^^^^^^^ Diag_Multiple_Export_Defaults.second_export_default\n"_diag
u8" ^^^^^^^ .first_export_default"_diag);
}
}

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

0 comments on commit b3448d5

Please sign in to comment.