diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d8217a6f12..a44bcb1264 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,7 @@ Semantic Versioning. * quick-lint-js now compiles correctly on big-endian architectures such as S/390 (Linux s390x). * TypeScript support (still experimental): + * Class method overload signatures are now parsed. * [E0398][] is now reported when using both `abstract` and `static` on a single class property. * `,` is now allowed after interface fields. (Previously only `;` or a newline diff --git a/po/messages.pot b/po/messages.pot index 8b4a6c74f4..11693a479a 100644 --- a/po/messages.pot +++ b/po/messages.pot @@ -417,10 +417,30 @@ msgstr "" msgid "'{0}' is not allowed with '{1}'" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "'{0}' is missing on overloaded method" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "write '{1}' here or remove it from the overload signature" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "'{1}' is missing on overload signature" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "overload signature must match modifiers on this overload method" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "'{0}' must precede '{1}'" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "'{0}' is not allowed in TypeScript overload signatures" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "missing body for {1:headlinese}" msgstr "" @@ -669,6 +689,10 @@ msgstr "" msgid "generator function '*' belongs before function name" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "getters and setters cannot have overload signatures" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "only one comma is allowed between or after generic parameters" msgstr "" @@ -1001,6 +1025,10 @@ msgstr "" msgid "missing semicolon after statement" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "missing semicolon after method overload signature" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "missing semicolon after field" msgstr "" @@ -1065,6 +1093,10 @@ msgstr "" msgid "'function' is here" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "newline is not allowed between '{0}' and the method name" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "newline is not allowed after 'abstract'" msgstr "" @@ -1485,6 +1517,14 @@ msgstr "" msgid "prior spread element is here" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "overload signature must have the correct access specifier ('{1}')" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "overloaded method is marked '{0}'" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "parameter properties cannot be destructured" msgstr "" @@ -1693,6 +1733,14 @@ msgstr "" msgid "unexpected '?' when destructuring" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "TypeScript overload signature can only have one semicolon" +msgstr "" + +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "original semicolon is here" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "C-style for loops have only three semicolon-separated components" msgstr "" diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp index eeea146b1c..a14a211c16 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp @@ -1016,6 +1016,44 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature + { + .code = 403, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("'{0}' is missing on overloaded method"), + QLJS_TRANSLATABLE("write '{1}' here or remove it from the overload signature"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature, signature_modifier), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature, missing_method_modifier), Diagnostic_Arg_Type::source_code_span), + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature, signature_modifier), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + + // Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature + { + .code = 404, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("'{1}' is missing on overload signature"), + QLJS_TRANSLATABLE("overload signature must match modifiers on this overload method"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature, missing_signature_modifier), Diagnostic_Arg_Type::source_code_span), + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature, method_modifier), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature, method_modifier), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Class_Modifier_Must_Preceed_Other_Modifier { .code = 395, @@ -1031,6 +1069,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Class_Modifier_Not_Allowed_On_TypeScript_Overload_Signature + { + .code = 402, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("'{0}' is not allowed in TypeScript overload signatures"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Class_Modifier_Not_Allowed_On_TypeScript_Overload_Signature, modifier), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Class_Statement_Not_Allowed_In_Body { .code = 149, @@ -1857,6 +1909,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature + { + .code = 401, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("getters and setters cannot have overload signatures"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature, get_or_set_token), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Multiple_Commas_In_Generic_Parameter_List { .code = 263, @@ -2966,6 +3032,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature + { + .code = 406, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("missing semicolon after method overload signature"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature, expected_semicolon), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Missing_Semicolon_After_Field { .code = 223, @@ -3161,6 +3241,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name + { + .code = 399, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("newline is not allowed between '{0}' and the method name"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name, modifier), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Newline_Not_Allowed_After_Abstract_Keyword { .code = 300, @@ -4452,6 +4546,25 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch + { + .code = 405, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("overload signature must have the correct access specifier ('{1}')"), + QLJS_TRANSLATABLE("overloaded method is marked '{0}'"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch, signature_access_specifier), Diagnostic_Arg_Type::source_code_span), + Diagnostic_Message_Arg_Info(offsetof(Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch, method_access_specifier), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch, method_access_specifier), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_TypeScript_Parameter_Property_Cannot_Be_Destructured { .code = 372, @@ -5142,6 +5255,24 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Unexpected_Semicolon_After_Overload_Signature + { + .code = 400, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("TypeScript overload signature can only have one semicolon"), + QLJS_TRANSLATABLE("original semicolon is here"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Unexpected_Semicolon_After_Overload_Signature, extra_semicolon), Diagnostic_Arg_Type::source_code_span), + }, + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Unexpected_Semicolon_After_Overload_Signature, original_semicolon), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Unexpected_Semicolon_In_C_Style_For_Loop { .code = 102, diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.h b/src/quick-lint-js/diag/diagnostic-metadata-generated.h index c200fcb064..89140d943d 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.h +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.h @@ -77,7 +77,10 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Class_Accessor_On_Getter_Or_Setter) \ QLJS_DIAG_TYPE_NAME(Diag_Class_Accessor_On_Method) \ QLJS_DIAG_TYPE_NAME(Diag_Class_Conflicting_Modifiers) \ + QLJS_DIAG_TYPE_NAME(Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature) \ + QLJS_DIAG_TYPE_NAME(Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Class_Modifier_Must_Preceed_Other_Modifier) \ + QLJS_DIAG_TYPE_NAME(Diag_Class_Modifier_Not_Allowed_On_TypeScript_Overload_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Class_Statement_Not_Allowed_In_Body) \ QLJS_DIAG_TYPE_NAME(Diag_Character_Disallowed_In_Identifiers) \ QLJS_DIAG_TYPE_NAME(Diag_Comma_Not_Allowed_After_Spread_Parameter) \ @@ -134,6 +137,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Function_Statement_Not_Allowed_In_Body) \ QLJS_DIAG_TYPE_NAME(Diag_Generator_Function_Star_Belongs_After_Keyword_Function) \ QLJS_DIAG_TYPE_NAME(Diag_Generator_Function_Star_Belongs_Before_Name) \ + QLJS_DIAG_TYPE_NAME(Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Multiple_Commas_In_Generic_Parameter_List) \ QLJS_DIAG_TYPE_NAME(Diag_In_Disallowed_In_C_Style_For_Loop) \ QLJS_DIAG_TYPE_NAME(Diag_Indexing_Requires_Expression) \ @@ -210,6 +214,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_Abstract_Method) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_Declare_Class_Method) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_Statement) \ + QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_Field) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_Index_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Missing_Semicolon_After_Interface_Method) \ @@ -223,6 +228,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Missing_While_And_Condition_For_Do_While_Statement) \ QLJS_DIAG_TYPE_NAME(Diag_Newline_Not_Allowed_Between_Async_And_Parameter_List) \ QLJS_DIAG_TYPE_NAME(Diag_Newline_Not_Allowed_Between_Async_And_Function_Keyword) \ + QLJS_DIAG_TYPE_NAME(Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name) \ QLJS_DIAG_TYPE_NAME(Diag_Newline_Not_Allowed_After_Abstract_Keyword) \ QLJS_DIAG_TYPE_NAME(Diag_Newline_Not_Allowed_After_Export_Declare) \ QLJS_DIAG_TYPE_NAME(Diag_Newline_Not_Allowed_After_Interface_Keyword) \ @@ -308,6 +314,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Optional_Parameters_Not_Allowed_In_JavaScript) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Optional_Properties_Not_Allowed_In_JavaScript) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Optional_Tuple_Element_Cannot_Follow_Spread_Element) \ + QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Parameter_Property_Cannot_Be_Destructured) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Parameter_Property_Cannot_Be_Rest) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Parameter_Property_Not_Allowed_In_Declare_Class) \ @@ -355,6 +362,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_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) \ QLJS_DIAG_TYPE_NAME(Diag_Unexpected_Semicolon_In_For_Of_Loop) \ @@ -415,7 +423,7 @@ namespace quick_lint_js { /* END */ // clang-format on -inline constexpr int Diag_Type_Count = 401; +inline constexpr int Diag_Type_Count = 409; 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 4c164e674a..08987ea0d5 100644 --- a/src/quick-lint-js/diag/diagnostic-types-2.h +++ b/src/quick-lint-js/diag/diagnostic-types-2.h @@ -546,6 +546,28 @@ struct Diag_Class_Conflicting_Modifiers { Source_Code_Span first_modifier; }; +struct + Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature { + [[qljs::diag("E0403", Diagnostic_Severity::error)]] // + [[qljs::message("'{0}' is missing on overloaded method", + ARG(signature_modifier))]] // + [[qljs::message("write '{1}' here or remove it from the overload signature", + ARG(missing_method_modifier), ARG(signature_modifier))]] // + Source_Code_Span signature_modifier; + Source_Code_Span missing_method_modifier; +}; + +struct Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature { + [[qljs::diag("E0404", Diagnostic_Severity::error)]] // + [[qljs::message("'{1}' is missing on overload signature", + ARG(missing_signature_modifier), ARG(method_modifier))]] // + [[qljs::message( + "overload signature must match modifiers on this overload method", + ARG(method_modifier))]] // + Source_Code_Span missing_signature_modifier; + Source_Code_Span method_modifier; +}; + struct Diag_Class_Modifier_Must_Preceed_Other_Modifier { [[qljs::diag("E0395", Diagnostic_Severity::error)]] // [[qljs::message("'{0}' must precede '{1}'", ARG(expected_first_modifier), @@ -554,6 +576,13 @@ struct Diag_Class_Modifier_Must_Preceed_Other_Modifier { Source_Code_Span expected_second_modifier; }; +struct Diag_Class_Modifier_Not_Allowed_On_TypeScript_Overload_Signature { + [[qljs::diag("E0402", Diagnostic_Severity::error)]] // + [[qljs::message("'{0}' is not allowed in TypeScript overload signatures", + ARG(modifier))]] // + Source_Code_Span modifier; +}; + struct Diag_Class_Statement_Not_Allowed_In_Body { [[qljs::diag("E0149", Diagnostic_Severity::error)]] // [[qljs::message("missing body for {1:headlinese}", ARG(expected_body), @@ -967,6 +996,13 @@ struct Diag_Generator_Function_Star_Belongs_Before_Name { Source_Code_Span star; }; +struct Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature { + [[qljs::diag("E0401", Diagnostic_Severity::error)]] // + [[qljs::message("getters and setters cannot have overload signatures", + ARG(get_or_set_token))]] // + Source_Code_Span get_or_set_token; +}; + struct Diag_Multiple_Commas_In_Generic_Parameter_List { [[qljs::diag("E0263", Diagnostic_Severity::error)]] // [[qljs::message( @@ -1513,6 +1549,13 @@ struct Diag_Missing_Semicolon_After_Statement { Source_Code_Span where; }; +struct Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature { + [[qljs::diag("E0406", Diagnostic_Severity::error)]] // + [[qljs::message("missing semicolon after method overload signature", + ARG(expected_semicolon))]] // + Source_Code_Span expected_semicolon; +}; + struct Diag_Missing_Semicolon_After_Field { [[qljs::diag("E0223", Diagnostic_Severity::error)]] // [[qljs::message("missing semicolon after field", @@ -1617,6 +1660,13 @@ struct Diag_Newline_Not_Allowed_Between_Async_And_Function_Keyword { Source_Code_Span function_keyword; }; +struct Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name { + [[qljs::diag("E0399", Diagnostic_Severity::error)]] // + [[qljs::message("newline is not allowed between '{0}' and the method name", + ARG(modifier))]] // + Source_Code_Span modifier; +}; + struct Diag_Newline_Not_Allowed_After_Abstract_Keyword { [[qljs::diag("E0300", Diagnostic_Severity::error)]] // [[qljs::message("newline is not allowed after 'abstract'", @@ -2300,6 +2350,17 @@ struct Diag_TypeScript_Optional_Tuple_Element_Cannot_Follow_Spread_Element { Source_Code_Span previous_spread; }; +struct Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch { + [[qljs::diag("E0405", Diagnostic_Severity::error)]] // + [[qljs::message( + "overload signature must have the correct access specifier ('{1}')", + ARG(signature_access_specifier), ARG(method_access_specifier))]] // + [[qljs::message("overloaded method is marked '{0}'", + ARG(method_access_specifier))]] // + Source_Code_Span method_access_specifier; + Source_Code_Span signature_access_specifier; +}; + struct Diag_TypeScript_Parameter_Property_Cannot_Be_Destructured { [[qljs::diag("E0372", Diagnostic_Severity::error)]] // [[qljs::message("parameter properties cannot be destructured", @@ -2650,6 +2711,16 @@ struct Diag_Unexpected_Question_When_Destructuring { Source_Code_Span question; }; +struct Diag_Unexpected_Semicolon_After_Overload_Signature { + [[qljs::diag("E0400", Diagnostic_Severity::error)]] // + [[qljs::message("TypeScript overload signature can only have one semicolon", + ARG(extra_semicolon))]] // + [[qljs::message("original semicolon is here", + ARG(original_semicolon))]] // + Source_Code_Span extra_semicolon; + Source_Code_Span original_semicolon; +}; + struct Diag_Unexpected_Semicolon_In_C_Style_For_Loop { [[qljs::diag("E0102", Diagnostic_Severity::error)]] // [[qljs::message( diff --git a/src/quick-lint-js/fe/parse-class.cpp b/src/quick-lint-js/fe/parse-class.cpp index a267dec537..c82760fdca 100644 --- a/src/quick-lint-js/fe/parse-class.cpp +++ b/src/quick-lint-js/fe/parse-class.cpp @@ -295,6 +295,39 @@ void Parser::parse_and_visit_class_or_interface_member( Bump_Vector("class member modifiers", &p->temporary_memory_); + // Example: + // + // class C { + // f(x: number); // TypeScript_Overload_Signature + // f(x: string); // TypeScript_Overload_Signature + // f(x: number|string) { + // } + // } + struct TypeScript_Overload_Signature { + Span modifiers; + + // If empty, this overload signature ended with a newline. + Span semicolons; + + // Where a function body would appear if this was not an overload + // signature. + const Char8 *expected_body; + + // If nullopt, the property is a computed property. + // + // See TODO[TypeScript-overload-signature-with-computed-property]. + std::optional name; + + Source_Code_Span name_span; + }; + Bump_Vector + overload_signatures{"class overload signatures", &p->temporary_memory_}; + + void reset_state_except_overload_signatures() { + this->last_ident = std::nullopt; + this->modifiers.clear(); + } + // Returns true if an entire property was parsed. // Returns false if nothing was parsed or if only modifiers were parsed. bool parse_modifiers() { @@ -323,23 +356,36 @@ void Parser::parse_and_visit_class_or_interface_member( }); p->skip(); if (p->peek().has_leading_newline) { - switch (p->peek().type) { - // 'async' is a field name: - // class { - // async - // method() {} - // } - QLJS_CASE_KEYWORD: - case Token_Type::left_square: - case Token_Type::number: - case Token_Type::string: - case Token_Type::identifier: - case Token_Type::private_identifier: - case Token_Type::star: - check_modifiers_for_field_without_type_annotation(); - v.visit_property_declaration(last_ident); - return true; - default: + if (this->overload_signatures.empty()) { + switch (p->peek().type) { + // 'async' is a field name: + // class { + // async + // method() {} + // } + QLJS_CASE_KEYWORD: + case Token_Type::left_square: + case Token_Type::number: + case Token_Type::string: + case Token_Type::identifier: + case Token_Type::private_identifier: + case Token_Type::star: + check_modifiers_for_field_without_type_annotation(); + v.visit_property_declaration(last_ident); + return true; + default: + continue; + } + } else { + // class C { + // f(); // TypeScript only. + // async // Invalid. + // f() {} + // } + p->diag_reporter_->report( + Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name{ + .modifier = last_ident->span(), + }); continue; } } else { @@ -384,6 +430,9 @@ void Parser::parse_and_visit_class_or_interface_member( } void parse_class_member() { + // Reset state in case we come here after parsing an overload signature. + this->reset_state_except_overload_signatures(); + if (bool done = parse_modifiers(); done) { return; } @@ -491,7 +540,10 @@ void Parser::parse_and_visit_class_or_interface_member( case Token_Type::equal: case Token_Type::question: case Token_Type::right_curly: - if (last_ident.has_value()) { + if (!this->overload_signatures.empty()) { + // class C { method(); } // Invalid. + this->error_on_overload_signatures(); + } else if (last_ident.has_value()) { modifiers.pop_back(); parse_and_visit_field_or_method(*last_ident); } else { @@ -764,8 +816,67 @@ void Parser::parse_and_visit_class_or_interface_member( v, property_name_span, attributes, param_options); v.visit_exit_function_scope(); } else { - p->parse_and_visit_function_parameters_and_body( - v, /*name=*/property_name_span, attributes, param_options); + // class C { myMethod() {} } + bool is_possibly_typescript_overload = false; + + v.visit_enter_function_scope(); + + Function_Guard guard = p->enter_function(attributes); + Function_Parameter_Parse_Result result = + p->parse_and_visit_function_parameter_list(v, property_name_span, + param_options); + switch (result) { + case Function_Parameter_Parse_Result::parsed_parameters: + case Function_Parameter_Parse_Result::missing_parameters: + v.visit_enter_function_scope_body(); + p->parse_and_visit_statement_block_no_scope(v); + property_name = this->check_overload_signatures(property_name, + property_name_span); + break; + + case Function_Parameter_Parse_Result::missing_parameters_ignore_body: + break; + + case Function_Parameter_Parse_Result::parsed_parameters_missing_body: + if (p->options_.typescript) { + // class C { myMethod(x: number); myMethod(x: any) {} } + // + // This looks like an overload signature. We remember it in + // this->overload_signatures then parse the next member. + is_possibly_typescript_overload = true; + const Char8 *expected_body = p->lexer_.end_of_previous_token(); + Bump_Vector semicolons( + "semicolons", &p->temporary_memory_); + while (p->peek().type == Token_Type::semicolon) { + semicolons.push_back(p->peek().span()); + p->skip(); + } + if (semicolons.empty()) { + // class C { f() f() {} } // Invalid. + p->consume_semicolon< + Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature>(); + } + this->overload_signatures.push_back(TypeScript_Overload_Signature{ + .modifiers = this->modifiers.release_to_span(), + .semicolons = semicolons.release_to_span(), + .expected_body = expected_body, + .name = property_name, + .name_span = property_name_span, + }); + } else { + p->diag_reporter_->report(Diag_Missing_Function_Body{ + .expected_body = Source_Code_Span::unit( + p->lexer_.end_of_previous_token())}); + } + break; + } + + v.visit_exit_function_scope(); + + if (is_possibly_typescript_overload) { + this->parse_class_member(); + break; // Skip v.visit_property_declaration(property_name). + } } v.visit_property_declaration(property_name); break; @@ -977,6 +1088,7 @@ void Parser::parse_and_visit_class_or_interface_member( error_if_optional_in_not_typescript(); error_if_optional_accessor(); error_if_abstract_not_in_abstract_class(); + error_on_overload_signatures(); } void check_modifiers_for_method() { @@ -1406,7 +1518,222 @@ void Parser::parse_and_visit_class_or_interface_member( } } + // If a better property name is discovered, the better name is returned. + // Otherwise, property_name is returned. + [[nodiscard]] std::optional check_overload_signatures( + std::optional property_name, + Source_Code_Span property_name_span) { + if (this->overload_signatures.empty()) { + return property_name; + } + + // Given the following example, set property_name to 'a'. + // + // class C { a(); "b"() {} } + // + // This shouldn't be necessary. See + // TODO[TypeScript-overload-signature-with-computed-property]. + if (!property_name.has_value()) { + for (TypeScript_Overload_Signature &signature : + this->overload_signatures) { + if (signature.name.has_value()) { + property_name = signature.name; + break; + } + } + } + + const Modifier *method_get_or_set_modifier = nullptr; + if (method_get_or_set_modifier == nullptr) { + method_get_or_set_modifier = this->find_modifier(Token_Type::kw_get); + } + if (method_get_or_set_modifier == nullptr) { + method_get_or_set_modifier = this->find_modifier(Token_Type::kw_set); + } + + const Modifier *method_question_modifier = + this->find_modifier(Token_Type::question); + const Modifier *method_static_modifier = + this->find_modifier(Token_Type::kw_static); + const Modifier *method_access_specifier = this->find_access_specifier(); + + bool have_likely_overload_signature = false; + for (TypeScript_Overload_Signature &signature : + this->overload_signatures) { + if (const Modifier *star_modifier = + this->find_modifier(Token_Type::star, signature.modifiers)) { + p->diag_reporter_->report( + Diag_TypeScript_Function_Overload_Signature_Must_Not_Have_Generator_Star{ + .generator_star = star_modifier->span, + }); + } + + const Modifier *signature_get_or_set_modifier = nullptr; + if (signature_get_or_set_modifier == nullptr) { + signature_get_or_set_modifier = + this->find_modifier(Token_Type::kw_get, signature.modifiers); + } + if (signature_get_or_set_modifier == nullptr) { + signature_get_or_set_modifier = + this->find_modifier(Token_Type::kw_set, signature.modifiers); + } + + if (signature.name.has_value() && property_name.has_value() && + signature.name->normalized_name() != + property_name->normalized_name()) { + if (signature.semicolons.empty() || + signature_get_or_set_modifier != nullptr) { + // class C { + // f() // Invalid; missing body. + // g() {} + // } + // + // class C { + // get f(); // Invalid; missing body + // get g() {} + // } + p->diag_reporter_->report(Diag_Missing_Function_Body{ + .expected_body = + Source_Code_Span::unit(signature.expected_body), + }); + v.visit_property_declaration(*signature.name); + } else { + // class C { + // f(); // Invalid; mismatched names. + // g() {} + // } + have_likely_overload_signature = true; + p->diag_reporter_->report( + Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name{ + .overload_name = signature.name->span(), + .function_name = property_name->span(), + }); + } + } else { + have_likely_overload_signature = true; + + if (signature_get_or_set_modifier != nullptr && + method_get_or_set_modifier == nullptr) { + // class C { get m(); m() {} } // Invalid. + p->diag_reporter_->report( + Diag_Class_Modifier_Not_Allowed_On_TypeScript_Overload_Signature{ + .modifier = signature_get_or_set_modifier->span, + }); + } + + const Modifier *signature_question_modifier = + this->find_modifier(Token_Type::question, signature.modifiers); + this->error_if_signature_and_method_have_mismatched_modifier_presence( + signature_question_modifier, signature.name_span.end(), + method_question_modifier, property_name_span.end()); + + const Modifier *signature_static_modifier = + this->find_modifier(Token_Type::kw_static, signature.modifiers); + this->error_if_signature_and_method_have_mismatched_modifier_presence( + signature_static_modifier, signature.name_span.begin(), + method_static_modifier, property_name_span.begin()); + + const Modifier *signature_access_specifier = + this->find_access_specifier(signature.modifiers); + if (is_explicitly_or_implicitly_public(method_access_specifier) && + is_explicitly_or_implicitly_public(signature_access_specifier)) { + // f(); public f() {} // OK. + // public f(); f() {} // OK. + // Emit no diagnostic. + } else { + this->error_if_signature_and_method_have_mismatched_modifier_presence( + signature_access_specifier, signature.name_span.begin(), + method_access_specifier, property_name_span.begin()); + } + + if (method_access_specifier != nullptr && + signature_access_specifier != nullptr) { + if (method_access_specifier->type != + signature_access_specifier->type) { + // class C { public f(); private f() {} } // Invalid. + p->diag_reporter_->report( + Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch{ + .method_access_specifier = method_access_specifier->span, + .signature_access_specifier = + signature_access_specifier->span, + }); + } + } + + for (Span_Size i = 1; i < signature.semicolons.size(); ++i) { + // class C { m();; m() {} } // Invalid. + p->diag_reporter_->report( + Diag_Unexpected_Semicolon_After_Overload_Signature{ + .extra_semicolon = signature.semicolons[i], + .original_semicolon = signature.semicolons[0], + }); + } + } + } + + if (have_likely_overload_signature) { + if (method_get_or_set_modifier != nullptr) { + // class C { m(); get m() {} } // Invalid. + p->diag_reporter_->report( + Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature{ + .get_or_set_token = method_get_or_set_modifier->span, + }); + } + } + + return property_name; + } + + void error_if_signature_and_method_have_mismatched_modifier_presence( + const Modifier *signature_modifier, + const Char8 *expected_signature_modifier_location, + const Modifier *method_modifier, + const Char8 *expected_method_modifier_location) { + if ((signature_modifier != nullptr) && (method_modifier == nullptr)) { + // class C { m?(); m() {} } // Invalid. + p->diag_reporter_->report( + Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature{ + .signature_modifier = signature_modifier->span, + .missing_method_modifier = + Source_Code_Span::unit(expected_method_modifier_location), + }); + } + if ((signature_modifier == nullptr) && (method_modifier != nullptr)) { + // class C { m(); m?() {} } // Invalid. + p->diag_reporter_->report( + Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature{ + .missing_signature_modifier = Source_Code_Span::unit( + expected_signature_modifier_location), + .method_modifier = method_modifier->span, + }); + } + } + + static bool is_explicitly_or_implicitly_public( + const Modifier *access_specifier) { + return access_specifier == nullptr || + access_specifier->type == Token_Type::kw_public; + } + + void error_on_overload_signatures() { + for (TypeScript_Overload_Signature &signature : + this->overload_signatures) { + if (signature.name.has_value()) { + v.visit_property_declaration(*signature.name); + } + p->diag_reporter_->report(Diag_Missing_Function_Body{ + .expected_body = Source_Code_Span::unit(signature.expected_body), + }); + } + } + const Modifier *find_modifier(Token_Type modifier_type) const { + return this->find_modifier(modifier_type, + Span(this->modifiers)); + } + + static const Modifier *find_modifier(Token_Type modifier_type, + Span modifiers) { for (const Modifier &m : modifiers) { if (m.type == modifier_type) { return &m; @@ -1416,6 +1743,11 @@ void Parser::parse_and_visit_class_or_interface_member( } const Modifier *find_access_specifier() const { + return this->find_access_specifier(Span(this->modifiers)); + } + + static const Modifier *find_access_specifier( + Span modifiers) { for (const Modifier &m : modifiers) { if (m.is_access_specifier()) { return &m; diff --git a/src/quick-lint-js/fe/parse.cpp b/src/quick-lint-js/fe/parse.cpp index 0a7483a57f..d9715390aa 100644 --- a/src/quick-lint-js/fe/parse.cpp +++ b/src/quick-lint-js/fe/parse.cpp @@ -840,6 +840,8 @@ template void Parser::consume_semicolon(); template void Parser::consume_semicolon(); +template void Parser::consume_semicolon< + Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature>(); template void Parser::consume_semicolon(); diff --git a/src/quick-lint-js/fe/parse.h b/src/quick-lint-js/fe/parse.h index 64ba901c99..6860cd2839 100644 --- a/src/quick-lint-js/fe/parse.h +++ b/src/quick-lint-js/fe/parse.h @@ -1194,6 +1194,8 @@ extern template void Parser::consume_semicolon(); extern template void Parser::consume_semicolon(); +extern template void Parser::consume_semicolon< + Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature>(); } // quick-lint-js finds bugs in JavaScript programs. diff --git a/src/quick-lint-js/i18n/translation-table-generated.cpp b/src/quick-lint-js/i18n/translation-table-generated.cpp index ee93179ee5..5ce529d2be 100644 --- a/src/quick-lint-js/i18n/translation-table-generated.cpp +++ b/src/quick-lint-js/i18n/translation-table-generated.cpp @@ -88,12 +88,15 @@ const Translation_Table translation_data = { {0, 0, 0, 0, 0, 25}, // {15, 38, 0, 25, 0, 42}, // {0, 0, 0, 0, 0, 17}, // - {15, 17, 0, 22, 0, 11}, // - {0, 0, 0, 46, 0, 50}, // + {0, 0, 0, 0, 0, 11}, // + {15, 17, 0, 22, 0, 38}, // + {0, 0, 0, 0, 0, 50}, // + {0, 0, 0, 46, 0, 55}, // {0, 0, 0, 0, 0, 32}, // {0, 0, 0, 0, 0, 32}, // {69, 48, 0, 36, 0, 25}, // {0, 0, 0, 0, 0, 62}, // + {0, 0, 0, 0, 0, 39}, // {68, 25, 0, 65, 0, 28}, // {0, 0, 0, 0, 0, 62}, // {83, 55, 66, 62, 49, 57}, // @@ -131,6 +134,7 @@ const Translation_Table translation_data = { {0, 0, 0, 80, 0, 72}, // {0, 0, 0, 71, 0, 61}, // {0, 0, 0, 0, 0, 66}, // + {0, 0, 0, 0, 0, 58}, // {0, 0, 0, 0, 0, 62}, // {0, 0, 0, 80, 0, 56}, // {0, 0, 0, 58, 0, 48}, // @@ -266,7 +270,8 @@ const Translation_Table translation_data = { {44, 18, 49, 39, 0, 39}, // {84, 28, 66, 75, 25, 54}, // {0, 0, 0, 70, 0, 52}, // - {70, 27, 0, 57, 0, 45}, // + {0, 0, 0, 0, 0, 45}, // + {70, 27, 0, 57, 0, 52}, // {0, 0, 0, 5, 0, 5}, // {5, 11, 66, 52, 54, 42}, // {41, 33, 48, 38, 36, 33}, // @@ -363,7 +368,8 @@ const Translation_Table translation_data = { {0, 0, 0, 50, 0, 40}, // {0, 0, 0, 39, 0, 30}, // {0, 0, 0, 44, 0, 40}, // - {44, 4, 46, 54, 42, 41}, // + {0, 0, 0, 0, 0, 41}, // + {44, 4, 46, 54, 42, 50}, // {31, 50, 44, 45, 35, 34}, // {73, 55, 78, 81, 66, 65}, // {0, 0, 0, 83, 0, 63}, // @@ -381,7 +387,8 @@ const Translation_Table translation_data = { {0, 0, 0, 0, 0, 36}, // {0, 0, 0, 47, 0, 35}, // {52, 31, 25, 61, 21, 54}, // - {0, 0, 0, 89, 0, 73}, // + {0, 0, 0, 0, 0, 73}, // + {0, 0, 0, 89, 0, 57}, // {0, 0, 0, 0, 0, 50}, // {63, 34, 103, 64, 73, 73}, // {69, 23, 79, 41, 51, 48}, // @@ -395,8 +402,12 @@ const Translation_Table translation_data = { {0, 0, 0, 0, 0, 25}, // {0, 0, 0, 29, 0, 62}, // {0, 0, 0, 0, 0, 68}, // - {0, 0, 0, 68, 0, 58}, // + {0, 0, 0, 0, 0, 58}, // + {0, 0, 0, 0, 0, 27}, // + {0, 0, 0, 0, 0, 66}, // + {0, 0, 0, 68, 0, 64}, // {0, 0, 0, 0, 0, 40}, // + {0, 0, 0, 0, 0, 34}, // {0, 0, 0, 0, 0, 56}, // {0, 0, 0, 0, 0, 60}, // {0, 0, 0, 0, 0, 48}, // @@ -487,7 +498,8 @@ const Translation_Table translation_data = { {48, 44, 68, 53, 45, 46}, // {43, 55, 61, 55, 43, 50}, // {0, 0, 0, 59, 0, 51}, // - {0, 0, 0, 33, 0, 36}, // + {0, 0, 0, 0, 0, 36}, // + {0, 0, 0, 33, 0, 58}, // {0, 0, 0, 44, 0, 48}, // {0, 0, 0, 43, 0, 42}, // {47, 55, 72, 36, 48, 35}, // @@ -1843,11 +1855,14 @@ const Translation_Table translation_data = { u8"'{0}' access specifier must precede '{1}'\0" u8"'{0}' found here\0" u8"'{0}' here\0" + u8"'{0}' is missing on overloaded method\0" u8"'{0}' is not allowed for strings; use {1} instead\0" + u8"'{0}' is not allowed in TypeScript overload signatures\0" u8"'{0}' is not allowed on methods\0" u8"'{0}' is not allowed with '{1}'\0" u8"'{0}' must precede '{1}'\0" u8"'{0}' operator cannot be used before '**' without parentheses\0" + u8"'{1}' is missing on overload signature\0" u8"'{1}' statement starts here\0" u8"'}' is not allowed directly in JSX text; write {{'}'} instead\0" u8".d.ts files cannot contain statements, only declarations\0" @@ -1885,6 +1900,7 @@ const Translation_Table translation_data = { u8"TypeScript optional parameter with type annotation requires parentheses\0" u8"TypeScript optional parameters are not allowed in JavaScript\0" u8"TypeScript optional properties are not allowed in JavaScript code\0" + u8"TypeScript overload signature can only have one semicolon\0" u8"TypeScript parameter properties are not allowed in JavaScript\0" u8"TypeScript requires whitespace between '>' and '=' here\0" u8"TypeScript type annotation requires parentheses\0" @@ -2021,6 +2037,7 @@ const Translation_Table translation_data = { u8"generator function '*' belongs after keyword function\0" u8"generator function '*' belongs before function name\0" u8"generic arrow function needs ',' here in TSX\0" + u8"getters and setters cannot have overload signatures\0" u8"here\0" u8"here is the assignment assertion operator\0" u8"hex number literal has no digits\0" @@ -2118,6 +2135,7 @@ const Translation_Table translation_data = { u8"missing semicolon after field\0" u8"missing semicolon after index signature\0" u8"missing semicolon after interface method\0" + u8"missing semicolon after method overload signature\0" u8"missing semicolon after statement\0" u8"missing semicolon between condition and update parts of for loop\0" u8"missing semicolon between init and condition parts of for loop\0" @@ -2136,6 +2154,7 @@ const Translation_Table translation_data = { u8"newline is not allowed after '{0}'\0" u8"newline is not allowed between 'async' and 'function'\0" u8"newline is not allowed between 'async' and arrow function parameter list\0" + u8"newline is not allowed between '{0}' and the method name\0" u8"newline is not allowed between field name and '!'\0" u8"nullish coalescing operator does nothing when left operand is never null\0" u8"number literal contains consecutive underscores\0" @@ -2150,7 +2169,11 @@ const Translation_Table translation_data = { u8"optional parameter cannot be followed by a required parameter\0" u8"optional parameter cannot have both '?' and initializer; remove '?'\0" u8"optional tuple elements cannot come after spread elements\0" + u8"original semicolon is here\0" + u8"overload signature must have the correct access specifier ('{1}')\0" + u8"overload signature must match modifiers on this overload method\0" u8"overloaded function '{0}' declared here\0" + u8"overloaded method is marked '{0}'\0" u8"parameter properties are not allowed in 'declare class'\0" u8"parameter properties are only allowed in class constructors\0" u8"parameter properties cannot be a rest parameter\0" @@ -2242,6 +2265,7 @@ const Translation_Table translation_data = { u8"with statement is missing '{1}' around expression\0" 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 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 19bfaef4b1..2e469b2028 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 = 484; -constexpr std::size_t translation_table_string_table_size = 78112; +constexpr std::uint16_t translation_table_mapping_table_size = 496; +constexpr std::size_t translation_table_string_table_size = 78710; constexpr std::size_t translation_table_locale_table_size = 35; QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( @@ -103,11 +103,14 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "'{0}' access specifier must precede '{1}'"sv, "'{0}' found here"sv, "'{0}' here"sv, + "'{0}' is missing on overloaded method"sv, "'{0}' is not allowed for strings; use {1} instead"sv, + "'{0}' is not allowed in TypeScript overload signatures"sv, "'{0}' is not allowed on methods"sv, "'{0}' is not allowed with '{1}'"sv, "'{0}' must precede '{1}'"sv, "'{0}' operator cannot be used before '**' without parentheses"sv, + "'{1}' is missing on overload signature"sv, "'{1}' statement starts here"sv, "'}' is not allowed directly in JSX text; write {{'}'} instead"sv, ".d.ts files cannot contain statements, only declarations"sv, @@ -145,6 +148,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "TypeScript optional parameter with type annotation requires parentheses"sv, "TypeScript optional parameters are not allowed in JavaScript"sv, "TypeScript optional properties are not allowed in JavaScript code"sv, + "TypeScript overload signature can only have one semicolon"sv, "TypeScript parameter properties are not allowed in JavaScript"sv, "TypeScript requires whitespace between '>' and '=' here"sv, "TypeScript type annotation requires parentheses"sv, @@ -281,6 +285,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "generator function '*' belongs after keyword function"sv, "generator function '*' belongs before function name"sv, "generic arrow function needs ',' here in TSX"sv, + "getters and setters cannot have overload signatures"sv, "here"sv, "here is the assignment assertion operator"sv, "hex number literal has no digits"sv, @@ -378,6 +383,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "missing semicolon after field"sv, "missing semicolon after index signature"sv, "missing semicolon after interface method"sv, + "missing semicolon after method overload signature"sv, "missing semicolon after statement"sv, "missing semicolon between condition and update parts of for loop"sv, "missing semicolon between init and condition parts of for loop"sv, @@ -396,6 +402,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "newline is not allowed after '{0}'"sv, "newline is not allowed between 'async' and 'function'"sv, "newline is not allowed between 'async' and arrow function parameter list"sv, + "newline is not allowed between '{0}' and the method name"sv, "newline is not allowed between field name and '!'"sv, "nullish coalescing operator does nothing when left operand is never null"sv, "number literal contains consecutive underscores"sv, @@ -410,7 +417,11 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "optional parameter cannot be followed by a required parameter"sv, "optional parameter cannot have both '?' and initializer; remove '?'"sv, "optional tuple elements cannot come after spread elements"sv, + "original semicolon is here"sv, + "overload signature must have the correct access specifier ('{1}')"sv, + "overload signature must match modifiers on this overload method"sv, "overloaded function '{0}' declared here"sv, + "overloaded method is marked '{0}'"sv, "parameter properties are not allowed in 'declare class'"sv, "parameter properties are only allowed in class constructors"sv, "parameter properties cannot be a rest parameter"sv, @@ -502,6 +513,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "with statement is missing '{1}' around expression"sv, "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 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 fad782d3ac..0f41154af3 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[483] = { +inline const Translated_String test_translation_table[495] = { { "\"global-groups\" entries must be strings"_translatable, u8"\"global-groups\" entries must be strings", @@ -875,6 +875,17 @@ inline const Translated_String test_translation_table[483] = { u8"'{0}' here", }, }, + { + "'{0}' is missing on overloaded method"_translatable, + u8"'{0}' is missing on overloaded method", + { + u8"'{0}' is missing on overloaded method", + u8"'{0}' is missing on overloaded method", + u8"'{0}' is missing on overloaded method", + u8"'{0}' is missing on overloaded method", + u8"'{0}' is missing on overloaded method", + }, + }, { "'{0}' is not allowed for strings; use {1} instead"_translatable, u8"'{0}' is not allowed for strings; use {1} instead", @@ -886,6 +897,17 @@ inline const Translated_String test_translation_table[483] = { u8"'{0}' is not allowed for strings; use {1} instead", }, }, + { + "'{0}' is not allowed in TypeScript overload signatures"_translatable, + u8"'{0}' is not allowed in TypeScript overload signatures", + { + u8"'{0}' is not allowed in TypeScript overload signatures", + u8"'{0}' is not allowed in TypeScript overload signatures", + u8"'{0}' is not allowed in TypeScript overload signatures", + u8"'{0}' is not allowed in TypeScript overload signatures", + u8"'{0}' is not allowed in TypeScript overload signatures", + }, + }, { "'{0}' is not allowed on methods"_translatable, u8"'{0}' is not allowed on methods", @@ -930,6 +952,17 @@ inline const Translated_String test_translation_table[483] = { u8"'{0}' operator cannot be used before '**' without parentheses", }, }, + { + "'{1}' is missing on overload signature"_translatable, + u8"'{1}' is missing on overload signature", + { + u8"'{1}' is missing on overload signature", + u8"'{1}' is missing on overload signature", + u8"'{1}' is missing on overload signature", + u8"'{1}' is missing on overload signature", + u8"'{1}' is missing on overload signature", + }, + }, { "'{1}' statement starts here"_translatable, u8"'{1}' statement starts here", @@ -1337,6 +1370,17 @@ inline const Translated_String test_translation_table[483] = { u8"TypeScript optional properties are not allowed in JavaScript code", }, }, + { + "TypeScript overload signature can only have one semicolon"_translatable, + u8"TypeScript overload signature can only have one semicolon", + { + u8"TypeScript overload signature can only have one semicolon", + u8"TypeScript overload signature can only have one semicolon", + u8"TypeScript overload signature can only have one semicolon", + u8"TypeScript overload signature can only have one semicolon", + u8"TypeScript overload signature can only have one semicolon", + }, + }, { "TypeScript parameter properties are not allowed in JavaScript"_translatable, u8"TypeScript parameter properties are not allowed in JavaScript", @@ -2833,6 +2877,17 @@ inline const Translated_String test_translation_table[483] = { u8"generic arrow function needs ',' here in TSX", }, }, + { + "getters and setters cannot have overload signatures"_translatable, + u8"getters and setters cannot have overload signatures", + { + u8"getters and setters cannot have overload signatures", + u8"getters and setters cannot have overload signatures", + u8"getters and setters cannot have overload signatures", + u8"getters and setters cannot have overload signatures", + u8"getters and setters cannot have overload signatures", + }, + }, { "here"_translatable, u8"here", @@ -3900,6 +3955,17 @@ inline const Translated_String test_translation_table[483] = { u8"missing semicolon after interface method", }, }, + { + "missing semicolon after method overload signature"_translatable, + u8"missing semicolon after method overload signature", + { + u8"missing semicolon after method overload signature", + u8"missing semicolon after method overload signature", + u8"missing semicolon after method overload signature", + u8"missing semicolon after method overload signature", + u8"missing semicolon after method overload signature", + }, + }, { "missing semicolon after statement"_translatable, u8"missing semicolon after statement", @@ -4098,6 +4164,17 @@ inline const Translated_String test_translation_table[483] = { u8"nyrad \u00e4r inte till\u00e5ten mellan 'async' och pilfunktions parameter lista", }, }, + { + "newline is not allowed between '{0}' and the method name"_translatable, + u8"newline is not allowed between '{0}' and the method name", + { + u8"newline is not allowed between '{0}' and the method name", + u8"newline is not allowed between '{0}' and the method name", + u8"newline is not allowed between '{0}' and the method name", + u8"newline is not allowed between '{0}' and the method name", + u8"newline is not allowed between '{0}' and the method name", + }, + }, { "newline is not allowed between field name and '!'"_translatable, u8"newline is not allowed between field name and '!'", @@ -4252,6 +4329,39 @@ inline const Translated_String test_translation_table[483] = { u8"optional tuple elements cannot come after spread elements", }, }, + { + "original semicolon is here"_translatable, + u8"original semicolon is here", + { + u8"original semicolon is here", + u8"original semicolon is here", + u8"original semicolon is here", + u8"original semicolon is here", + u8"original semicolon is here", + }, + }, + { + "overload signature must have the correct access specifier ('{1}')"_translatable, + u8"overload signature must have the correct access specifier ('{1}')", + { + u8"overload signature must have the correct access specifier ('{1}')", + u8"overload signature must have the correct access specifier ('{1}')", + u8"overload signature must have the correct access specifier ('{1}')", + u8"overload signature must have the correct access specifier ('{1}')", + u8"overload signature must have the correct access specifier ('{1}')", + }, + }, + { + "overload signature must match modifiers on this overload method"_translatable, + u8"overload signature must match modifiers on this overload method", + { + u8"overload signature must match modifiers on this overload method", + u8"overload signature must match modifiers on this overload method", + u8"overload signature must match modifiers on this overload method", + u8"overload signature must match modifiers on this overload method", + u8"overload signature must match modifiers on this overload method", + }, + }, { "overloaded function '{0}' declared here"_translatable, u8"overloaded function '{0}' declared here", @@ -4263,6 +4373,17 @@ inline const Translated_String test_translation_table[483] = { u8"overloaded function '{0}' declared here", }, }, + { + "overloaded method is marked '{0}'"_translatable, + u8"overloaded method is marked '{0}'", + { + u8"overloaded method is marked '{0}'", + u8"overloaded method is marked '{0}'", + u8"overloaded method is marked '{0}'", + u8"overloaded method is marked '{0}'", + u8"overloaded method is marked '{0}'", + }, + }, { "parameter properties are not allowed in 'declare class'"_translatable, u8"parameter properties are not allowed in 'declare class'", @@ -5264,6 +5385,17 @@ inline const Translated_String test_translation_table[483] = { u8"write 'const' instead of '{0}' here", }, }, + { + "write '{1}' here or remove it from the overload signature"_translatable, + u8"write '{1}' here or remove it from the overload signature", + { + u8"write '{1}' here or remove it from the overload signature", + u8"write '{1}' here or remove it from the overload signature", + u8"write '{1}' here or remove it from the overload signature", + u8"write '{1}' here or remove it from the overload signature", + u8"write '{1}' here or remove it from the overload signature", + }, + }, { "write the type assertion with 'as' here instead"_translatable, u8"write the type assertion with 'as' here instead", diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7080fa8695..accd3332cd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -146,6 +146,7 @@ quick_lint_js_add_executable( test-parse-statement.cpp test-parse-typescript-ambiguous.cpp test-parse-typescript-angle-type-assertion.cpp + test-parse-typescript-class-overload.cpp test-parse-typescript-class.cpp test-parse-typescript-declare-class.cpp test-parse-typescript-declare-function.cpp diff --git a/test/quick-lint-js/parse-support.cpp b/test/quick-lint-js/parse-support.cpp index dbbd46f289..6103a486b7 100644 --- a/test/quick-lint-js/parse-support.cpp +++ b/test/quick-lint-js/parse-support.cpp @@ -411,6 +411,7 @@ Spy_Visitor test_parse_and_visit_statement( Spy_Visitor test_parse_and_visit_statement( String8_View input, Span diags, Parser_Options options, Source_Location caller) { + SCOPED_TRACE(out_string8(input)); Test_Parser p(input, options, capture_diags); p.parse_and_visit_statement(); p.assert_diagnostics(diags, caller); diff --git a/test/test-parse-typescript-class-overload.cpp b/test/test-parse-typescript-class-overload.cpp new file mode 100644 index 0000000000..8b330b34d1 --- /dev/null +++ b/test/test-parse-typescript-class-overload.cpp @@ -0,0 +1,588 @@ +// Copyright (C) 2020 Matthew "strager" Glazar +// See end of file for extended copyright information. + +#include +#include +#include +#include +#include +#include +#include + +using ::testing::ElementsAreArray; + +namespace quick_lint_js { +namespace { +class Test_Parse_TypeScript_Class_Overload : public Test_Parse_Expression {}; + +TEST_F(Test_Parse_TypeScript_Class_Overload, method_overload_signatures) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n"_sv + u8" f();\n"_sv + u8" f() {}\n"_sv + u8"}", + no_diags, typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_class_scope", // C + "visit_enter_class_scope_body", // { + "visit_enter_function_scope", // f #0 + "visit_exit_function_scope", // f #0 + "visit_enter_function_scope", // f #1 + "visit_enter_function_scope_body", // { + "visit_exit_function_scope", // } + "visit_property_declaration", // f + "visit_exit_class_scope", // } + "visit_variable_declaration", // C + })); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + // ASI + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n"_sv + u8" f()\n"_sv + u8" f() {}\n"_sv + u8"}"_sv, + no_diags, typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n"_sv + u8" f();\n"_sv + u8" \\u{66}() {}\n"_sv + u8"}"_sv, + no_diags, typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n"_sv + u8" \\u{66}();\n"_sv + u8" f() {}\n"_sv + u8"}"_sv, + no_diags, typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + for (String8_View function_name : contextual_keywords) { + Spy_Visitor p = test_parse_and_visit_statement( + concat(u8"class C { "_sv, function_name, u8"(); "_sv, function_name, + u8"() {} }"_sv), + no_diags, typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({function_name})); + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + extra_semicolons_between_method_overload_signatures_are_disallowed) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n f();;\n f() {}\n}", + u8" ^ Diag_Unexpected_Semicolon_After_Overload_Signature.extra_semicolon\n"_diag + u8" ^ .original_semicolon"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f();;; f() {} }", + u8" ^ Diag_Unexpected_Semicolon_After_Overload_Signature.extra_semicolon\n"_diag + u8" ^ .original_semicolon"_diag, + u8" ^ Diag_Unexpected_Semicolon_After_Overload_Signature.extra_semicolon\n"_diag + u8" ^ .original_semicolon"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + semicolon_is_required_after_overload_signature) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f() f() {} }", + u8" ` Diag_Missing_Semicolon_After_TypeScript_Method_Overload_Signature"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_overload_signature_with_wrong_name) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(); g() {} }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.function_name\n"_diag + u8" ^ .overload_name"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"g"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(); g(); h() {} }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.function_name\n"_diag + u8" ^ .overload_name"_diag, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.function_name\n"_diag + u8" ^ .overload_name"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"h"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(); g(); f() {} }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.function_name\n"_diag + u8" ^ .overload_name"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { g(); f(); f() {} }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.function_name\n"_diag + u8" ^ .overload_name"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + // Missing method body is more likely, so don't report + // Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name. + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n f()\n g() {} }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, + ElementsAreArray({u8"f"_sv, u8"g"_sv})); + } + + { + // Missing method body is more likely, so don't report + // Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name. + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n f()\n async\n g() { await(myPromise); } }"_sv, + u8" ^^^^^ Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name.modifier"_diag, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myPromise"_sv})) + << "'async' should be an operator, not a variable reference or field " + "declaration"; + EXPECT_THAT(p.property_declarations, + ElementsAreArray({u8"f"_sv, u8"g"_sv})); + } + + // Missing method body is more likely for a getter or setter, so don't report + // Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name. + test_parse_and_visit_statement( + u8"class C { get p(); get q() {} }"_sv, // + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { set p(v); set q(v) {} }"_sv, // + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + mixed_computed_name_and_normal_name_is_allowed) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { 'method'(); method() {} }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { method(); 'method'() {} }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { ['method'](); method() {} }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { ['method'](); ['method']() {} }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({std::nullopt})); + } +} + +// TODO[TypeScript-overload-signature-with-computed-property]: Report +// Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name if two +// computed properties (or string or number literal properties) have different +// names according to TypeScript's non-trivial expression equality rules. + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_overload_signature_with_wrong_computed_name) { + // HACK(strager): The parser does not handle computed names intelligently. See + // TODO[TypeScript-overload-signature-with-computed-property]. For now, if the + // function's name is computed, the parser treats the first non-computed + // overload name as the correct name. + test_parse_and_visit_statement( + u8"class C { f(); g(); 'f'() {} }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.overload_name\n"_diag + u8" ^ .function_name"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { f(); g(); 'g'() {} }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Have_Same_Name.overload_name\n"_diag + u8" ^ .function_name"_diag, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_overload_signature_with_newline_after_modifier) { + { + // Normally, we would treat 'async' as a field name here. However, we + // know that there's a syntax error anyway (missing function body), so + // Diag_Newline_Not_Allowed_Between_Async_And_Method_Name seems more + // helpful. + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n f()\n async\n f() { await(myPromise); } }"_sv, + u8" ^^^^^ Diag_Newline_Not_Allowed_Between_Modifier_And_Method_Name.modifier"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myPromise"_sv})) + << "await should be treated as an operator"; + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_overload_signature_declaration_can_be_async) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { async f(); f() { await(myValue); } }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, + ElementsAreArray({u8"await"_sv, u8"myValue"_sv})) + << "f's body should be parsed as in a normal function"; + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + function_with_overload_signatures_can_be_async) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(); async f() { await(myValue); } }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myValue"_sv})) + << "f's body should be parsed as in an async function"; + } + + test_parse_and_visit_statement( + u8"class C { async f(); async f() { await(myValue); } }"_sv, no_diags, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_overload_signature_declaration_cannot_have_generator_star) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { *f(); *f() { yield(myValue); } }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Not_Have_Generator_Star"_diag, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myValue"})) + << "f's body should be parsed as in a generator function"; + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { *f(a); *f(a, b); *f(...args) { yield(myValue); } }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Not_Have_Generator_Star"_diag, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Not_Have_Generator_Star"_diag, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myValue"})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { *f(a); f(a, b); f(...args) { yield(myValue); } }"_sv, + u8" ^ Diag_TypeScript_Function_Overload_Signature_Must_Not_Have_Generator_Star"_diag, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"yield", u8"myValue"})) + << "'yield' should not be a keyword in the implementation"; + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_with_overload_signatures_can_be_generator) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(); *f() { yield(myValue); } }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myValue"_sv})) + << "f's body should be parsed as in a generator function"; + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + method_with_overload_signatures_can_be_async_generator) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(); async *f() { yield(await(myValue)); } }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"myValue"_sv})) + << "f's body should be parsed as in an async generator function"; + } + + test_parse_and_visit_statement( + u8"class C { async f(); async *f() { yield(await(myValue)); } }"_sv, + no_diags, typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + field_after_overload_signature_is_invalid) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { method(); field; }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_class_scope", // C + "visit_enter_class_scope_body", // { + "visit_enter_function_scope", // method + "visit_exit_function_scope", // method + "visit_property_declaration", // method + "visit_property_declaration", // field + "visit_exit_class_scope", // } + "visit_variable_declaration", // C + })); + EXPECT_THAT(p.property_declarations, + ElementsAreArray({u8"method"_sv, u8"field"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { method(); field = null; }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, + ElementsAreArray({u8"method"_sv, u8"field"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { method(); field: T; }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, + ElementsAreArray({u8"method"_sv, u8"field"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n method();\n field\n otherMethod() {} }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT( + p.property_declarations, + ElementsAreArray({u8"method"_sv, u8"field"_sv, u8"otherMethod"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C {\n method();\n field\n ['otherMethod']() {} }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, + ElementsAreArray>( + {u8"method"_sv, u8"field"_sv, std::nullopt})); + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signature_at_end_is_invalid) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { method(); }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } + + // The extra semicolon should not lead to + // Diag_Unexpected_Semicolon_After_Overload_Signature. + test_parse_and_visit_statement( + u8"class C { method();; }", + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { method() }"_sv, + u8" ` Diag_Missing_Function_Body"_diag, + typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"method"_sv})); + } +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signatures_can_be_generic) { + test_parse_and_visit_statement(u8"class C { m(); m() {} }"_sv, no_diags, + typescript_options); + + // Generic parameters don't need to match. + test_parse_and_visit_statement(u8"class C { m(); m() {} }"_sv, no_diags, + typescript_options); + test_parse_and_visit_statement(u8"class C { m(); m() {} }"_sv, no_diags, + typescript_options); + test_parse_and_visit_statement(u8"class C { m(); m() {} }"_sv, no_diags, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signatures_are_not_allowed_on_getters_or_setters) { + test_parse_and_visit_statement( + u8"class C { get p(); get p() {} }"_sv, // + u8" ^^^ Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { get p(); p() {} }"_sv, // + u8" ^^^ Diag_Class_Modifier_Not_Allowed_On_TypeScript_Overload_Signature"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { p(); get p() {} }"_sv, // + u8" ^^^ Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature"_diag, + typescript_options); + + test_parse_and_visit_statement( + u8"class C { set p(v); set p(v) {} }"_sv, // + u8" ^^^ Diag_Getter_Or_Setter_Cannot_Have_TypeScript_Overload_Signature"_diag, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signatures_must_have_matching_optionality) { + test_parse_and_visit_statement( + u8"class C { m?(); m() {} }"_sv, // + u8" ` Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature.missing_method_modifier\n"_diag + u8" ^ .signature_modifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { m(); m?() {} }"_sv, // + u8" ^ Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature.method_modifier\n"_diag + u8" ` .missing_signature_modifier"_diag, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signatures_must_have_matching_static) { + test_parse_and_visit_statement( + u8"class C { static m(); m() {} }"_sv, // + u8" ` Diag_Class_Modifier_Missing_On_Method_With_TypeScript_Overload_Signature.missing_method_modifier\n"_diag + u8" ^^^^^^ .signature_modifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { m(); static m() {} }"_sv, // + u8" ^^^^^^ Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature.method_modifier\n"_diag + u8" ` .missing_signature_modifier"_diag, + typescript_options); +} + +// TODO(strager): Check 'abstract' conflicts: +// +// abstract class C { abstract m(); m(); } // Invalid. +// abstract class C { m(); abstract m(); } // Invalid. +// abstract class C { abstract m(); abstract m(); } // OK. +// abstract class C { abstract m(); abstract g(); } // OK. + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signatures_may_omit_public) { + test_parse_and_visit_statement(u8"class C { m(); public m() {} }"_sv, + no_diags, typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + public_overload_signature_allowed_on_method_without_public) { + test_parse_and_visit_statement(u8"class C { public m(); m() {} }"_sv, + no_diags, typescript_options); +} + +TEST_F( + Test_Parse_TypeScript_Class_Overload, + overload_signatures_of_private_or_protected_method_must_have_access_specifier) { + test_parse_and_visit_statement( + u8"class C { m(); private m() {} }"_sv, // + u8" ^^^^^^^ Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature.method_modifier\n"_diag + u8" ` .missing_signature_modifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { m(); protected m() {} }"_sv, // + u8" ^^^^^^^^^ Diag_Class_Modifier_Missing_On_TypeScript_Overload_Signature.method_modifier\n"_diag + u8" ` .missing_signature_modifier"_diag, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + overload_signatures_must_have_matching_access_specifiers) { + test_parse_and_visit_statement( + u8"class C { public m(); private m() {} }"_sv, // + u8" ^^^^^^^ Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch.method_access_specifier\n"_diag + u8" ^^^^^^ .signature_access_specifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { private m(); public m() {} }"_sv, // + u8" ^^^^^^ Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch.method_access_specifier\n"_diag + u8" ^^^^^^^ .signature_access_specifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { protected m(); public m() {} }"_sv, // + u8" ^^^^^^ Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch.method_access_specifier\n"_diag + u8" ^^^^^^^^^ .signature_access_specifier"_diag, + typescript_options); + test_parse_and_visit_statement( + u8"class C { public m(); private m(); protected m() {} }"_sv, // + u8" ^^^^^^^^^ Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch.method_access_specifier\n"_diag + u8" ^^^^^^^ .signature_access_specifier"_diag, + u8" ^^^^^^^^^ Diag_TypeScript_Overload_Signature_Access_Specifier_Mismatch.method_access_specifier\n"_diag + u8" ^^^^^^ .signature_access_specifier"_diag, + typescript_options); +} + +TEST_F(Test_Parse_TypeScript_Class_Overload, + interfaces_do_not_interpret_method_overload_signatures) { + Spy_Visitor p = test_parse_and_visit_statement( + u8"interface I {\n"_sv + u8" f();\n"_sv + u8" f();\n"_sv + u8"}", + no_diags, typescript_options); + EXPECT_THAT(p.property_declarations, ElementsAreArray({u8"f"_sv, u8"f"_sv})) + << "the first f declaration should not be interpreted as an overload " + "signature"; +} +} +} + +// quick-lint-js finds bugs in JavaScript programs. +// Copyright (C) 2020 Matthew "strager" Glazar +// +// This file is part of quick-lint-js. +// +// quick-lint-js is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// quick-lint-js is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with quick-lint-js. If not, see .