Skip to content

Commit

Permalink
feat(typescript): parse assertion signatures
Browse files Browse the repository at this point in the history
  • Loading branch information
strager committed Nov 21, 2023
1 parent 0db3c80 commit 2f5d134
Show file tree
Hide file tree
Showing 15 changed files with 342 additions and 37 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions po/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
30 changes: 29 additions & 1 deletion src/quick-lint-js/diag/diagnostic-metadata-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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'"),
},
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 3 additions & 1 deletion src/quick-lint-js/diag/diagnostic-metadata-generated.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down Expand Up @@ -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) \
Expand Down Expand Up @@ -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];
}
Expand Down
16 changes: 15 additions & 1 deletion src/quick-lint-js/diag/diagnostic-types-2.h
Original file line number Diff line number Diff line change
Expand Up @@ -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))]] //
Expand Down Expand Up @@ -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))]] //
Expand Down
38 changes: 21 additions & 17 deletions src/quick-lint-js/fe/parse-expression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
// <T>(param) => body
Expand Down
2 changes: 1 addition & 1 deletion src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}

Expand Down
69 changes: 61 additions & 8 deletions src/quick-lint-js/fe/parse-type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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,
});
}

Expand Down
5 changes: 3 additions & 2 deletions src/quick-lint-js/fe/parse.h
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,9 @@ class Parser {
std::optional<Declaring_Type> 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);
Expand Down
13 changes: 11 additions & 2 deletions src/quick-lint-js/fe/variable-analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 &current_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(
Expand Down
4 changes: 3 additions & 1 deletion src/quick-lint-js/i18n/translation-table-generated.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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}, //
Expand Down Expand Up @@ -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"
Expand Down
Loading

0 comments on commit 2f5d134

Please sign in to comment.