From af790cad6780a2af56eb20bff2d4470b3208d821 Mon Sep 17 00:00:00 2001 From: "Matthew \"strager\" Glazar" Date: Wed, 27 Dec 2023 15:25:37 -0500 Subject: [PATCH] feat(typescript): parse abstract constructor types --- docs/CHANGELOG.md | 2 + po/messages.pot | 4 ++ .../diag/diagnostic-metadata-generated.cpp | 14 ++++ .../diag/diagnostic-metadata-generated.h | 3 +- src/quick-lint-js/diag/diagnostic-types-2.h | 6 ++ src/quick-lint-js/fe/parse-type.cpp | 40 ++++++++++- .../i18n/translation-table-generated.cpp | 4 +- .../i18n/translation-table-generated.h | 5 +- .../i18n/translation-table-test-generated.h | 13 +++- test/test-parse-typescript-type.cpp | 72 +++++++++++++++++++ 10 files changed, 157 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0fa25556ea..37960444fc 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -44,6 +44,8 @@ Semantic Versioning. * Assigning to a class now reports [E0003][] ("cannot assign to class"). * Definite assignment assertions (`!` after a variable name in `let` or `var`) is now supported. + * Abstract constructor types (such as `abstract new () => C`) are now + supported. ### Fixed diff --git a/po/messages.pot b/po/messages.pot index 241ca1f2f9..aca3b90ec1 100644 --- a/po/messages.pot +++ b/po/messages.pot @@ -1205,6 +1205,10 @@ msgstr "" msgid "missing name or parentheses for function" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "missing 'new' in constructor type" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "missing operand for operator" msgstr "" diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp index 0567153b3b..8f28e6c7bb 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp @@ -3022,6 +3022,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Missing_New_In_Abstract_Constructor_Type + { + .code = 447, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("missing 'new' in constructor type"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Missing_New_In_Abstract_Constructor_Type, expected_new), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Missing_Operand_For_Operator { .code = 26, diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.h b/src/quick-lint-js/diag/diagnostic-metadata-generated.h index 20e51dca57..762529bb60 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.h +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.h @@ -212,6 +212,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Missing_Name_Of_Exported_Class) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Name_Of_Exported_Function) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Name_Or_Parentheses_For_Function) \ + QLJS_DIAG_TYPE_NAME(Diag_Missing_New_In_Abstract_Constructor_Type) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Operand_For_Operator) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Parameter_Name) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Separator_Between_Object_Type_Entries) \ @@ -468,7 +469,7 @@ namespace quick_lint_js { /* END */ // clang-format on -inline constexpr int Diag_Type_Count = 454; +inline constexpr int Diag_Type_Count = 455; 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 40ead53d3d..7b53dfb314 100644 --- a/src/quick-lint-js/diag/diagnostic-types-2.h +++ b/src/quick-lint-js/diag/diagnostic-types-2.h @@ -1553,6 +1553,12 @@ struct Diag_Missing_Name_Or_Parentheses_For_Function { Source_Code_Span function; }; +struct Diag_Missing_New_In_Abstract_Constructor_Type { + [[qljs::diag("E0447", Diagnostic_Severity::error)]] // + [[qljs::message("missing 'new' in constructor type", ARG(expected_new))]] // + Source_Code_Span expected_new; +}; + struct Diag_Missing_Operand_For_Operator { [[qljs::diag("E0026", Diagnostic_Severity::error)]] // [[qljs::message("missing operand for operator", ARG(where))]] // diff --git a/src/quick-lint-js/fe/parse-type.cpp b/src/quick-lint-js/fe/parse-type.cpp index 82fff5d41d..92d93051b0 100644 --- a/src/quick-lint-js/fe/parse-type.cpp +++ b/src/quick-lint-js/fe/parse-type.cpp @@ -201,7 +201,6 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope( // ns.Type // param is Type type_variable_or_namespace_or_type_predicate: - case Token_Type::kw_abstract: case Token_Type::kw_accessor: case Token_Type::kw_as: case Token_Type::kw_assert: @@ -495,6 +494,45 @@ void Parser::parse_and_visit_typescript_type_expression_no_scope( this->parse_and_visit_typescript_arrow_type_expression(v); break; + // abstract + // abstract new (param, param) => ReturnType + case Token_Type::kw_abstract: { + Lexer_Transaction transaction = this->lexer_.begin_transaction(); + this->skip(); + switch (this->peek().type) { + // abstract new (param, param) => ReturnType + case Token_Type::kw_new: + this->lexer_.commit_transaction(std::move(transaction)); + this->skip(); + this->parse_and_visit_typescript_arrow_type_expression(v); + break; + + // type T = abstract /*ASI*/ (param, param) => body; + // abstract (param, param) => ReturnType // Invalid. + case Token_Type::left_paren: + if (this->peek().has_leading_newline) { + // ASI. + this->lexer_.roll_back_transaction(std::move(transaction)); + goto type_variable_or_namespace_or_type_predicate; + } + // Missing 'new' keyword. + this->lexer_.commit_transaction(std::move(transaction)); + this->diag_reporter_->report( + Diag_Missing_New_In_Abstract_Constructor_Type{ + .expected_new = Source_Code_Span::unit(this->peek().begin), + }); + this->parse_and_visit_typescript_arrow_type_expression(v); + break; + + // type T = abstract; + // [abstract] + default: + this->lexer_.roll_back_transaction(std::move(transaction)); + goto type_variable_or_namespace_or_type_predicate; + } + break; + } + // (param, param) => ReturnType case Token_Type::less: this->parse_and_visit_typescript_arrow_type_expression(v); diff --git a/src/quick-lint-js/i18n/translation-table-generated.cpp b/src/quick-lint-js/i18n/translation-table-generated.cpp index 03a8ee8635..94d0e3f6c5 100644 --- a/src/quick-lint-js/i18n/translation-table-generated.cpp +++ b/src/quick-lint-js/i18n/translation-table-generated.cpp @@ -388,7 +388,8 @@ const Translation_Table translation_data = { {0, 0, 0, 0, 0, 27}, // {0, 0, 0, 0, 0, 74}, // {0, 40, 0, 28, 0, 38}, // - {29, 22, 33, 28, 26, 26}, // + {0, 0, 0, 0, 0, 26}, // + {29, 22, 33, 28, 26, 34}, // {0, 0, 0, 55, 0, 51}, // {48, 27, 63, 27, 0, 24}, // {40, 4, 58, 41, 50, 42}, // @@ -2252,6 +2253,7 @@ const Translation_Table translation_data = { u8"missing 'break;' or '// fallthrough' comment between statement and 'case'\0" u8"missing 'export' keyword for function\0" u8"missing 'if' after 'else'\0" + u8"missing 'new' in constructor type\0" u8"missing 'while (condition)' for do-while statement\0" u8"missing TypeScript type\0" u8"missing arrow operator for arrow function\0" diff --git a/src/quick-lint-js/i18n/translation-table-generated.h b/src/quick-lint-js/i18n/translation-table-generated.h index 883602d038..671c1b6181 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 = 598; -constexpr std::size_t translation_table_string_table_size = 82115; +constexpr std::uint16_t translation_table_mapping_table_size = 599; +constexpr std::size_t translation_table_string_table_size = 82149; constexpr std::size_t translation_table_locale_table_size = 35; QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( @@ -403,6 +403,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "missing 'break;' or '// fallthrough' comment between statement and 'case'"sv, "missing 'export' keyword for function"sv, "missing 'if' after 'else'"sv, + "missing 'new' in constructor type"sv, "missing 'while (condition)' for do-while statement"sv, "missing TypeScript type"sv, "missing arrow operator for arrow function"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 5f4c475265..2010bf2dec 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[597] = { +inline const Translated_String test_translation_table[598] = { { "\"global-groups\" entries must be strings"_translatable, u8"\"global-groups\" entries must be strings", @@ -4175,6 +4175,17 @@ inline const Translated_String test_translation_table[597] = { u8"missing 'if' after 'else'", }, }, + { + "missing 'new' in constructor type"_translatable, + u8"missing 'new' in constructor type", + { + u8"missing 'new' in constructor type", + u8"missing 'new' in constructor type", + u8"missing 'new' in constructor type", + u8"missing 'new' in constructor type", + u8"missing 'new' in constructor type", + }, + }, { "missing 'while (condition)' for do-while statement"_translatable, u8"missing 'while (condition)' for do-while statement", diff --git a/test/test-parse-typescript-type.cpp b/test/test-parse-typescript-type.cpp index 3ccb5338a8..bff1eeb07b 100644 --- a/test/test-parse-typescript-type.cpp +++ b/test/test-parse-typescript-type.cpp @@ -1141,6 +1141,78 @@ TEST_F(Test_Parse_TypeScript_Type, constructor_function) { } } +TEST_F(Test_Parse_TypeScript_Type, abstract_constructor_function) { + { + Spy_Visitor p = test_parse_and_visit_typescript_type_expression( + u8"abstract new () => ReturnType"_sv, no_diags, typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_function_scope", // + "visit_enter_type_scope", // => + "visit_variable_type_use", // ReturnType + "visit_exit_type_scope", // + "visit_exit_function_scope", + })); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"ReturnType"})); + } +} + +TEST_F(Test_Parse_TypeScript_Type, + abstract_constructor_function_requires_new_keyword) { + { + Spy_Visitor p = test_parse_and_visit_typescript_type_expression( + u8"abstract () => ReturnType"_sv, // + u8" ` Diag_Missing_New_In_Abstract_Constructor_Type.expected_new"_diag, + typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_function_scope", // + "visit_enter_type_scope", // => + "visit_variable_type_use", // ReturnType + "visit_exit_type_scope", // + "visit_exit_function_scope", + })); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"ReturnType"})); + } +} + +TEST_F( + Test_Parse_TypeScript_Type, + newline_between_abstract_and_new_in_constructor_function_does_not_trigger_asi) { + { + Spy_Visitor p = test_parse_and_visit_typescript_type_expression( + u8"abstract\nnew () => ReturnType"_sv, no_diags, typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_function_scope", // + "visit_enter_type_scope", // => + "visit_variable_type_use", // ReturnType + "visit_exit_type_scope", // + "visit_exit_function_scope", + })); + } +} + +TEST_F( + Test_Parse_TypeScript_Type, + newline_after_abstract_without_new_in_constructor_function_triggers_asi) { + { + Spy_Visitor p = + test_parse_and_visit_module(u8"type T = abstract\n() => ReturnType;"_sv, + no_diags, typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_variable_declaration", // T + "visit_enter_type_scope", // + "visit_variable_type_use", // abstract + "visit_exit_type_scope", // + "visit_enter_function_scope", // + "visit_enter_function_scope_body", // => + "visit_variable_use", // ReturnType + "visit_exit_function_scope", // + "visit_end_of_module", // + })); + EXPECT_THAT(p.variable_uses, + ElementsAreArray({u8"abstract"_sv, u8"ReturnType"_sv})); + } +} + TEST_F(Test_Parse_TypeScript_Type, array) { { Spy_Visitor p = test_parse_and_visit_typescript_type_expression(