diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 51b873e3d0..b70e8f409b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,6 +16,7 @@ Semantic Versioning. Masani][].) * TypeScript support (still experimental): * `export as namespace` statements are now parsed. + * Assertion signatures (`function f(param): asserts param`) are now parsed. * `case await x:` no longer treats `:` as if it was a type annotation colon in an arrow function parameter list. * If a type predicate appears outside a return type, quick-lint-js now reports diff --git a/po/messages.pot b/po/messages.pot index d552051848..32e03a203b 100644 --- a/po/messages.pot +++ b/po/messages.pot @@ -1349,6 +1349,10 @@ msgstr "" msgid "use ':' instead of '{0}' to type a function parameter" msgstr "" +#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +msgid "assertion signatures are only allowed as function return types" +msgstr "" + #: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp msgid "assignment-asserted fields are not allowed in 'declare class'" msgstr "" diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp index c69da16718..107deb6f18 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.cpp @@ -3998,10 +3998,24 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, - // Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_Declare_Class + // Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types { .code = 336, .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("assertion signatures are only allowed as function return types"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types, asserts_keyword), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + + // Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_Declare_Class + { + .code = 425, + .severity = Diagnostic_Severity::error, .message_formats = { QLJS_TRANSLATABLE("assignment-asserted fields are not allowed in 'declare class'"), }, @@ -5840,6 +5854,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = { }, }, + // Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature + { + .code = 428, + .severity = Diagnostic_Severity::error, + .message_formats = { + QLJS_TRANSLATABLE("{0} is not the name of a parameter"), + }, + .message_args = { + { + Diagnostic_Message_Arg_Info(offsetof(Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature, name), Diagnostic_Arg_Type::source_code_span), + }, + }, + }, + // Diag_Use_Of_Undeclared_Parameter_In_Type_Predicate { .code = 315, diff --git a/src/quick-lint-js/diag/diagnostic-metadata-generated.h b/src/quick-lint-js/diag/diagnostic-metadata-generated.h index ebebd8b9d5..e3ef34d123 100644 --- a/src/quick-lint-js/diag/diagnostic-metadata-generated.h +++ b/src/quick-lint-js/diag/diagnostic-metadata-generated.h @@ -277,6 +277,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_TypeScript_As_Const_With_Non_Literal_Typeable) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_As_Type_Assertion_Not_Allowed_In_JavaScript) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_As_Or_Satisfies_Used_For_Parameter_Type_Annotation) \ + QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_Declare_Class) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_Interfaces) \ QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_JavaScript) \ @@ -400,6 +401,7 @@ namespace quick_lint_js { QLJS_DIAG_TYPE_NAME(Diag_Unmatched_Indexing_Bracket) \ QLJS_DIAG_TYPE_NAME(Diag_Unmatched_Parenthesis) \ QLJS_DIAG_TYPE_NAME(Diag_Unmatched_Right_Curly) \ + QLJS_DIAG_TYPE_NAME(Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature) \ QLJS_DIAG_TYPE_NAME(Diag_Use_Of_Undeclared_Parameter_In_Type_Predicate) \ QLJS_DIAG_TYPE_NAME(Diag_Use_Of_Undeclared_Type) \ QLJS_DIAG_TYPE_NAME(Diag_Use_Of_Undeclared_Variable) \ @@ -448,7 +450,7 @@ namespace quick_lint_js { /* END */ // clang-format on -inline constexpr int Diag_Type_Count = 434; +inline constexpr int Diag_Type_Count = 436; 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 2e05b00d01..f19cb7877b 100644 --- a/src/quick-lint-js/diag/diagnostic-types-2.h +++ b/src/quick-lint-js/diag/diagnostic-types-2.h @@ -2060,8 +2060,16 @@ struct Diag_TypeScript_As_Or_Satisfies_Used_For_Parameter_Type_Annotation { Source_Code_Span bad_keyword; }; -struct Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_Declare_Class { +struct Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types { [[qljs::diag("E0336", Diagnostic_Severity::error)]] // + [[qljs::message( + "assertion signatures are only allowed as function return types", + ARG(asserts_keyword))]] // + Source_Code_Span asserts_keyword; +}; + +struct Diag_TypeScript_Assignment_Asserted_Fields_Not_Allowed_In_Declare_Class { + [[qljs::diag("E0425", Diagnostic_Severity::error)]] // [[qljs::message( "assignment-asserted fields are not allowed in 'declare class'", ARG(bang))]] // @@ -3027,6 +3035,12 @@ struct Diag_Unmatched_Right_Curly { Source_Code_Span right_curly; }; +struct Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature { + [[qljs::diag("E0428", Diagnostic_Severity::error)]] // + [[qljs::message("{0} is not the name of a parameter", ARG(name))]] // + Source_Code_Span name; +}; + struct Diag_Use_Of_Undeclared_Parameter_In_Type_Predicate { [[qljs::diag("E0315", Diagnostic_Severity::error)]] // [[qljs::message("{0} is not the name of a parameter", ARG(name))]] // diff --git a/src/quick-lint-js/fe/parse-expression.cpp b/src/quick-lint-js/fe/parse-expression.cpp index e106b5d7b2..568877b007 100644 --- a/src/quick-lint-js/fe/parse-expression.cpp +++ b/src/quick-lint-js/fe/parse-expression.cpp @@ -890,10 +890,11 @@ Expression* Parser::parse_async_expression_only( if (this->peek().type == Token_Type::colon && this->options_.typescript) { // async (params): ReturnType => {} // TypeScript only. this->parse_and_visit_typescript_colon_type_expression( - return_type_visits, TypeScript_Type_Parse_Options{ - .allow_parenthesized_type = false, - .allow_type_predicate = true, - }); + return_type_visits, + TypeScript_Type_Parse_Options{ + .allow_parenthesized_type = false, + .allow_assertion_signature_or_type_predicate = true, + }); } bool is_arrow_function = this->peek().type == Token_Type::equal_greater; @@ -1031,10 +1032,11 @@ Expression* Parser::parse_async_expression_only( Buffering_Visitor return_type_visits(&this->type_expression_memory_); if (this->peek().type == Token_Type::colon) { this->parse_and_visit_typescript_colon_type_expression( - return_type_visits, TypeScript_Type_Parse_Options{ - .allow_parenthesized_type = false, - .allow_type_predicate = true, - }); + return_type_visits, + TypeScript_Type_Parse_Options{ + .allow_parenthesized_type = false, + .allow_assertion_signature_or_type_predicate = true, + }); } QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::equal_greater); @@ -2177,7 +2179,7 @@ Expression* Parser::parse_expression_remainder(Parse_Visitor_Base& v, TypeScript_Type_Parse_Options{ .allow_parenthesized_type = !is_possibly_arrow_function_return_type_annotation, - .allow_type_predicate = + .allow_assertion_signature_or_type_predicate = is_possibly_arrow_function_return_type_annotation, }); const Char8* type_end = this->lexer_.end_of_previous_token(); @@ -3880,10 +3882,11 @@ Expression* Parser::parse_typescript_generic_arrow_expression( Buffering_Visitor return_type_visits(&this->type_expression_memory_); if (this->peek().type == Token_Type::colon) { this->parse_and_visit_typescript_colon_type_expression( - return_type_visits, TypeScript_Type_Parse_Options{ - .allow_parenthesized_type = false, - .allow_type_predicate = true, - }); + return_type_visits, + TypeScript_Type_Parse_Options{ + .allow_parenthesized_type = false, + .allow_assertion_signature_or_type_predicate = true, + }); } QLJS_PARSER_UNIMPLEMENTED_IF_NOT_TOKEN(Token_Type::equal_greater); @@ -3953,10 +3956,11 @@ Expression* Parser::parse_typescript_angle_type_assertion_expression( Buffering_Visitor return_type_visits(&this->type_expression_memory_); if (this->peek().type == Token_Type::colon) { this->parse_and_visit_typescript_colon_type_expression( - return_type_visits, TypeScript_Type_Parse_Options{ - .allow_parenthesized_type = false, - .allow_type_predicate = true, - }); + return_type_visits, + TypeScript_Type_Parse_Options{ + .allow_parenthesized_type = false, + .allow_assertion_signature_or_type_predicate = true, + }); } if (this->peek().type == Token_Type::equal_greater) { // (param) => body diff --git a/src/quick-lint-js/fe/parse-statement.cpp b/src/quick-lint-js/fe/parse-statement.cpp index 5318429aa4..439b8e0133 100644 --- a/src/quick-lint-js/fe/parse-statement.cpp +++ b/src/quick-lint-js/fe/parse-statement.cpp @@ -2277,7 +2277,7 @@ Parser::parse_and_visit_function_parameter_list( this->parse_and_visit_typescript_colon_type_expression( v, TypeScript_Type_Parse_Options{ .allow_parenthesized_type = true, - .allow_type_predicate = true, + .allow_assertion_signature_or_type_predicate = true, }); } diff --git a/src/quick-lint-js/fe/parse-type.cpp b/src/quick-lint-js/fe/parse-type.cpp index 7fabbb8502..300b4f0262 100644 --- a/src/quick-lint-js/fe/parse-type.cpp +++ b/src/quick-lint-js/fe/parse-type.cpp @@ -101,7 +101,7 @@ void Parser::parse_and_visit_typescript_type_expression( Identifier readonly = this->peek().identifier_name(); this->skip(); - if (parse_options.allow_type_predicate && + if (parse_options.allow_assertion_signature_or_type_predicate && this->peek().type == Token_Type::kw_is) { // readonly is Type this->lexer_.roll_back_transaction(std::move(transaction)); @@ -183,7 +183,6 @@ void Parser::parse_and_visit_typescript_type_expression( case Token_Type::kw_accessor: case Token_Type::kw_as: case Token_Type::kw_assert: - case Token_Type::kw_asserts: case Token_Type::kw_async: case Token_Type::kw_await: case Token_Type::kw_constructor: @@ -223,13 +222,13 @@ void Parser::parse_and_visit_typescript_type_expression( this->skip(); if (name_type != Token_Type::kw_this) { // this is Type - if (parse_options.allow_type_predicate) { + if (parse_options.allow_assertion_signature_or_type_predicate) { v.visit_variable_type_predicate_use(name); } else { v.visit_variable_use(name); } } - if (!parse_options.allow_type_predicate) { + if (!parse_options.allow_assertion_signature_or_type_predicate) { this->diag_reporter_->report( Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type{ .is_keyword = is_keyword, @@ -281,6 +280,60 @@ void Parser::parse_and_visit_typescript_type_expression( break; } + // asserts + // asserts param + // asserts param is Type + case Token_Type::kw_asserts: { + Lexer_Transaction transaction = this->lexer_.begin_transaction(); + Source_Code_Span asserts_keyword = this->peek().span(); + this->skip(); + switch (this->peek().type) { + // asserts param + // asserts param is Type + QLJS_CASE_CONTEXTUAL_KEYWORD: + case Token_Type::kw_this: + case Token_Type::identifier: + if (this->peek().type == Token_Type::kw_is) { + // asserts is Type // Type predicate for parameter 'asserts'. + // asserts is is Type // Invalid. + this->lexer_.roll_back_transaction(std::move(transaction)); + goto type_variable_or_namespace_or_type_predicate; + } + + this->lexer_.commit_transaction(std::move(transaction)); + if (this->peek().type == Token_Type::kw_this) { + // TODO(#881): Only allow 'this' within class and interface method + // signatures. + } else { + if (parse_options.allow_assertion_signature_or_type_predicate) { + v.visit_variable_assertion_signature_use( + this->peek().identifier_name()); + } else { + v.visit_variable_use(this->peek().identifier_name()); + } + } + if (!parse_options.allow_assertion_signature_or_type_predicate) { + this->diag_reporter_->report( + Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types{ + .asserts_keyword = asserts_keyword, + }); + } + this->skip(); + if (this->peek().type == Token_Type::kw_is) { + // asserts param is Type + this->skip(); + goto again; // Parse a type. + } + return; + + // asserts // Parameter name. + default: + this->lexer_.roll_back_transaction(std::move(transaction)); + goto type_variable_or_namespace_or_type_predicate; + } + break; + } + // infer T // Invalid. // T extends infer U ? V : W // T extends infer U extends X ? V : W @@ -321,7 +374,7 @@ void Parser::parse_and_visit_typescript_type_expression( // infer is // 'is' is the declared name. // infer is Type // Type predicate where 'infer' is the parameter name. case Token_Type::kw_is: - if (parse_options.allow_type_predicate) { + if (parse_options.allow_assertion_signature_or_type_predicate) { // infer is Type this->lexer_.roll_back_transaction(std::move(transaction)); goto type_variable_or_namespace_or_type_predicate; @@ -374,7 +427,7 @@ void Parser::parse_and_visit_typescript_type_expression( case Token_Type::kw_unique: { Lexer_Transaction transaction = this->lexer_.begin_transaction(); this->skip(); - if (parse_options.allow_type_predicate && + if (parse_options.allow_assertion_signature_or_type_predicate && this->peek().type == Token_Type::kw_is) { // unique is Type this->lexer_.roll_back_transaction(std::move(transaction)); @@ -571,7 +624,7 @@ void Parser::parse_and_visit_typescript_type_expression( case Token_Type::kw_keyof: { Lexer_Transaction transaction = this->lexer_.begin_transaction(); this->skip(); - if (parse_options.allow_type_predicate && + if (parse_options.allow_assertion_signature_or_type_predicate && this->peek().type == Token_Type::kw_is) { // keyof is Type this->lexer_.roll_back_transaction(std::move(transaction)); @@ -744,7 +797,7 @@ void Parser:: this->parse_and_visit_typescript_type_expression( v, TypeScript_Type_Parse_Options{ .allow_parenthesized_type = false, - .allow_type_predicate = true, + .allow_assertion_signature_or_type_predicate = true, }); } diff --git a/src/quick-lint-js/fe/parse.h b/src/quick-lint-js/fe/parse.h index c9eaa97348..93077d68c7 100644 --- a/src/quick-lint-js/fe/parse.h +++ b/src/quick-lint-js/fe/parse.h @@ -211,8 +211,9 @@ class Parser { std::optional type_being_declared = std::nullopt; bool parse_question_as_invalid = true; bool allow_parenthesized_type = true; - // If false, a type predicate is parsed but a diagnostic is reported. - bool allow_type_predicate = false; + // If false, assertion predicates and type predicates are parsed but a + // diagnostic is reported. + bool allow_assertion_signature_or_type_predicate = false; }; void parse_and_visit_typescript_colon_type_expression(Parse_Visitor_Base &v); diff --git a/src/quick-lint-js/fe/variable-analyzer.cpp b/src/quick-lint-js/fe/variable-analyzer.cpp index 479b7a71db..a28145483f 100644 --- a/src/quick-lint-js/fe/variable-analyzer.cpp +++ b/src/quick-lint-js/fe/variable-analyzer.cpp @@ -433,8 +433,17 @@ void Variable_Analyzer::visit_variable_assignment(Identifier name) { void Variable_Analyzer::visit_variable_assertion_signature_use([ [maybe_unused]] Identifier name) { - // TODO(#690) - QLJS_UNIMPLEMENTED(); + QLJS_ASSERT(!this->scopes_.empty()); + Scope ¤t_scope = this->current_scope(); + Declared_Variable *var = current_scope.declared_variables.find_runtime(name); + if (var) { + // FIXME(strager): Should we mark the parameter as used? + } else { + this->diag_reporter_->report( + Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature{ + .name = name.span(), + }); + } } void Variable_Analyzer::visit_variable_delete_use( diff --git a/src/quick-lint-js/i18n/translation-table-generated.cpp b/src/quick-lint-js/i18n/translation-table-generated.cpp index 183e68cf6c..defdcdfbb9 100644 --- a/src/quick-lint-js/i18n/translation-table-generated.cpp +++ b/src/quick-lint-js/i18n/translation-table-generated.cpp @@ -177,7 +177,8 @@ const Translation_Table translation_data = { {0, 26, 0, 21, 0, 18}, // {18, 53, 70, 52, 76, 53}, // {19, 30, 21, 19, 19, 19}, // - {15, 13, 19, 17, 15, 14}, // + {0, 0, 0, 0, 0, 14}, // + {15, 13, 19, 17, 15, 63}, // {0, 0, 0, 0, 0, 59}, // {80, 64, 90, 59, 56, 66}, // {40, 33, 46, 45, 40, 36}, // @@ -1979,6 +1980,7 @@ const Translation_Table translation_data = { u8"another invalid string, do not use outside benchmark\0" u8"array started here\0" u8"arrow is here\0" + u8"assertion signatures are only allowed as function return types\0" u8"assigning to 'async' in a for-of loop requires parentheses\0" u8"assignment assertion is not allowed on fields be marked 'declare'\0" u8"assignment to const global variable\0" diff --git a/src/quick-lint-js/i18n/translation-table-generated.h b/src/quick-lint-js/i18n/translation-table-generated.h index fc96ad0008..a6ee991325 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 = 531; -constexpr std::size_t translation_table_string_table_size = 80436; +constexpr std::uint16_t translation_table_mapping_table_size = 532; +constexpr std::size_t translation_table_string_table_size = 80499; constexpr std::size_t translation_table_locale_table_size = 35; QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( @@ -192,6 +192,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up( "another invalid string, do not use outside benchmark"sv, "array started here"sv, "arrow is here"sv, + "assertion signatures are only allowed as function return types"sv, "assigning to 'async' in a for-of loop requires parentheses"sv, "assignment assertion is not allowed on fields be marked 'declare'"sv, "assignment to const global variable"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 ae2fa59916..6c9903c801 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[530] = { +inline const Translated_String test_translation_table[531] = { { "\"global-groups\" entries must be strings"_translatable, u8"\"global-groups\" entries must be strings", @@ -1854,6 +1854,17 @@ inline const Translated_String test_translation_table[530] = { u8"pilen \u00e4r h\u00e4r", }, }, + { + "assertion signatures are only allowed as function return types"_translatable, + u8"assertion signatures are only allowed as function return types", + { + u8"assertion signatures are only allowed as function return types", + u8"assertion signatures are only allowed as function return types", + u8"assertion signatures are only allowed as function return types", + u8"assertion signatures are only allowed as function return types", + u8"assertion signatures are only allowed as function return types", + }, + }, { "assigning to 'async' in a for-of loop requires parentheses"_translatable, u8"assigning to 'async' in a for-of loop requires parentheses", diff --git a/test/test-parse-typescript-function.cpp b/test/test-parse-typescript-function.cpp index 04e0c184b6..0594e315f3 100644 --- a/test/test-parse-typescript-function.cpp +++ b/test/test-parse-typescript-function.cpp @@ -1062,6 +1062,157 @@ TEST_F(Test_Parse_TypeScript_Function, })); } } + +TEST_F(Test_Parse_TypeScript_Function, boolean_assertion_signature) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(param): asserts param {}"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.visits, + ElementsAreArray({ + "visit_enter_function_scope", // + "visit_variable_declaration", // param + "visit_variable_assertion_signature_use", // param + "visit_enter_function_scope_body", // { + "visit_exit_function_scope", // } + "visit_variable_declaration", // f + })); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"param"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_expression( + u8"(p): asserts p => {}"_sv, no_diags, typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_function_scope", // + "visit_variable_declaration", // p + "visit_variable_assertion_signature_use", // p + "visit_enter_function_scope_body", // { + "visit_exit_function_scope", // } + })); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p"_sv})); + } +} + +TEST_F(Test_Parse_TypeScript_Function, assertion_signature_with_type) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(param): asserts param is MyType {}"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.visits, + ElementsAreArray({ + "visit_enter_function_scope", // + "visit_variable_declaration", // param + "visit_variable_assertion_signature_use", // param + "visit_variable_type_use", // MyType + "visit_enter_function_scope_body", // { + "visit_exit_function_scope", // } + "visit_variable_declaration", // f + })); + EXPECT_THAT(p.variable_uses, + ElementsAreArray({u8"param"_sv, u8"MyType"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(p): asserts p is MyType|MyOtherType {}"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p"_sv, u8"MyType"_sv, + u8"MyOtherType"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(p): asserts p is readonly T[] {}"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p"_sv, u8"T"_sv})); + } +} + +TEST_F(Test_Parse_TypeScript_Function, assertion_signature_with_funny_type) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(p): asserts p is is {}"_sv, no_diags, typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p"_sv, u8"is"_sv})); + } +} + +TEST_F(Test_Parse_TypeScript_Function, assertion_signature_can_assert_this) { + // TODO(#881): Only allow 'this' within class and interface method signatures. + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(): asserts this {} }"_sv, no_diags, typescript_options); + EXPECT_THAT(p.variable_uses, IsEmpty()); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"class C { f(): asserts this is SomeType {} }"_sv, no_diags, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"SomeType"_sv})); + } +} + +TEST_F( + Test_Parse_TypeScript_Function, + assertion_signature_can_assert_parameter_named_contextual_keyword_except_is) { + for (String8_View keyword : + contextual_keywords - Dirty_Set{u8"is"}) { + SCOPED_TRACE(out_string8(keyword)); + + { + Spy_Visitor p = test_parse_and_visit_statement( + concat(u8"class C { f("_sv, keyword, u8"): asserts "_sv, keyword, + u8" {} }"_sv), + no_diags, typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({keyword})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + concat(u8"class C { f("_sv, keyword, u8"): asserts "_sv, keyword, + u8" is string {} }"_sv), + no_diags, typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({keyword})); + } + } +} + +TEST_F(Test_Parse_TypeScript_Function, + assertion_signature_is_only_allowed_as_return_types) { + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(p, q: asserts p) {}"_sv, // + u8" ^^^^^^^ Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types"_diag, + typescript_options); + EXPECT_THAT(p.visits, ElementsAreArray({ + "visit_enter_function_scope", // + "visit_variable_declaration", // p + "visit_variable_use", // p + "visit_variable_declaration", // q + "visit_enter_function_scope_body", // { + "visit_exit_function_scope", // } + "visit_variable_declaration", // f + })); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"let x: asserts x is string;"_sv, // + u8" ^^^^^^^ Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types"_diag, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"x"_sv})); + } + + { + Spy_Visitor p = test_parse_and_visit_statement( + u8"function f(p): asserts p is (asserts u) {}"_sv, // + u8" ^^^^^^^ Diag_TypeScript_Assertion_Signature_Only_Allowed_As_Return_Types"_diag, + typescript_options); + EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p"_sv, u8"u"_sv})); + } +} } } diff --git a/test/test-variable-analyzer-type.cpp b/test/test-variable-analyzer-type.cpp index 41cef832c9..fbb49b1e1e 100644 --- a/test/test-variable-analyzer-type.cpp +++ b/test/test-variable-analyzer-type.cpp @@ -672,6 +672,30 @@ TEST(Test_Variable_Analyzer_Type, typescript_analyze_options, default_globals); } +TEST(Test_Variable_Analyzer_Type, + assertion_signature_finds_function_parameter) { + test_parse_and_analyze( + u8"((p): asserts p => {"_sv + u8"});"_sv, + no_diags, typescript_analyze_options, default_globals); +} + +TEST(Test_Variable_Analyzer_Type, + assertion_signature_does_not_find_outer_function_parameter) { + test_parse_and_analyze( + u8"((outer) => { ((inner): asserts outer => { }); });"_sv, + u8" ^^^^^ Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature.name"_diag, + typescript_analyze_options, default_globals); +} + +TEST(Test_Variable_Analyzer_Type, + assertion_signature_does_not_find_generic_parameter) { + test_parse_and_analyze( + u8"((p): asserts T is String => { });"_sv, + u8" ^ Diag_Use_Of_Undeclared_Parameter_In_Assertion_Signature.name"_diag, + typescript_analyze_options, default_globals); +} + TEST(Test_Variable_Analyzer_Type, variables_referenced_in_conditional_type_scope_are_looked_up) { test_parse_and_analyze(