From 28dcac69d4046b34c9af3e085694ecad7f8f9fb2 Mon Sep 17 00:00:00 2001 From: Ariel Don Date: Mon, 23 Oct 2023 19:51:42 -0500 Subject: [PATCH] feat: error on multiple export defaults in module --- docs/errors/E0715.md | 31 +++++++++++++++++++ po/messages.pot | 8 +++++ .../diag/diagnostic-metadata-generated.cpp | 18 +++++++++++ .../diag/diagnostic-metadata-generated.h | 3 +- src/quick-lint-js/diag/diagnostic-types-2.h | 11 +++++++ src/quick-lint-js/fe/parse-statement.cpp | 13 ++++++++ src/quick-lint-js/fe/parse.h | 5 +++ .../i18n/translation-table-generated.cpp | 6 +++- .../i18n/translation-table-generated.h | 6 ++-- .../i18n/translation-table-test-generated.h | 24 +++++++++++++- test/test-parse-module.cpp | 18 +++++++++++ 11 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 docs/errors/E0715.md diff --git a/docs/errors/E0715.md b/docs/errors/E0715.md new file mode 100644 index 0000000000..2fd63b9315 --- /dev/null +++ b/docs/errors/E0715.md @@ -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 }; +``` diff --git a/po/messages.pot b/po/messages.pot index baa254be30..57504375ed 100644 --- a/po/messages.pot +++ b/po/messages.pot @@ -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" diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp index e53d4f6276..8f4b3cface 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp @@ -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), + }, + }, + }, }; } diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.h b/src/quick-lint-js/diag/diagnostic-metadata-generated.h index 28d0409760..ce26201539 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.h +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.h @@ -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]; } diff --git a/src/quick-lint-js/diag/diagnostic-types-2.h b/src/quick-lint-js/diag/diagnostic-types-2.h index 07918727b5..59839d2462 100644 --- a/src/quick-lint-js/diag/diagnostic-types-2.h +++ b/src/quick-lint-js/diag/diagnostic-types-2.h @@ -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 diff --git a/src/quick-lint-js/fe/parse-statement.cpp b/src/quick-lint-js/fe/parse-statement.cpp index 8902971c06..f97a0b74f6 100644 --- a/src/quick-lint-js/fe/parse-statement.cpp +++ b/src/quick-lint-js/fe/parse-statement.cpp @@ -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, @@ -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_) { diff --git a/src/quick-lint-js/fe/parse.h b/src/quick-lint-js/fe/parse.h index e5a8790db6..69d55e4f1c 100644 --- a/src/quick-lint-js/fe/parse.h +++ b/src/quick-lint-js/fe/parse.h @@ -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 + first_export_default_statement_default_keyword_ = std::nullopt; + struct Parse_Export_Options { TypeScript_Declare_Context declare_context; diff --git a/src/quick-lint-js/i18n/translation-table-generated.cpp b/src/quick-lint-js/i18n/translation-table-generated.cpp index ace2dc583b..04ee6b82d4 100644 --- a/src/quick-lint-js/i18n/translation-table-generated.cpp +++ b/src/quick-lint-js/i18n/translation-table-generated.cpp @@ -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}, // @@ -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}, // @@ -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" @@ -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" diff --git a/src/quick-lint-js/i18n/translation-table-generated.h b/src/quick-lint-js/i18n/translation-table-generated.h index 16657ed1f5..514e20b711 100644 --- a/src/quick-lint-js/i18n/translation-table-generated.h +++ b/src/quick-lint-js/i18n/translation-table-generated.h @@ -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( @@ -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, @@ -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, diff --git a/src/quick-lint-js/i18n/translation-table-test-generated.h b/src/quick-lint-js/i18n/translation-table-test-generated.h index f50238d171..59c04ad26a 100644 --- a/src/quick-lint-js/i18n/translation-table-test-generated.h +++ b/src/quick-lint-js/i18n/translation-table-test-generated.h @@ -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", @@ -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", @@ -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'", diff --git a/test/test-parse-module.cpp b/test/test-parse-module.cpp index 4bfee6a51e..a6dac87cec 100644 --- a/test/test-parse-module.cpp +++ b/test/test-parse-module.cpp @@ -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);