Skip to content

Commit f0ef6e1

Browse files
committed
feat(typescript): parse 'import type A = require("A");'
1 parent f1e826a commit f0ef6e1

File tree

3 files changed

+41
-16
lines changed

3 files changed

+41
-16
lines changed

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ Semantic Versioning.
120120
assertion failure.
121121
* Subnamespaces can now be named contextual keywords such as `string`.
122122
* Import aliases can now be named contextual keywords such as `implements`.
123+
* Import aliases can now be declared with `import type`.
123124
* Namespace aliases can now reference variables named contextual keywords such as
124125
`yield` inside namespaces.
125126
* Type annotations can now reference types inside namespaces named contextual

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4536,7 +4536,8 @@ void Parser::parse_and_visit_import(
45364536
bool possibly_typescript_import_alias = false;
45374537
// For 'import fs from "node:fs";', declared_variable is 'fs'.
45384538
std::optional<Token> declared_variable = std::nullopt;
4539-
auto declare_variable_if_needed = [&](Variable_Kind kind) {
4539+
Variable_Kind declared_variable_kind = Variable_Kind::_import;
4540+
auto declare_variable_if_needed = [&]() {
45404541
if (declared_variable.has_value()) {
45414542
switch (declared_variable->type) {
45424543
// import var from "module"; // Invalid.
@@ -4552,7 +4553,7 @@ void Parser::parse_and_visit_import(
45524553
case Token_Type::kw_await:
45534554
case Token_Type::kw_yield:
45544555
case Token_Type::kw_let:
4555-
if (kind == Variable_Kind::_import_alias) {
4556+
if (declared_variable_kind == Variable_Kind::_import_alias) {
45564557
// NOTE[TypeScript-namespace-alias-name]: TypeScript namespace aliases
45574558
// can be named 'await' because TypeScript namespace aliases do not
45584559
// enforce strict mode.
@@ -4578,7 +4579,8 @@ void Parser::parse_and_visit_import(
45784579
break;
45794580
}
45804581

4581-
v.visit_variable_declaration(declared_variable->identifier_name(), kind,
4582+
v.visit_variable_declaration(declared_variable->identifier_name(),
4583+
declared_variable_kind,
45824584
Variable_Declaration_Flags::none);
45834585
declared_variable = std::nullopt; // Prevent double declaration.
45844586
}
@@ -4616,7 +4618,7 @@ void Parser::parse_and_visit_import(
46164618
declared_variable = this->peek();
46174619
this->skip();
46184620
if (this->peek().type == Token_Type::comma) {
4619-
declare_variable_if_needed(Variable_Kind::_import);
4621+
declare_variable_if_needed();
46204622
this->skip();
46214623
switch (this->peek().type) {
46224624
// import fs, {readFile} from "fs";
@@ -4708,9 +4710,8 @@ void Parser::parse_and_visit_import(
47084710
case Token_Type::kw_type:
47094711
this->lexer_.commit_transaction(std::move(transaction));
47104712
report_type_only_import_in_javascript_if_needed();
4711-
v.visit_variable_declaration(this->peek().identifier_name(),
4712-
Variable_Kind::_import_type,
4713-
Variable_Declaration_Flags::none);
4713+
declared_variable = this->peek();
4714+
declared_variable_kind = Variable_Kind::_import_type;
47144715
this->skip();
47154716
if (this->peek().type == Token_Type::comma) {
47164717
this->skip();
@@ -4739,6 +4740,8 @@ void Parser::parse_and_visit_import(
47394740
QLJS_PARSER_UNIMPLEMENTED();
47404741
break;
47414742
}
4743+
} else {
4744+
possibly_typescript_import_alias = true;
47424745
}
47434746
break;
47444747

@@ -4772,12 +4775,12 @@ void Parser::parse_and_visit_import(
47724775

47734776
switch (this->peek().type) {
47744777
case Token_Type::kw_from:
4775-
declare_variable_if_needed(Variable_Kind::_import);
4778+
declare_variable_if_needed();
47764779
this->skip();
47774780
break;
47784781

47794782
case Token_Type::string:
4780-
declare_variable_if_needed(Variable_Kind::_import);
4783+
declare_variable_if_needed();
47814784
this->diag_reporter_->report(Diag_Expected_From_Before_Module_Specifier{
47824785
.module_specifier = this->peek().span(),
47834786
});
@@ -4805,9 +4808,16 @@ void Parser::parse_and_visit_import(
48054808
if (this->peek().type == Token_Type::left_paren &&
48064809
namespace_name.normalized_name() == u8"require"_sv) {
48074810
// import fs = require("fs");
4811+
// import type fs = require("fs");
4812+
4813+
// NOTE[TypeScript-type-import-alias]: 'import fs = ' and
4814+
// 'import type fs = ...' both declare variables which conflict with
4815+
// 'let', 'class', etc. Overwrite Variable_Kind::_import_type.
4816+
//
48084817
// FIXME(strager): Should this behave like an import or an import
48094818
// alias or some other kind?
4810-
declare_variable_if_needed(Variable_Kind::_import);
4819+
declared_variable_kind = Variable_Kind::_import;
4820+
declare_variable_if_needed();
48114821
if (declare_context.declare_namespace_declare_keyword.has_value() &&
48124822
!declare_context.in_module) {
48134823
this->diag_reporter_->report(
@@ -4826,7 +4836,8 @@ void Parser::parse_and_visit_import(
48264836
} else {
48274837
// import myns = ns;
48284838
// import C = ns.C;
4829-
declare_variable_if_needed(Variable_Kind::_import_alias);
4839+
declared_variable_kind = Variable_Kind::_import_alias;
4840+
declare_variable_if_needed();
48304841
v.visit_variable_namespace_use(namespace_name);
48314842
while (this->peek().type == Token_Type::dot) {
48324843
this->skip();
@@ -4849,7 +4860,7 @@ void Parser::parse_and_visit_import(
48494860
}
48504861

48514862
default:
4852-
declare_variable_if_needed(Variable_Kind::_import);
4863+
declare_variable_if_needed();
48534864
QLJS_PARSER_UNIMPLEMENTED();
48544865
break;
48554866
}
@@ -4858,10 +4869,10 @@ void Parser::parse_and_visit_import(
48584869
return;
48594870
}
48604871

4861-
declare_variable_if_needed(Variable_Kind::_import);
4872+
declare_variable_if_needed();
48624873
[[fallthrough]];
48634874
default:
4864-
declare_variable_if_needed(Variable_Kind::_import);
4875+
declare_variable_if_needed();
48654876
this->diag_reporter_->report(Diag_Expected_From_And_Module_Specifier{
48664877
.where = Source_Code_Span::unit(this->lexer_.end_of_previous_token()),
48674878
});

test/test-parse-typescript-module.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ TEST_F(Test_Parse_TypeScript_Module,
122122
}));
123123
EXPECT_THAT(
124124
p.variable_declarations,
125-
ElementsAreArray({import_type_decl(u8"A"_sv), import_decl(u8"B"_sv)}))
125+
ElementsAreArray({import_decl(u8"B"_sv), import_type_decl(u8"A"_sv)}))
126126
<< "B should be imported as an 'import' not an 'import_type' in case "
127127
"the user thought that the 'type' keyword only applied to 'A'";
128128
assert_diagnostics(
@@ -144,7 +144,7 @@ TEST_F(Test_Parse_TypeScript_Module,
144144
}));
145145
EXPECT_THAT(
146146
p.variable_declarations,
147-
ElementsAreArray({import_type_decl(u8"A"_sv), import_decl(u8"B"_sv)}));
147+
ElementsAreArray({import_decl(u8"B"_sv), import_type_decl(u8"A"_sv)}));
148148
}
149149
}
150150

@@ -420,6 +420,19 @@ TEST_F(Test_Parse_TypeScript_Module, import_require) {
420420
EXPECT_THAT(p.variable_declarations,
421421
ElementsAreArray({import_decl(u8"fs"_sv)}));
422422
}
423+
424+
{
425+
Spy_Visitor p =
426+
test_parse_and_visit_module(u8"import type fs = require('node:fs');"_sv,
427+
no_diags, typescript_options);
428+
EXPECT_THAT(p.visits, ElementsAreArray({
429+
"visit_variable_declaration", // fs
430+
"visit_end_of_module",
431+
}));
432+
EXPECT_THAT(p.variable_declarations,
433+
ElementsAreArray({import_decl(u8"fs"_sv)}))
434+
<< "See NOTE[TypeScript-type-import-alias].";
435+
}
423436
}
424437

425438
TEST_F(Test_Parse_TypeScript_Module,

0 commit comments

Comments
 (0)