Skip to content

Commit 4ac79ee

Browse files
committed
feat(typescript): error on 'var foo: p is Type'
Report a diagnostic if a TypeScript type predicate appears where it shouldn't appear.
1 parent aaed137 commit 4ac79ee

11 files changed

+96
-6
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Semantic Versioning.
1414
* `export as namespace` statements are now parsed.
1515
* `case await x:` no longer treats `:` as if it was a type annotation colon in
1616
an arrow function parameter list.
17+
* If a type predicate appears outside a return type, quick-lint-js now reports
18+
[E0426][] ("type predicates are only allowed as function return types").
1719

1820
### Fixed
1921

@@ -1293,6 +1295,7 @@ Beta release.
12931295
[E0383]: https://quick-lint-js.com/errors/E0383/
12941296
[E0384]: https://quick-lint-js.com/errors/E0384/
12951297
[E0398]: https://quick-lint-js.com/errors/E0398/
1298+
[E0426]: https://quick-lint-js.com/errors/E0426/
12961299
[E0450]: https://quick-lint-js.com/errors/E0450/
12971300
[E0451]: https://quick-lint-js.com/errors/E0451/
12981301
[E0452]: https://quick-lint-js.com/errors/E0452/

po/messages.pot

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,10 @@ msgstr ""
15091509
msgid "TypeScript type exports are not allowed in JavaScript"
15101510
msgstr ""
15111511

1512+
#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
1513+
msgid "type predicates are only allowed as function return types"
1514+
msgstr ""
1515+
15121516
#: src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
15131517
msgid "'type' cannot be used twice in export"
15141518
msgstr ""

src/quick-lint-js/diag/diagnostic-metadata-generated.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4481,6 +4481,20 @@ const QLJS_CONSTINIT Diagnostic_Info all_diagnostic_infos[] = {
44814481
},
44824482
},
44834483

4484+
// Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type
4485+
{
4486+
.code = 426,
4487+
.severity = Diagnostic_Severity::error,
4488+
.message_formats = {
4489+
QLJS_TRANSLATABLE("type predicates are only allowed as function return types"),
4490+
},
4491+
.message_args = {
4492+
{
4493+
Diagnostic_Message_Arg_Info(offsetof(Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type, is_keyword), Diagnostic_Arg_Type::source_code_span),
4494+
},
4495+
},
4496+
},
4497+
44844498
// Diag_TypeScript_Inline_Type_Export_Not_Allowed_In_Type_Only_Export
44854499
{
44864500
.code = 280,

src/quick-lint-js/diag/diagnostic-metadata-generated.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ namespace quick_lint_js {
309309
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Global_Block_Not_Allowed_In_JavaScript) \
310310
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Global_Block_Not_Allowed_In_Namespace) \
311311
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Type_Export_Not_Allowed_In_JavaScript) \
312+
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type) \
312313
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Inline_Type_Export_Not_Allowed_In_Type_Only_Export) \
313314
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Inline_Type_Import_Not_Allowed_In_Type_Only_Import) \
314315
QLJS_DIAG_TYPE_NAME(Diag_TypeScript_Interfaces_Cannot_Contain_Static_Blocks) \
@@ -446,7 +447,7 @@ namespace quick_lint_js {
446447
/* END */
447448
// clang-format on
448449

449-
inline constexpr int Diag_Type_Count = 432;
450+
inline constexpr int Diag_Type_Count = 433;
450451

451452
extern const Diagnostic_Info all_diagnostic_infos[Diag_Type_Count];
452453
}

src/quick-lint-js/diag/diagnostic-types-2.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2318,6 +2318,13 @@ struct Diag_TypeScript_Type_Export_Not_Allowed_In_JavaScript {
23182318
Source_Code_Span type_keyword;
23192319
};
23202320

2321+
struct Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type {
2322+
[[qljs::diag("E0426", Diagnostic_Severity::error)]] //
2323+
[[qljs::message("type predicates are only allowed as function return types",
2324+
ARG(is_keyword))]] //
2325+
Source_Code_Span is_keyword;
2326+
};
2327+
23212328
struct Diag_TypeScript_Inline_Type_Export_Not_Allowed_In_Type_Only_Export {
23222329
[[qljs::diag("E0280", Diagnostic_Severity::error)]] //
23232330
[[qljs::message("'type' cannot be used twice in export",

src/quick-lint-js/fe/parse-expression.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2177,7 +2177,8 @@ Expression* Parser::parse_expression_remainder(Parse_Visitor_Base& v,
21772177
TypeScript_Type_Parse_Options{
21782178
.allow_parenthesized_type =
21792179
!is_possibly_arrow_function_return_type_annotation,
2180-
.allow_type_predicate = true,
2180+
.allow_type_predicate =
2181+
is_possibly_arrow_function_return_type_annotation,
21812182
});
21822183
const Char8* type_end = this->lexer_.end_of_previous_token();
21832184
binary_builder.replace_last(

src/quick-lint-js/fe/parse-type.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,21 @@ void Parser::parse_and_visit_typescript_type_expression(
219219
if (this->peek().type == Token_Type::kw_is) {
220220
// param is Type
221221
// this is Type
222+
Source_Code_Span is_keyword = this->peek().span();
222223
this->skip();
223224
if (name_type != Token_Type::kw_this) {
224225
// this is Type
225-
v.visit_variable_type_predicate_use(name);
226+
if (parse_options.allow_type_predicate) {
227+
v.visit_variable_type_predicate_use(name);
228+
} else {
229+
v.visit_variable_use(name);
230+
}
231+
}
232+
if (!parse_options.allow_type_predicate) {
233+
this->diag_reporter_->report(
234+
Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type{
235+
.is_keyword = is_keyword,
236+
});
226237
}
227238
this->parse_and_visit_typescript_type_expression(v);
228239
return;

src/quick-lint-js/i18n/translation-table-generated.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ const Translation_Table translation_data = {
469469
{50, 25, 0, 70, 0, 78}, //
470470
{33, 21, 74, 25, 44, 21}, //
471471
{0, 0, 0, 0, 0, 26}, //
472+
{0, 0, 0, 0, 0, 58}, //
472473
{27, 19, 30, 29, 22, 31}, //
473474
{25, 50, 0, 36, 0, 23}, //
474475
{66, 43, 31, 36, 30, 44}, //
@@ -2268,6 +2269,7 @@ const Translation_Table translation_data = {
22682269
u8"this tuple type is a named tuple type because at least one element has a name\0"
22692270
u8"this {0} looks fishy\0"
22702271
u8"try statement starts here\0"
2272+
u8"type predicates are only allowed as function return types\0"
22712273
u8"type {1} is being defined here\0"
22722274
u8"unclosed block comment\0"
22732275
u8"unclosed class; expected '}' by end of file\0"

src/quick-lint-js/i18n/translation-table-generated.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ namespace quick_lint_js {
1818
using namespace std::literals::string_view_literals;
1919

2020
constexpr std::uint32_t translation_table_locale_count = 5;
21-
constexpr std::uint16_t translation_table_mapping_table_size = 529;
22-
constexpr std::size_t translation_table_string_table_size = 80304;
21+
constexpr std::uint16_t translation_table_mapping_table_size = 530;
22+
constexpr std::size_t translation_table_string_table_size = 80362;
2323
constexpr std::size_t translation_table_locale_table_size = 35;
2424

2525
QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
@@ -483,6 +483,7 @@ QLJS_CONSTEVAL std::uint16_t translation_table_const_look_up(
483483
"this tuple type is a named tuple type because at least one element has a name"sv,
484484
"this {0} looks fishy"sv,
485485
"try statement starts here"sv,
486+
"type predicates are only allowed as function return types"sv,
486487
"type {1} is being defined here"sv,
487488
"unclosed block comment"sv,
488489
"unclosed class; expected '}' by end of file"sv,

src/quick-lint-js/i18n/translation-table-test-generated.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct Translated_String {
2727
};
2828

2929
// clang-format off
30-
inline const Translated_String test_translation_table[528] = {
30+
inline const Translated_String test_translation_table[529] = {
3131
{
3232
"\"global-groups\" entries must be strings"_translatable,
3333
u8"\"global-groups\" entries must be strings",
@@ -5055,6 +5055,17 @@ inline const Translated_String test_translation_table[528] = {
50555055
u8"try sats startar h\u00e4r",
50565056
},
50575057
},
5058+
{
5059+
"type predicates are only allowed as function return types"_translatable,
5060+
u8"type predicates are only allowed as function return types",
5061+
{
5062+
u8"type predicates are only allowed as function return types",
5063+
u8"type predicates are only allowed as function return types",
5064+
u8"type predicates are only allowed as function return types",
5065+
u8"type predicates are only allowed as function return types",
5066+
u8"type predicates are only allowed as function return types",
5067+
},
5068+
},
50585069
{
50595070
"type {1} is being defined here"_translatable,
50605071
u8"type {1} is being defined here",

test/test-parse-typescript-function.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,41 @@ TEST_F(Test_Parse_TypeScript_Function, type_predicate_on_generator_function) {
991991
}
992992
}
993993

994+
TEST_F(Test_Parse_TypeScript_Function,
995+
type_predicate_is_only_allowed_as_return_type) {
996+
{
997+
Spy_Visitor p = test_parse_and_visit_statement(
998+
u8"function f(p, q: p is SomeType) {}"_sv, //
999+
u8" ^^ Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type"_diag,
1000+
typescript_options);
1001+
EXPECT_THAT(p.visits, ElementsAreArray({
1002+
"visit_enter_function_scope", //
1003+
"visit_variable_declaration", // p
1004+
"visit_variable_use", // p
1005+
"visit_variable_type_use", // SomeType
1006+
"visit_variable_declaration", // q
1007+
"visit_enter_function_scope_body", // {
1008+
"visit_exit_function_scope", // }
1009+
"visit_variable_declaration", // f
1010+
}));
1011+
EXPECT_THAT(p.variable_uses, ElementsAreArray({u8"p", u8"SomeType"}));
1012+
}
1013+
1014+
{
1015+
test_parse_and_visit_statement(
1016+
u8"var x: p is T;"_sv, //
1017+
u8" ^^ Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type"_diag,
1018+
typescript_options);
1019+
}
1020+
1021+
{
1022+
test_parse_and_visit_statement(
1023+
u8"function f(p): p is p is T {}"_sv, //
1024+
u8" ^^ Diag_TypeScript_Type_Predicate_Only_Allowed_As_Return_Type"_diag,
1025+
typescript_options);
1026+
}
1027+
}
1028+
9941029
TEST_F(Test_Parse_TypeScript_Function, type_predicate_in_type) {
9951030
{
9961031
Spy_Visitor p = test_parse_and_visit_typescript_type_expression(

0 commit comments

Comments
 (0)