Skip to content

Commit

Permalink
feat: error on multiple export defaults in module
Browse files Browse the repository at this point in the history
  • Loading branch information
arieldon committed Oct 25, 2023
1 parent 05a4d14 commit 28dcac6
Show file tree
Hide file tree
Showing 11 changed files with 138 additions and 5 deletions.
31 changes: 31 additions & 0 deletions docs/errors/E0715.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# E0715: cannot use multiple `export default` statements in one module

Modules in JavaScript can use two types of exports: default export and named export. While a module
can use multiple named exports, it can only use a single default export.


```javascript
export default function foo() {
console.log("foo");
}

export default function bar() {
console.log("bar");
}
```


If you want to export several values from a module, use named exports.


```javascript
function foo(x) {
console.log("foo");
}

function bar(x) {
console.log("bar");
}

export { foo, bar };
```
8 changes: 8 additions & 0 deletions po/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -2081,6 +2081,14 @@ msgstr ""
msgid "'async' keyword is not allowed on getters or setters"
msgstr ""

#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
msgid "cannot use multiple `export default` statements in one module"
msgstr ""

#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
msgid "export default previously appeared here"
msgstr ""

#: test/test-diagnostic-formatter.cpp
#: test/test-vim-qflist-json-diag-reporter.cpp
msgid "something happened"
Expand Down
18 changes: 18 additions & 0 deletions src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6407,6 +6407,24 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = {
},
},
},

// Diag_Multiple_Export_Defaults
{
.code = 715,
.severity = Diagnostic_Severity::error,
.message_formats = {
QLJS_TRANSLATABLE("cannot use multiple `export default` statements in one module"),
QLJS_TRANSLATABLE("export default previously appeared here"),
},
.message_args = {
{
Diagnostic_Message_Arg_Info(offsetof(Diag_Multiple_Export_Defaults, second_export_default), Diagnostic_Arg_Type::source_code_span),
},
{
Diagnostic_Message_Arg_Info(offsetof(Diag_Multiple_Export_Defaults, first_export_default), Diagnostic_Arg_Type::source_code_span),
},
},
},
};
}

Expand Down
3 changes: 2 additions & 1 deletion src/quick-lint-js/diag/diagnostic-metadata-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -439,10 +439,11 @@ namespace quick_lint_js {
QLJS_DIAG_TYPE_NAME(Diag_Missing_Comma_Between_Array_Elements) \
QLJS_DIAG_TYPE_NAME(Diag_Class_Generator_On_Getter_Or_Setter) \
QLJS_DIAG_TYPE_NAME(Diag_Class_Async_On_Getter_Or_Setter) \
QLJS_DIAG_TYPE_NAME(Diag_Multiple_Export_Defaults) \
/* END */
// clang-format on

inline constexpr int Diag_Type_Count = 428;
inline constexpr int Diag_Type_Count = 429;

extern const Diagnostic_Info all_diagnostic_infos[Diag_Type_Count];
}
Expand Down
11 changes: 11 additions & 0 deletions src/quick-lint-js/diag/diagnostic-types-2.h
Original file line number Diff line number Diff line change
Expand Up @@ -3307,6 +3307,17 @@ struct Diag_Class_Async_On_Getter_Or_Setter {
Source_Code_Span async_keyword;
Source_Code_Span getter_setter_keyword;
};

struct Diag_Multiple_Export_Defaults {
[[qljs::diag("E0715", Diagnostic_Severity::error)]] //
[[qljs::message(
"cannot use multiple `export default` statements in one module",
ARG(second_export_default))]] //
[[qljs::message("export default previously appeared here",
ARG(first_export_default))]] //
Source_Code_Span second_export_default;
Source_Code_Span first_export_default;
};
}
QLJS_WARNING_POP

Expand Down
13 changes: 13 additions & 0 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ bool Parser::parse_and_visit_module_catching_fatal_parse_errors(
}

void Parser::parse_and_visit_module(Parse_Visitor_Base &v) {
QLJS_ASSERT(
!this->first_export_default_statement_default_keyword_.has_value());
bool done = false;
Parse_Statement_Options statement_options = {
.possibly_followed_by_another_statement = true,
Expand Down Expand Up @@ -1011,6 +1013,17 @@ 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->is_current_typescript_namespace_non_empty_ = true;
if (this->in_typescript_namespace_or_module_.has_value() &&
!this->in_typescript_module_) {
Expand Down
5 changes: 5 additions & 0 deletions src/quick-lint-js/fe/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,11 @@ class Parser {
void parse_and_visit_named_exports_for_typescript_type_only_import(
Parse_Visitor_Base &v, Source_Code_Span type_keyword);

// If set, refers to the first `export default` statement in this module. A
// module cannot contain more than one `export default`.
std::optional<Source_Code_Span>
first_export_default_statement_default_keyword_ = std::nullopt;

struct Parse_Export_Options {
TypeScript_Declare_Context declare_context;

Expand Down
6 changes: 5 additions & 1 deletion src/quick-lint-js/i18n/translation-table-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ const Translation_Table translation_data = {
{0, 0, 0, 67, 0, 53}, //
{0, 0, 0, 0, 0, 37}, //
{0, 0, 0, 0, 0, 43}, //
{0, 0, 0, 0, 0, 62}, //
{0, 0, 0, 50, 0, 47}, //
{72, 31, 71, 68, 56, 61}, //
{34, 30, 0, 46, 0, 40}, //
Expand Down Expand Up @@ -259,7 +260,8 @@ const Translation_Table translation_data = {
{33, 27, 36, 45, 0, 35}, //
{39, 42, 0, 49, 0, 41}, //
{24, 24, 0, 24, 0, 24}, //
{22, 22, 42, 22, 40, 22}, //
{0, 0, 0, 0, 0, 22}, //
{22, 22, 42, 22, 40, 40}, //
{32, 30, 35, 26, 30, 29}, //
{0, 0, 0, 27, 0, 32}, //
{35, 45, 38, 53, 33, 46}, //
Expand Down Expand Up @@ -1998,6 +2000,7 @@ const Translation_Table translation_data = {
u8"cannot update variable with '{0}' while declaring it\0"
u8"cannot use '...' on 'this' parameter\0"
u8"cannot use 'declare' keyword with 'import'\0"
u8"cannot use multiple `export default` statements in one module\0"
u8"cannot use type directly in its own definition\0"
u8"catch variable can only be typed as '*', 'any', or 'unknown'\0"
u8"character is not allowed in identifiers\0"
Expand Down Expand Up @@ -2052,6 +2055,7 @@ const Translation_Table translation_data = {
u8"expected variable name for 'import'-'as'\0"
u8"expected {1:headlinese}\0"
u8"expected {1:singular}\0"
u8"export default previously appeared here\0"
u8"exporting requires 'default'\0"
u8"exporting requires '{{' and '}'\0"
u8"extra ',' is not allowed between enum members\0"
Expand Down
6 changes: 4 additions & 2 deletions src/quick-lint-js/i18n/translation-table-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ namespace quick_lint_js {
using namespace std::literals::string_view_literals;

constexpr std::uint32_t translation_table_locale_count = 5;
constexpr std::uint16_t translation_table_mapping_table_size = 522;
constexpr std::size_t translation_table_string_table_size = 79941;
constexpr std::uint16_t translation_table_mapping_table_size = 524;
constexpr std::size_t translation_table_string_table_size = 80043;
constexpr std::size_t translation_table_locale_table_size = 35;

QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
Expand Down Expand Up @@ -220,6 +220,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
"cannot update variable with '{0}' while declaring it"sv,
"cannot use '...' on 'this' parameter"sv,
"cannot use 'declare' keyword with 'import'"sv,
"cannot use multiple `export default` statements in one module"sv,
"cannot use type directly in its own definition"sv,
"catch variable can only be typed as '*', 'any', or 'unknown'"sv,
"character is not allowed in identifiers"sv,
Expand Down Expand Up @@ -274,6 +275,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
"expected variable name for 'import'-'as'"sv,
"expected {1:headlinese}"sv,
"expected {1:singular}"sv,
"export default previously appeared here"sv,
"exporting requires 'default'"sv,
"exporting requires '{{' and '}'"sv,
"extra ',' is not allowed between enum members"sv,
Expand Down
24 changes: 23 additions & 1 deletion src/quick-lint-js/i18n/translation-table-test-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct Translated_String {
};

// clang-format off
inline const Translated_String test_translation_table[521] = {
inline const Translated_String test_translation_table[523] = {
{
"\"global-groups\" entries must be strings"_translatable,
u8"\"global-groups\" entries must be strings",
Expand Down Expand Up @@ -2162,6 +2162,17 @@ inline const Translated_String test_translation_table[521] = {
u8"cannot use 'declare' keyword with 'import'",
},
},
{
"cannot use multiple `export default` statements in one module"_translatable,
u8"cannot use multiple `export default` statements in one module",
{
u8"cannot use multiple `export default` statements in one module",
u8"cannot use multiple `export default` statements in one module",
u8"cannot use multiple `export default` statements in one module",
u8"cannot use multiple `export default` statements in one module",
u8"cannot use multiple `export default` statements in one module",
},
},
{
"cannot use type directly in its own definition"_translatable,
u8"cannot use type directly in its own definition",
Expand Down Expand Up @@ -2756,6 +2767,17 @@ inline const Translated_String test_translation_table[521] = {
u8"expected {1:singular}",
},
},
{
"export default previously appeared here"_translatable,
u8"export default previously appeared here",
{
u8"export default previously appeared here",
u8"export default previously appeared here",
u8"export default previously appeared here",
u8"export default previously appeared here",
u8"export default previously appeared here",
},
},
{
"exporting requires 'default'"_translatable,
u8"exporting requires 'default'",
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 @@ -162,6 +162,24 @@ 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 Down

0 comments on commit 28dcac6

Please sign in to comment.