diff --git a/po/messages.pot b/po/messages.pot index fc53566b34..61c0b5c596 100644 --- a/po/messages.pot +++ b/po/messages.pot @@ -309,6 +309,42 @@ msgstr "" msgid "TypeScript 'declare {1}' is not allowed in JavaScript" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "decorators must appear before '{1}" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "write the decorator before here" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "static blocks cannot have a decorator" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "static block starts here" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "decorators are not allowed inside TypeScript interfaces" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "decorators are not allowed on abstract properties" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "property declared 'abstract' here" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "decorators must appear after overload signatures" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "decorator belongs immediately before this overloaded method" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "'function async' is not allowed; write 'async function' instead" msgstr "" @@ -941,6 +977,14 @@ msgstr "" msgid "unexpected expression; missing key for object entry" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "missing class method or field after decorator" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "decorator starts here" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "missing name for class method" msgstr "" @@ -1733,6 +1777,10 @@ msgstr "" msgid "unexpected '?' when destructuring" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "semicolon is not allowed after decorators" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "TypeScript overload signature can only have one semicolon" msgstr "" diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp index 767fd895f6..09d9d99d97 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp @@ -663,6 +663,93 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Decorator_After_Class_Member_Modifiers + { + .code = 408, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("decorators must appear before '{1}"), + QLJS_TRANSLATABLE("write the decorator before here"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_After_Class_Member_Modifiers, decorator_at), Diagnostic_Arg_Type::source_code_span), + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_After_Class_Member_Modifiers, modifier), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_After_Class_Member_Modifiers, modifier), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + + // Diag_Decorator_Not_Allowed_On_Class_Static_Block + { + .code = 407, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("static blocks cannot have a decorator"), + QLJS_TRANSLATABLE("static block starts here"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_Not_Allowed_On_Class_Static_Block, decorator_at), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_Not_Allowed_On_Class_Static_Block, static_keyword), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + + // Diag_Decorator_In_TypeScript_Interface + { + .code = 411, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("decorators are not allowed inside TypeScript interfaces"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_In_TypeScript_Interface, decorator_at), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + + // Diag_Decorator_On_Abstract_Class_Member + { + .code = 412, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("decorators are not allowed on abstract properties"), + QLJS_TRANSLATABLE("property declared 'abstract' here"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_On_Abstract_Class_Member, decorator_at), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_On_Abstract_Class_Member, abstract_keyword), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + + // Diag_Decorator_On_Overload_Signature + { + .code = 413, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("decorators must appear after overload signatures"), + QLJS_TRANSLATABLE("decorator belongs immediately before this overloaded method"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_On_Overload_Signature, decorator_at), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Decorator_On_Overload_Signature, expected_location), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Function_Async_Function { .code = 327, @@ -2767,6 +2854,24 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Missing_Class_Member_After_Decorator + { + .code = 409, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("missing class method or field after decorator"), + QLJS_TRANSLATABLE("decorator starts here"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Missing_Class_Member_After_Decorator, expected_member), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Missing_Class_Member_After_Decorator, decorator_at), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Missing_Class_Method_Name { .code = 229, @@ -5255,6 +5360,24 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Unexpected_Semicolon_After_Decorator + { + .code = 410, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("semicolon is not allowed after decorators"), + QLJS_TRANSLATABLE("decorator starts here"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Unexpected_Semicolon_After_Decorator, semicolon), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Unexpected_Semicolon_After_Decorator, decorator_at), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Unexpected_Semicolon_After_Overload_Signature { .code = 400, diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.h b/src/quick-lint-js/diag/diagnostic-metadata-generated.h index 786fe2a4a7..eda13ac5f3 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.h +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.h @@ -53,6 +53,11 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Declare_Namespace_Cannot_Import_Module) \ QLJS_DIAG_TYPE_NAME(Diag_Declare_Var_Cannot_Have_Initializer) \ QLJS_DIAG_TYPE_NAME(Diag_Declare_Var_Not_Allowed_In_JavaScript) \ + QLJS_DIAG_TYPE_NAME(Diag_Decorator_After_Class_Member_Modifiers) \ + QLJS_DIAG_TYPE_NAME(Diag_Decorator_Not_Allowed_On_Class_Static_Block) \ + QLJS_DIAG_TYPE_NAME(Diag_Decorator_In_TypeScript_Interface) \ + QLJS_DIAG_TYPE_NAME(Diag_Decorator_On_Abstract_Class_Member) \ + QLJS_DIAG_TYPE_NAME(Diag_Decorator_On_Overload_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Function_Async_Function) \ QLJS_DIAG_TYPE_NAME(Diag_Big_Int_Literal_Contains_Decimal_Point) \ QLJS_DIAG_TYPE_NAME(Diag_Big_Int_Literal_Contains_Exponent) \ @@ -196,6 +201,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Missing_Header_Of_For_Loop) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Initializer_In_Const_Declaration) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Key_For_Object_Entry) \ + QLJS_DIAG_TYPE_NAME(Diag_Missing_Class_Member_After_Decorator) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Class_Method_Name) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Name_In_Function_Statement) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Name_In_Class_Statement) \ @@ -362,6 +368,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Right_Curly_In_JSX_Text) \ QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Question_In_Expression) \ QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Question_When_Destructuring) \ + QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Semicolon_After_Decorator) \ QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Semicolon_After_Overload_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Semicolon_In_C_Style_For_Loop) \ QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Semicolon_In_For_In_Loop) \ @@ -424,7 +431,7 @@ namespace quick_lint_js { /* END */ // clang-format on -inline constexpr int Diag_Type_Count = 410; +inline constexpr int Diag_Type_Count = 417; 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 fa90b2d64f..a6288044ea 100644 --- a/src/quick-lint-js/diag/diagnostic-types-2.h +++ b/src/quick-lint-js/diag/diagnostic-types-2.h @@ -371,6 +371,52 @@ struct Diag_Declare_Var_Not_Allowed_In_JavaScript { Source_Code_Span declaring_token; }; +struct Diag_Decorator_After_Class_Member_Modifiers { + [[qljs::diag("E0408", Diagnostic_Severity::error)]] // + [[qljs::message("decorators must appear before '{1}", ARG(decorator_at), + ARG(modifier))]] // + [[qljs::message("write the decorator before here", ARG(modifier))]] // + Source_Code_Span decorator_at; + Source_Code_Span modifier; +}; + +struct Diag_Decorator_Not_Allowed_On_Class_Static_Block { + [[qljs::diag("E0407", Diagnostic_Severity::error)]] // + [[qljs::message("static blocks cannot have a decorator", + ARG(decorator_at))]] // + [[qljs::message("static block starts here", + ARG(static_keyword))]] // + Source_Code_Span decorator_at; + Source_Code_Span static_keyword; +}; + +struct Diag_Decorator_In_TypeScript_Interface { + [[qljs::diag("E0411", Diagnostic_Severity::error)]] // + [[qljs::message("decorators are not allowed inside TypeScript interfaces", + ARG(decorator_at))]] // + Source_Code_Span decorator_at; +}; + +struct Diag_Decorator_On_Abstract_Class_Member { + [[qljs::diag("E0412", Diagnostic_Severity::error)]] // + [[qljs::message("decorators are not allowed on abstract properties", + ARG(decorator_at))]] // + [[qljs::message("property declared 'abstract' here", + ARG(abstract_keyword))]] // + Source_Code_Span decorator_at; + Source_Code_Span abstract_keyword; +}; + +struct Diag_Decorator_On_Overload_Signature { + [[qljs::diag("E0413", Diagnostic_Severity::error)]] // + [[qljs::message("decorators must appear after overload signatures", + ARG(decorator_at))]] // + [[qljs::message("decorator belongs immediately before this overloaded method", + ARG(expected_location))]] // + Source_Code_Span decorator_at; + Source_Code_Span expected_location; +}; + struct Diag_Function_Async_Function { [[qljs::diag("E0327", Diagnostic_Severity::error)]] // [[qljs::message( @@ -1421,6 +1467,15 @@ struct Diag_Missing_Key_For_Object_Entry { Source_Code_Span expression; }; +struct Diag_Missing_Class_Member_After_Decorator { + [[qljs::diag("E0409", Diagnostic_Severity::error)]] // + [[qljs::message("missing class method or field after decorator", + ARG(expected_member))]] // + [[qljs::message("decorator starts here", ARG(decorator_at))]] // + Source_Code_Span expected_member; + Source_Code_Span decorator_at; +}; + struct Diag_Missing_Class_Method_Name { [[qljs::diag("E0229", Diagnostic_Severity::error)]] // [[qljs::message("missing name for class method", ARG(expected_name))]] // @@ -2711,6 +2766,16 @@ struct Diag_Unexpected_Question_When_Destructuring { Source_Code_Span question; }; +struct Diag_Unexpected_Semicolon_After_Decorator { + [[qljs::diag("E0410", Diagnostic_Severity::error)]] // + [[qljs::message("semicolon is not allowed after decorators", + ARG(semicolon))]] // + [[qljs::message("decorator starts here", + ARG(decorator_at))]] // + Source_Code_Span semicolon; + Source_Code_Span decorator_at; +}; + struct Diag_Unexpected_Semicolon_After_Overload_Signature { [[qljs::diag("E0400", Diagnostic_Severity::error)]] // [[qljs::message("TypeScript overload signature can only have one semicolon", diff --git a/src/quick-lint-js/fe/parse-class.cpp b/src/quick-lint-js/fe/parse-class.cpp index c82760fdca..5c3734863b 100644 --- a/src/quick-lint-js/fe/parse-class.cpp +++ b/src/quick-lint-js/fe/parse-class.cpp @@ -278,9 +278,12 @@ void Parser::parse_and_visit_class_or_interface_member( std::optional declare_keyword; std::optional last_ident; + // The location of where this member starts. It could be the start of the + // first modifier or the beginning of the member's name. + const Char8 *current_member_begin; // *, !, ?, accessor, async, function, get, private, protected, public, - // readonly, set, static + // readonly, set, static, @ (decorator) struct Modifier { Source_Code_Span span; Token_Type type; @@ -324,6 +327,7 @@ void Parser::parse_and_visit_class_or_interface_member( overload_signatures{"class overload signatures", &p->temporary_memory_}; void reset_state_except_overload_signatures() { + this->current_member_begin = p->peek().begin; this->last_ident = std::nullopt; this->modifiers.clear(); } @@ -423,6 +427,20 @@ void Parser::parse_and_visit_class_or_interface_member( p->skip(); continue; + // @decorator f() {} + case Token_Type::at: + if (this->is_interface) { + p->diag_reporter_->report(Diag_Decorator_In_TypeScript_Interface{ + .decorator_at = p->peek().span(), + }); + } + modifiers.push_back(Modifier{ + .span = p->peek().span(), + .type = p->peek().type, + }); + p->parse_and_visit_decorator(v); + continue; + default: return false; } @@ -507,6 +525,27 @@ void Parser::parse_and_visit_class_or_interface_member( if (last_ident.has_value()) { modifiers.pop_back(); parse_and_visit_field_or_method(*last_ident); + } else if (!this->modifiers.empty() && + this->modifiers.back().type == Token_Type::at) { + // @decorator; // Invalid. + const Char8 *end_of_decorator = p->lexer_.end_of_previous_token(); + Source_Code_Span semicolon_span = p->peek().span(); + p->skip(); + if (this->is_interface) { + // We already reported Diag_Decorator_In_TypeScript_Interface. + } else if (p->peek().type == Token_Type::right_curly) { + // class C { @decorator; } // Invalid. + p->diag_reporter_->report(Diag_Missing_Class_Member_After_Decorator{ + .expected_member = Source_Code_Span::unit(end_of_decorator), + .decorator_at = this->modifiers.back().span, + }); + } else { + // class C { @decorator; foo() {} } // Invalid. + p->diag_reporter_->report(Diag_Unexpected_Semicolon_After_Decorator{ + .semicolon = semicolon_span, + .decorator_at = this->modifiers.back().span, + }); + } } else { p->skip(); } @@ -546,6 +585,17 @@ void Parser::parse_and_visit_class_or_interface_member( } else if (last_ident.has_value()) { modifiers.pop_back(); parse_and_visit_field_or_method(*last_ident); + } else if (!this->modifiers.empty() && + this->modifiers.back().type == Token_Type::at) { + if (this->is_interface) { + // We already reported Diag_Decorator_In_TypeScript_Interface. + } else { + p->diag_reporter_->report(Diag_Missing_Class_Member_After_Decorator{ + .expected_member = + Source_Code_Span::unit(p->lexer_.end_of_previous_token()), + .decorator_at = this->modifiers.back().span, + }); + } } else { QLJS_PARSER_UNIMPLEMENTED_WITH_PARSER(p); } @@ -586,14 +636,14 @@ void Parser::parse_and_visit_class_or_interface_member( // class C { { // Invalid. // class C { static { } } // TypeScript only. case Token_Type::left_curly: - if (modifiers.size() == 1 && - modifiers[0].type == Token_Type::kw_static) { + if (!modifiers.empty() && + modifiers.back().type == Token_Type::kw_static) { // class C { static { } } v.visit_enter_block_scope(); Source_Code_Span left_curly_span = p->peek().span(); p->skip(); - error_if_invalid_static_block(/*static_modifier=*/modifiers[0]); + error_if_invalid_static_block(/*static_modifier=*/modifiers.back()); p->parse_and_visit_statement_block_after_left_curly(v, left_curly_span); @@ -1079,6 +1129,8 @@ void Parser::parse_and_visit_class_or_interface_member( error_if_invalid_access_specifier(); error_if_getter_setter_field(); error_if_access_specifier_not_first_in_field(); + error_if_decorator_not_first(); + error_if_abstract_with_decorator(); error_if_abstract_or_static_after_accessor(); error_if_conflicting_modifiers(); error_if_readonly_in_not_typescript(); @@ -1097,6 +1149,8 @@ void Parser::parse_and_visit_class_or_interface_member( error_if_async_or_generator_without_method_body(); error_if_invalid_access_specifier(); error_if_access_specifier_not_first_in_method(); + error_if_decorator_not_first(); + error_if_abstract_with_decorator(); error_if_static_in_interface(); error_if_static_abstract(); error_if_optional_in_not_typescript(); @@ -1135,6 +1189,14 @@ void Parser::parse_and_visit_class_or_interface_member( .static_token = static_modifier.span, }); } + if (const Modifier *decorator_modifier = + this->find_modifier(Token_Type::at)) { + p->diag_reporter_->report( + Diag_Decorator_Not_Allowed_On_Class_Static_Block{ + .decorator_at = decorator_modifier->span, + .static_keyword = static_modifier.span, + }); + } if (declare_keyword.has_value() && !is_interface) { if (p->peek().type == Token_Type::right_curly) { // static { } @@ -1358,6 +1420,52 @@ void Parser::parse_and_visit_class_or_interface_member( } } + void error_if_decorator_not_first() { + if (this->is_interface) { + // We already reported Diag_Decorator_In_TypeScript_Interface. + return; + } + Span_Size i = 0; + for (; i < this->modifiers.size() && + this->modifiers[i].type == Token_Type::at; + ++i) { + // Skip decorators. + } + if (i < this->modifiers.size()) { + // There is at least one non-decorator modifier. + const Modifier *non_decorator_modifier = &this->modifiers[i]; + if (non_decorator_modifier->type == Token_Type::kw_abstract) { + // abstract @decorator myMethod() {} // Invalid. + // Diag_Decorator_On_Abstract_Class_Member is reported elsewhere. + return; + } + for (; i < this->modifiers.size(); ++i) { + if (this->modifiers[i].type == Token_Type::at) { + p->diag_reporter_->report( + Diag_Decorator_After_Class_Member_Modifiers{ + .decorator_at = this->modifiers[i].span, + .modifier = non_decorator_modifier->span, + }); + } + } + } + } + + void error_if_abstract_with_decorator() { + if (const Modifier *abstract_modifier = + this->find_modifier(Token_Type::kw_abstract)) { + for (const Modifier &other_modifier : this->modifiers) { + if (other_modifier.type == Token_Type::at) { + // @decorator abstract myMethod() {} // Invalid. + p->diag_reporter_->report(Diag_Decorator_On_Abstract_Class_Member{ + .decorator_at = other_modifier.span, + .abstract_keyword = abstract_modifier->span, + }); + } + } + } + } + void error_if_abstract_or_static_after_accessor() { if (const Modifier *accessor_modifier = this->find_modifier(Token_Type::kw_accessor)) { @@ -1567,6 +1675,14 @@ void Parser::parse_and_visit_class_or_interface_member( .generator_star = star_modifier->span, }); } + if (const Modifier *decorator_modifier = + this->find_modifier(Token_Type::at, signature.modifiers)) { + p->diag_reporter_->report(Diag_Decorator_On_Overload_Signature{ + .decorator_at = decorator_modifier->span, + .expected_location = + Source_Code_Span::unit(this->current_member_begin), + }); + } const Modifier *signature_get_or_set_modifier = nullptr; if (signature_get_or_set_modifier == nullptr) { diff --git a/src/quick-lint-js/fe/parse-statement.cpp b/src/quick-lint-js/fe/parse-statement.cpp index 6ce1fa7c19..0d3e3d7586 100644 --- a/src/quick-lint-js/fe/parse-statement.cpp +++ b/src/quick-lint-js/fe/parse-statement.cpp @@ -2630,7 +2630,10 @@ void Parser::parse_and_visit_decorator(Parse_Visitor_Base &v) { // @(myDecorator) case Token_Type::left_paren: + this->skip(); this->parse_and_visit_expression(v); + QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::right_paren); + this->skip(); break; default: diff --git a/src/quick-lint-js/i18n/translation-table-generated.cpp b/src/quick-lint-js/i18n/translation-table-generated.cpp index ed4d9bcf69..0c4e89c6b5 100644 --- a/src/quick-lint-js/i18n/translation-table-generated.cpp +++ b/src/quick-lint-js/i18n/translation-table-generated.cpp @@ -212,7 +212,13 @@ const Translation_Table translation_data = { {90, 38, 91, 90, 53, 75}, // {0, 0, 0, 0, 0, 29}, // {41, 16, 34, 35, 34, 43}, // - {49, 11, 64, 46, 39, 43}, // + {0, 0, 0, 0, 0, 43}, // + {0, 0, 0, 0, 0, 60}, // + {0, 0, 0, 0, 0, 22}, // + {0, 0, 0, 0, 0, 56}, // + {0, 0, 0, 0, 0, 50}, // + {0, 0, 0, 0, 0, 49}, // + {49, 11, 64, 46, 39, 35}, // {46, 20, 32, 32, 31, 21}, // {47, 58, 59, 52, 39, 48}, // {51, 26, 71, 60, 46, 49}, // @@ -333,7 +339,8 @@ const Translation_Table translation_data = { {33, 31, 36, 32, 30, 28}, // {34, 47, 35, 36, 34, 32}, // {53, 77, 57, 54, 46, 50}, // - {0, 0, 0, 44, 0, 48}, // + {0, 0, 0, 0, 0, 48}, // + {0, 0, 0, 44, 0, 46}, // {44, 21, 60, 49, 44, 41}, // {49, 35, 0, 45, 0, 45}, // {76, 28, 56, 73, 48, 69}, // @@ -416,6 +423,7 @@ const Translation_Table translation_data = { {0, 0, 0, 0, 0, 44}, // {32, 13, 50, 41, 38, 29}, // {0, 0, 0, 0, 0, 54}, // + {0, 0, 0, 0, 0, 34}, // {0, 0, 0, 0, 0, 30}, // {66, 33, 76, 54, 58, 35}, // {40, 29, 35, 35, 33, 33}, // @@ -425,10 +433,13 @@ const Translation_Table translation_data = { {0, 0, 0, 56, 0, 44}, // {53, 14, 55, 19, 0, 19}, // {49, 31, 0, 52, 0, 45}, // - {11, 9, 0, 10, 0, 9}, // + {0, 0, 0, 0, 0, 9}, // + {11, 9, 0, 10, 0, 42}, // {0, 0, 0, 0, 0, 19}, // {0, 0, 0, 0, 0, 19}, // - {14, 26, 53, 15, 0, 41}, // + {0, 0, 0, 0, 0, 41}, // + {0, 0, 0, 0, 0, 25}, // + {14, 26, 53, 15, 0, 38}, // {41, 63, 47, 41, 57, 34}, // {0, 0, 0, 0, 0, 29}, // {36, 33, 41, 34, 31, 57}, // @@ -500,7 +511,8 @@ const Translation_Table translation_data = { {43, 55, 61, 55, 43, 50}, // {0, 0, 0, 59, 0, 51}, // {0, 0, 0, 0, 0, 36}, // - {0, 0, 0, 33, 0, 58}, // + {0, 0, 0, 0, 0, 58}, // + {0, 0, 0, 33, 0, 32}, // {0, 0, 0, 44, 0, 48}, // {0, 0, 0, 43, 0, 42}, // {47, 55, 72, 36, 48, 35}, // @@ -1980,6 +1992,12 @@ const Translation_Table translation_data = { u8"const variable declared here\0" u8"containing 'declare namespace' starts here\0" u8"continue can only be used inside of a loop\0" + u8"decorator belongs immediately before this overloaded method\0" + u8"decorator starts here\0" + u8"decorators are not allowed inside TypeScript interfaces\0" + u8"decorators are not allowed on abstract properties\0" + u8"decorators must appear after overload signatures\0" + u8"decorators must appear before '{1}\0" u8"depth limit exceeded\0" u8"do-while loop is missing '{1}' around condition\0" u8"do-while loop needs parentheses around condition\0" @@ -2101,6 +2119,7 @@ const Translation_Table translation_data = { u8"missing body for {1:headlinese}\0" u8"missing catch or finally clause for try statement\0" u8"missing catch variable name between parentheses\0" + u8"missing class method or field after decorator\0" u8"missing comma between generic parameters\0" u8"missing comma between object literal entries\0" u8"missing comparison; '{1}' does not extend to the right side of '{0}'\0" @@ -2183,6 +2202,7 @@ const Translation_Table translation_data = { u8"parentheses are required around 'infer {1}'\0" u8"prior spread element is here\0" u8"private properties are not allowed in object literals\0" + u8"property declared 'abstract' here\0" u8"property declared static here\0" u8"property declared using '{0}' here\0" u8"redeclaration of global variable\0" @@ -2193,9 +2213,12 @@ const Translation_Table translation_data = { u8"remove this 'type'\0" u8"return statement returns nothing (undefined)\0" u8"see here\0" + u8"semicolon is not allowed after decorators\0" u8"something happened\0" u8"spread starts here\0" u8"spread tuple elements cannot be optional\0" + u8"static block starts here\0" + u8"static blocks cannot have a decorator\0" u8"stray comma in function parameter\0" u8"stray comma in let statement\0" u8"string module name is only allowed with 'declare module'\0" @@ -2268,6 +2291,7 @@ const Translation_Table translation_data = { u8"with statement needs parentheses around expression\0" u8"write 'const' instead of '{0}' here\0" u8"write '{1}' here or remove it from the overload signature\0" + u8"write the decorator before here\0" u8"write the type assertion with 'as' here instead\0" u8"{0} classes are not allowed in JavaScript\0" u8"{0} is not the name of a parameter\0" diff --git a/src/quick-lint-js/i18n/translation-table-generated.h b/src/quick-lint-js/i18n/translation-table-generated.h index 7836a022ba..a2964c4eba 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 = 497; -constexpr std::size_t translation_table_string_table_size = 78745; +constexpr std::uint16_t translation_table_mapping_table_size = 509; +constexpr std::size_t translation_table_string_table_size = 79234; constexpr std::size_t translation_table_locale_table_size = 35; QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( @@ -227,6 +227,12 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "const variable declared here"sv, "containing 'declare namespace' starts here"sv, "continue can only be used inside of a loop"sv, + "decorator belongs immediately before this overloaded method"sv, + "decorator starts here"sv, + "decorators are not allowed inside TypeScript interfaces"sv, + "decorators are not allowed on abstract properties"sv, + "decorators must appear after overload signatures"sv, + "decorators must appear before '{1}"sv, "depth limit exceeded"sv, "do-while loop is missing '{1}' around condition"sv, "do-while loop needs parentheses around condition"sv, @@ -348,6 +354,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "missing body for {1:headlinese}"sv, "missing catch or finally clause for try statement"sv, "missing catch variable name between parentheses"sv, + "missing class method or field after decorator"sv, "missing comma between generic parameters"sv, "missing comma between object literal entries"sv, "missing comparison; '{1}' does not extend to the right side of '{0}'"sv, @@ -430,6 +437,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "parentheses are required around 'infer {1}'"sv, "prior spread element is here"sv, "private properties are not allowed in object literals"sv, + "property declared 'abstract' here"sv, "property declared static here"sv, "property declared using '{0}' here"sv, "redeclaration of global variable"sv, @@ -440,9 +448,12 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "remove this 'type'"sv, "return statement returns nothing (undefined)"sv, "see here"sv, + "semicolon is not allowed after decorators"sv, "something happened"sv, "spread starts here"sv, "spread tuple elements cannot be optional"sv, + "static block starts here"sv, + "static blocks cannot have a decorator"sv, "stray comma in function parameter"sv, "stray comma in let statement"sv, "string module name is only allowed with 'declare module'"sv, @@ -515,6 +526,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "with statement needs parentheses around expression"sv, "write 'const' instead of '{0}' here"sv, "write '{1}' here or remove it from the overload signature"sv, + "write the decorator before here"sv, "write the type assertion with 'as' here instead"sv, "{0} classes are not allowed in JavaScript"sv, "{0} is not the name of a parameter"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 6456475abc..2a1f0773f7 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[496] = { +inline const Translated_String test_translation_table[508] = { { "\"global-groups\" entries must be strings"_translatable, u8"\"global-groups\" entries must be strings", @@ -2239,6 +2239,72 @@ inline const Translated_String test_translation_table[496] = { u8"continue kan enbart vara inuti en loop", }, }, + { + "decorator belongs immediately before this overloaded method"_translatable, + u8"decorator belongs immediately before this overloaded method", + { + u8"decorator belongs immediately before this overloaded method", + u8"decorator belongs immediately before this overloaded method", + u8"decorator belongs immediately before this overloaded method", + u8"decorator belongs immediately before this overloaded method", + u8"decorator belongs immediately before this overloaded method", + }, + }, + { + "decorator starts here"_translatable, + u8"decorator starts here", + { + u8"decorator starts here", + u8"decorator starts here", + u8"decorator starts here", + u8"decorator starts here", + u8"decorator starts here", + }, + }, + { + "decorators are not allowed inside TypeScript interfaces"_translatable, + u8"decorators are not allowed inside TypeScript interfaces", + { + u8"decorators are not allowed inside TypeScript interfaces", + u8"decorators are not allowed inside TypeScript interfaces", + u8"decorators are not allowed inside TypeScript interfaces", + u8"decorators are not allowed inside TypeScript interfaces", + u8"decorators are not allowed inside TypeScript interfaces", + }, + }, + { + "decorators are not allowed on abstract properties"_translatable, + u8"decorators are not allowed on abstract properties", + { + u8"decorators are not allowed on abstract properties", + u8"decorators are not allowed on abstract properties", + u8"decorators are not allowed on abstract properties", + u8"decorators are not allowed on abstract properties", + u8"decorators are not allowed on abstract properties", + }, + }, + { + "decorators must appear after overload signatures"_translatable, + u8"decorators must appear after overload signatures", + { + u8"decorators must appear after overload signatures", + u8"decorators must appear after overload signatures", + u8"decorators must appear after overload signatures", + u8"decorators must appear after overload signatures", + u8"decorators must appear after overload signatures", + }, + }, + { + "decorators must appear before '{1}"_translatable, + u8"decorators must appear before '{1}", + { + u8"decorators must appear before '{1}", + u8"decorators must appear before '{1}", + u8"decorators must appear before '{1}", + u8"decorators must appear before '{1}", + u8"decorators must appear before '{1}", + }, + }, { "depth limit exceeded"_translatable, u8"depth limit exceeded", @@ -3570,6 +3636,17 @@ inline const Translated_String test_translation_table[496] = { u8"saknar catch variabelnamn mellan paranteser", }, }, + { + "missing class method or field after decorator"_translatable, + u8"missing class method or field after decorator", + { + u8"missing class method or field after decorator", + u8"missing class method or field after decorator", + u8"missing class method or field after decorator", + u8"missing class method or field after decorator", + u8"missing class method or field after decorator", + }, + }, { "missing comma between generic parameters"_translatable, u8"missing comma between generic parameters", @@ -4472,6 +4549,17 @@ inline const Translated_String test_translation_table[496] = { u8"privata egenskaper \u00e4r inte till\u00e5tna i objektlitter\u00e4ler", }, }, + { + "property declared 'abstract' here"_translatable, + u8"property declared 'abstract' here", + { + u8"property declared 'abstract' here", + u8"property declared 'abstract' here", + u8"property declared 'abstract' here", + u8"property declared 'abstract' here", + u8"property declared 'abstract' here", + }, + }, { "property declared static here"_translatable, u8"property declared static here", @@ -4582,6 +4670,17 @@ inline const Translated_String test_translation_table[496] = { u8"see here", }, }, + { + "semicolon is not allowed after decorators"_translatable, + u8"semicolon is not allowed after decorators", + { + u8"semicolon is not allowed after decorators", + u8"semicolon is not allowed after decorators", + u8"semicolon is not allowed after decorators", + u8"semicolon is not allowed after decorators", + u8"semicolon is not allowed after decorators", + }, + }, { "something happened"_translatable, u8"something happened", @@ -4615,6 +4714,28 @@ inline const Translated_String test_translation_table[496] = { u8"spread tuple elements cannot be optional", }, }, + { + "static block starts here"_translatable, + u8"static block starts here", + { + u8"static block starts here", + u8"static block starts here", + u8"static block starts here", + u8"static block starts here", + u8"static block starts here", + }, + }, + { + "static blocks cannot have a decorator"_translatable, + u8"static blocks cannot have a decorator", + { + u8"static blocks cannot have a decorator", + u8"static blocks cannot have a decorator", + u8"static blocks cannot have a decorator", + u8"static blocks cannot have a decorator", + u8"static blocks cannot have a decorator", + }, + }, { "stray comma in function parameter"_translatable, u8"stray comma in function parameter", @@ -5407,6 +5528,17 @@ inline const Translated_String test_translation_table[496] = { u8"write '{1}' here or remove it from the overload signature", }, }, + { + "write the decorator before here"_translatable, + u8"write the decorator before here", + { + u8"write the decorator before here", + u8"write the decorator before here", + u8"write the decorator before here", + u8"write the decorator before here", + u8"write the decorator before here", + }, + }, { "write the type assertion with 'as' here instead"_translatable, u8"write the type assertion with 'as' here instead", diff --git a/test/test-parse-decorator.cpp b/test/test-parse-decorator.cpp index b1376af9e1..b954730680 100644 --- a/test/test-parse-decorator.cpp +++ b/test/test-parse-decorator.cpp @@ -184,6 +184,203 @@ TEST_F(Test_Parse_Decorator, EXPECT_THAT(p.variable_uses, ElementsAreArray({keyword})); } } + +TEST_F(Test_Parse_Decorator, class_methods_can_have_decorator) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator method() {} }"_sv, no_diags, + javascript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"decorator"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator static method() {} }"_sv, no_diags, + javascript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"decorator"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator1 @(decorator2) @decorator3(x) method() {} }"_sv, + no_diags, javascript_options); + EXPECT_THAT(p.variable_uses, + ElementsAreArray({u8"decorator1"_sv, u8"decorator2"_sv, + u8"decorator3"_sv, u8"x"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } +} + +TEST_F(Test_Parse_Decorator, class_fields_can_have_decorator) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator myField; }"_sv, no_diags, javascript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"decorator"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"myField"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator static myField; }"_sv, no_diags, + javascript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"decorator"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"myField"_sv})); + } +} + +TEST_F(Test_Parse_Decorator, class_accessors_can_have_decorator) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator accessor myProp; }"_sv, no_diags, + javascript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"decorator"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"myProp"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator static accessor myProp; }"_sv, no_diags, + javascript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"decorator"_sv})); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"myProp"_sv})); + } +} + +TEST_F(Test_Parse_Decorator, class_static_block_cannot_have_accessor) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { @decorator static { console.log('hi'); } }"_sv, + u8" ^^^^^^ Diag_Decorator_Not_Allowed_On_Class_Static_Block.static_keyword\n"_diag + u8" ^ .decorator_at"_diag, + javascript_options); + EXPECT_THAT(p.variable_uses, + ElementsAreArray({u8"decorator"_sv, u8"console"_sv})); + } +} + +TEST_F(Test_Parse_Decorator, + decorator_must_appear_before_class_member_modifiers) { + test_parse_and_visit_statement( + u8"class C { static @decorator foo() {} }"_sv, + u8" ^ Diag_Decorator_After_Class_Member_Modifiers.decorator_at\n"_diag + u8" ^^^^^^ .modifier"_diag, + javascript_options); + test_parse_and_visit_statement( + u8"class C { @decorator1 static @decorator2 foo() {} }"_sv, + u8" ^ Diag_Decorator_After_Class_Member_Modifiers.decorator_at\n"_diag + u8" ^^^^^^ .modifier"_diag, + javascript_options); + test_parse_and_visit_statement( + u8"class C { public static @decorator1 @decorator2 async foo() {} }"_sv, + u8" ^ Diag_Decorator_After_Class_Member_Modifiers.decorator_at\n"_diag + u8" ^^^^^^ .modifier"_diag, + u8" ^ Diag_Decorator_After_Class_Member_Modifiers.decorator_at\n"_diag + u8" ^^^^^^ .modifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { static @decorator foo: number = 42; }"_sv, + u8" ^ Diag_Decorator_After_Class_Member_Modifiers.decorator_at\n"_diag + u8" ^^^^^^ .modifier"_diag, + typescript_options); +} + +TEST_F(Test_Parse_Decorator, decorator_must_decorate_something_inside_class) { + test_parse_and_visit_statement( + u8"class C { @decorator }"_sv, + u8" ` Diag_Missing_Class_Member_After_Decorator.expected_member\n"_diag + u8" ^ .decorator_at"_diag, + javascript_options); + test_parse_and_visit_statement( + u8"class C { @decorator ; }"_sv, + u8" ` Diag_Missing_Class_Member_After_Decorator.expected_member\n"_diag + u8" ^ .decorator_at"_diag, + javascript_options); +} + +TEST_F(Test_Parse_Decorator, semicolon_is_not_allowed_after_decorator) { + test_parse_and_visit_statement( + u8"class C { @decorator; foo() {} }"_sv, + u8" ^ Diag_Unexpected_Semicolon_After_Decorator.semicolon\n"_diag + u8" ^ .decorator_at"_diag, + javascript_options); +} + +TEST_F(Test_Parse_Decorator, + decorators_are_not_allowed_in_typescript_interfaces) { + test_parse_and_visit_statement( + u8"interface I { @decorator foo(); }"_sv, + u8" ^ Diag_Decorator_In_TypeScript_Interface"_diag, + typescript_options); + + // Shouldn't also report Diag_Missing_Class_Member_After_Decorator. + test_parse_and_visit_statement( + u8"interface I { @decorator }"_sv, + u8" ^ Diag_Decorator_In_TypeScript_Interface"_diag, + typescript_options); + // Shouldn't also report Diag_Missing_Class_Member_After_Decorator. + test_parse_and_visit_statement( + u8"interface I { @decorator; }"_sv, + u8" ^ Diag_Decorator_In_TypeScript_Interface"_diag, + typescript_options); + // Shouldn't also report Diag_Unexpected_Semicolon_After_Decorator. + test_parse_and_visit_statement( + u8"interface I { @decorator; foo(); }"_sv, + u8" ^ Diag_Decorator_In_TypeScript_Interface"_diag, + typescript_options); + // Shouldn't also report Diag_Decorator_After_Class_Member_Modifiers. + test_parse_and_visit_statement( + u8"interface I { readonly @decorator field; }"_sv, + u8" ^ Diag_Decorator_In_TypeScript_Interface"_diag, + typescript_options); +} + +TEST_F(Test_Parse_Decorator, + decorators_are_not_allowed_on_typescript_abstract_methods) { + test_parse_and_visit_statement( + u8"abstract class C { @decorator abstract foo(); }"_sv, + u8" ^^^^^^^^ Diag_Decorator_On_Abstract_Class_Member.abstract_keyword\n"_diag + u8" ^ .decorator_at"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"abstract class C { @decorator abstract myField; }"_sv, + u8" ^^^^^^^^ Diag_Decorator_On_Abstract_Class_Member.abstract_keyword\n"_diag + u8" ^ .decorator_at"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"abstract class C { @decorator1 @decorator2 abstract foo(); }"_sv, + u8" ^^^^^^^^ Diag_Decorator_On_Abstract_Class_Member.abstract_keyword\n"_diag + u8" ^ .decorator_at"_diag, + u8" ^^^^^^^^ Diag_Decorator_On_Abstract_Class_Member.abstract_keyword\n"_diag + u8" ^ .decorator_at"_diag, + typescript_options); + // Shouldn't also report Diag_Decorator_After_Class_Member_Modifiers. + test_parse_and_visit_statement( + u8"abstract class C { abstract @decorator foo(); }"_sv, + u8" ^ Diag_Decorator_On_Abstract_Class_Member.decorator_at\n"_diag + u8" ^^^^^^^^ .abstract_keyword"_diag, + typescript_options); +} + +TEST_F(Test_Parse_Decorator, decorator_on_typescript_overloaded_method) { + test_parse_and_visit_statement(u8"class C { foo(); @decorator foo() {} }"_sv, + no_diags, typescript_options); +} + +TEST_F(Test_Parse_Decorator, + decorators_are_not_allowed_on_typescript_method_overload_signatures) { + test_parse_and_visit_statement( + u8"class C { @decorator foo(); foo() {} }"_sv, + u8" ` Diag_Decorator_On_Overload_Signature.expected_location\n"_diag + u8" ^ .decorator_at"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { @decorator foo(); async foo() {} }"_sv, + u8" ` Diag_Decorator_On_Overload_Signature.expected_location\n"_diag + u8" ^ .decorator_at"_diag, + typescript_options); +} } }