Skip to content

Commit

Permalink
feat(typescript): parse 'import type A = require("A");'
Browse files Browse the repository at this point in the history
  • Loading branch information
strager committed Dec 30, 2023
1 parent f1e826a commit f0ef6e1
Show file tree
Hide file tree
Showing 3 changed files with 41 additions and 16 deletions.
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ Semantic Versioning.
assertion failure.
* Subnamespaces can now be named contextual keywords such as `string`.
* Import aliases can now be named contextual keywords such as `implements`.
* Import aliases can now be declared with `import type`.
* Namespace aliases can now reference variables named contextual keywords such as
`yield` inside namespaces.
* Type annotations can now reference types inside namespaces named contextual
Expand Down
39 changes: 25 additions & 14 deletions src/quick-lint-js/fe/parse-statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4536,7 +4536,8 @@ void Parser::parse_and_visit_import(
bool possibly_typescript_import_alias = false;
// For 'import fs from "node:fs";', declared_variable is 'fs'.
std::optional<Token> declared_variable = std::nullopt;
auto declare_variable_if_needed = [&](Variable_Kind kind) {
Variable_Kind declared_variable_kind = Variable_Kind::_import;
auto declare_variable_if_needed = [&]() {
if (declared_variable.has_value()) {
switch (declared_variable->type) {
// import var from "module"; // Invalid.
Expand All @@ -4552,7 +4553,7 @@ void Parser::parse_and_visit_import(
case Token_Type::kw_await:
case Token_Type::kw_yield:
case Token_Type::kw_let:
if (kind == Variable_Kind::_import_alias) {
if (declared_variable_kind == Variable_Kind::_import_alias) {
// NOTE[TypeScript-namespace-alias-name]: TypeScript namespace aliases
// can be named 'await' because TypeScript namespace aliases do not
// enforce strict mode.
Expand All @@ -4578,7 +4579,8 @@ void Parser::parse_and_visit_import(
break;
}

v.visit_variable_declaration(declared_variable->identifier_name(), kind,
v.visit_variable_declaration(declared_variable->identifier_name(),
declared_variable_kind,
Variable_Declaration_Flags::none);
declared_variable = std::nullopt; // Prevent double declaration.
}
Expand Down Expand Up @@ -4616,7 +4618,7 @@ void Parser::parse_and_visit_import(
declared_variable = this->peek();
this->skip();
if (this->peek().type == Token_Type::comma) {
declare_variable_if_needed(Variable_Kind::_import);
declare_variable_if_needed();
this->skip();
switch (this->peek().type) {
// import fs, {readFile} from "fs";
Expand Down Expand Up @@ -4708,9 +4710,8 @@ void Parser::parse_and_visit_import(
case Token_Type::kw_type:
this->lexer_.commit_transaction(std::move(transaction));
report_type_only_import_in_javascript_if_needed();
v.visit_variable_declaration(this->peek().identifier_name(),
Variable_Kind::_import_type,
Variable_Declaration_Flags::none);
declared_variable = this->peek();
declared_variable_kind = Variable_Kind::_import_type;
this->skip();
if (this->peek().type == Token_Type::comma) {
this->skip();
Expand Down Expand Up @@ -4739,6 +4740,8 @@ void Parser::parse_and_visit_import(
QLJS_PARSER_UNIMPLEMENTED();
break;
}
} else {
possibly_typescript_import_alias = true;
}
break;

Expand Down Expand Up @@ -4772,12 +4775,12 @@ void Parser::parse_and_visit_import(

switch (this->peek().type) {
case Token_Type::kw_from:
declare_variable_if_needed(Variable_Kind::_import);
declare_variable_if_needed();
this->skip();
break;

case Token_Type::string:
declare_variable_if_needed(Variable_Kind::_import);
declare_variable_if_needed();
this->diag_reporter_->report(Diag_Expected_From_Before_Module_Specifier{
.module_specifier = this->peek().span(),
});
Expand Down Expand Up @@ -4805,9 +4808,16 @@ void Parser::parse_and_visit_import(
if (this->peek().type == Token_Type::left_paren &&
namespace_name.normalized_name() == u8"require"_sv) {
// import fs = require("fs");
// import type fs = require("fs");

// NOTE[TypeScript-type-import-alias]: 'import fs = ' and
// 'import type fs = ...' both declare variables which conflict with
// 'let', 'class', etc. Overwrite Variable_Kind::_import_type.
//
// FIXME(strager): Should this behave like an import or an import
// alias or some other kind?
declare_variable_if_needed(Variable_Kind::_import);
declared_variable_kind = Variable_Kind::_import;
declare_variable_if_needed();
if (declare_context.declare_namespace_declare_keyword.has_value() &&
!declare_context.in_module) {
this->diag_reporter_->report(
Expand All @@ -4826,7 +4836,8 @@ void Parser::parse_and_visit_import(
} else {
// import myns = ns;
// import C = ns.C;
declare_variable_if_needed(Variable_Kind::_import_alias);
declared_variable_kind = Variable_Kind::_import_alias;
declare_variable_if_needed();
v.visit_variable_namespace_use(namespace_name);
while (this->peek().type == Token_Type::dot) {
this->skip();
Expand All @@ -4849,7 +4860,7 @@ void Parser::parse_and_visit_import(
}

default:
declare_variable_if_needed(Variable_Kind::_import);
declare_variable_if_needed();
QLJS_PARSER_UNIMPLEMENTED();
break;
}
Expand All @@ -4858,10 +4869,10 @@ void Parser::parse_and_visit_import(
return;
}

declare_variable_if_needed(Variable_Kind::_import);
declare_variable_if_needed();
[[fallthrough]];
default:
declare_variable_if_needed(Variable_Kind::_import);
declare_variable_if_needed();
this->diag_reporter_->report(Diag_Expected_From_And_Module_Specifier{
.where = Source_Code_Span::unit(this->lexer_.end_of_previous_token()),
});
Expand Down
17 changes: 15 additions & 2 deletions test/test-parse-typescript-module.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ TEST_F(Test_Parse_TypeScript_Module,
}));
EXPECT_THAT(
p.variable_declarations,
ElementsAreArray({import_type_decl(u8"A"_sv), import_decl(u8"B"_sv)}))
ElementsAreArray({import_decl(u8"B"_sv), import_type_decl(u8"A"_sv)}))
<< "B should be imported as an 'import' not an 'import_type' in case "
"the user thought that the 'type' keyword only applied to 'A'";
assert_diagnostics(
Expand All @@ -144,7 +144,7 @@ TEST_F(Test_Parse_TypeScript_Module,
}));
EXPECT_THAT(
p.variable_declarations,
ElementsAreArray({import_type_decl(u8"A"_sv), import_decl(u8"B"_sv)}));
ElementsAreArray({import_decl(u8"B"_sv), import_type_decl(u8"A"_sv)}));
}
}

Expand Down Expand Up @@ -420,6 +420,19 @@ TEST_F(Test_Parse_TypeScript_Module, import_require) {
EXPECT_THAT(p.variable_declarations,
ElementsAreArray({import_decl(u8"fs"_sv)}));
}

{
Spy_Visitor p =
test_parse_and_visit_module(u8"import type fs = require('node:fs');"_sv,
no_diags, typescript_options);
EXPECT_THAT(p.visits, ElementsAreArray({
"visit_variable_declaration", // fs
"visit_end_of_module",
}));
EXPECT_THAT(p.variable_declarations,
ElementsAreArray({import_decl(u8"fs"_sv)}))
<< "See NOTE[TypeScript-type-import-alias].";
}
}

TEST_F(Test_Parse_TypeScript_Module,
Expand Down

0 comments on commit f0ef6e1

Please sign in to comment.