Skip to content

Commit

Permalink
Support TypeScript 4.5 type-only import/export specifiers (#13802)
Browse files Browse the repository at this point in the history
  • Loading branch information
sosukesuzuki committed Oct 28, 2021
1 parent 872086a commit d5ba355
Show file tree
Hide file tree
Showing 90 changed files with 1,342 additions and 57 deletions.
5 changes: 5 additions & 0 deletions packages/babel-generator/src/generators/modules.ts
Expand Up @@ -40,6 +40,11 @@ export function ExportDefaultSpecifier(
}

export function ExportSpecifier(this: Printer, node: t.ExportSpecifier) {
if (node.exportKind === "type") {
this.word("type");
this.space();
}

this.print(node.local, node);
// @ts-expect-error todo(flow-ts) maybe check node type instead of relying on name to be undefined on t.StringLiteral
if (node.exported && node.local.name !== node.exported.name) {
Expand Down
@@ -0,0 +1 @@
export { type foo } from "foo";
@@ -0,0 +1 @@
export { type foo } from "foo";
@@ -0,0 +1 @@
import { type foo } from "foo";
@@ -0,0 +1 @@
import { type foo } from "foo";
76 changes: 55 additions & 21 deletions packages/babel-parser/src/parser/statement.js
Expand Up @@ -1878,7 +1878,8 @@ export default class StatementParser extends ExpressionParser {
maybeParseExportNamedSpecifiers(node: N.Node): boolean {
if (this.match(tt.braceL)) {
if (!node.specifiers) node.specifiers = [];
node.specifiers.push(...this.parseExportSpecifiers());
const isTypeExport = node.exportKind === "type";
node.specifiers.push(...this.parseExportSpecifiers(isTypeExport));

node.source = null;
node.declaration = null;
Expand Down Expand Up @@ -2158,7 +2159,7 @@ export default class StatementParser extends ExpressionParser {

// Parses a comma-separated list of module exports.

parseExportSpecifiers(): Array<N.ExportSpecifier> {
parseExportSpecifiers(isInTypeExport: boolean): Array<N.ExportSpecifier> {
const nodes = [];
let first = true;

Expand All @@ -2172,24 +2173,41 @@ export default class StatementParser extends ExpressionParser {
this.expect(tt.comma);
if (this.eat(tt.braceR)) break;
}

const node = this.startNode();
const isMaybeTypeOnly = this.isContextual(tt._type);
const isString = this.match(tt.string);
const local = this.parseModuleExportName();
node.local = local;
if (this.eatContextual(tt._as)) {
node.exported = this.parseModuleExportName();
} else if (isString) {
node.exported = cloneStringLiteral(local);
} else {
node.exported = cloneIdentifier(local);
}
nodes.push(this.finishNode(node, "ExportSpecifier"));
const node = this.startNode();
node.local = this.parseModuleExportName();
nodes.push(
this.parseExportSpecifier(
node,
isString,
isInTypeExport,
isMaybeTypeOnly,
),
);
}

return nodes;
}

parseExportSpecifier(
node: any,
isString: boolean,
/* eslint-disable no-unused-vars -- used in TypeScript parser */
isInTypeExport: boolean,
isMaybeTypeOnly: boolean,
/* eslint-enable no-unused-vars */
): N.ExportSpecifier {
if (this.eatContextual(tt._as)) {
node.exported = this.parseModuleExportName();
} else if (isString) {
node.exported = cloneStringLiteral(node.local);
} else if (!node.exported) {
node.exported = cloneIdentifier(node.local);
}
return this.finishNode<N.ExportSpecifier>(node, "ExportSpecifier");
}

// https://tc39.es/ecma262/#prod-ModuleExportName
parseModuleExportName(): N.StringLiteral | N.Identifier {
if (this.match(tt.string)) {
Expand Down Expand Up @@ -2438,15 +2456,29 @@ export default class StatementParser extends ExpressionParser {
if (this.eat(tt.braceR)) break;
}

this.parseImportSpecifier(node);
const specifier = this.startNode();
const importedIsString = this.match(tt.string);
const isMaybeTypeOnly = this.isContextual(tt._type);
specifier.imported = this.parseModuleExportName();
const importSpecifier = this.parseImportSpecifier(
specifier,
importedIsString,
node.importKind === "type" || node.importKind === "typeof",
isMaybeTypeOnly,
);
node.specifiers.push(importSpecifier);
}
}

// https://tc39.es/ecma262/#prod-ImportSpecifier
parseImportSpecifier(node: N.ImportDeclaration): void {
const specifier = this.startNode();
const importedIsString = this.match(tt.string);
specifier.imported = this.parseModuleExportName();
parseImportSpecifier(
specifier: any,
importedIsString: boolean,
/* eslint-disable no-unused-vars -- used in TypeScript and Flow parser */
isInTypeOnlyImport: boolean,
isMaybeTypeOnly: boolean,
/* eslint-enable no-unused-vars */
): N.ImportSpecifier {
if (this.eatContextual(tt._as)) {
specifier.local = this.parseIdentifier();
} else {
Expand All @@ -2459,10 +2491,12 @@ export default class StatementParser extends ExpressionParser {
);
}
this.checkReservedWord(imported.name, specifier.start, true, true);
specifier.local = cloneIdentifier(imported);
if (!specifier.local) {
specifier.local = cloneIdentifier(imported);
}
}
this.checkLVal(specifier.local, "import specifier", BIND_LEXICAL);
node.specifiers.push(this.finishNode(specifier, "ImportSpecifier"));
return this.finishNode(specifier, "ImportSpecifier");
}

// This is used in flow and typescript plugin
Expand Down
27 changes: 16 additions & 11 deletions packages/babel-parser/src/plugins/flow/index.js
Expand Up @@ -2123,7 +2123,9 @@ export default (superClass: Class<Parser>): Class<Parser> =>

if (this.match(tt.braceL)) {
// export type { foo, bar };
node.specifiers = this.parseExportSpecifiers();
node.specifiers = this.parseExportSpecifiers(
/* isInTypeExport */ true,
);
this.parseExportFrom(node);
return null;
} else {
Expand Down Expand Up @@ -2629,10 +2631,14 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}

// parse import-type/typeof shorthand
parseImportSpecifier(node: N.ImportDeclaration): void {
const specifier = this.startNode();
const firstIdentIsString = this.match(tt.string);
const firstIdent = this.parseModuleExportName();
parseImportSpecifier(
specifier: any,
importedIsString: boolean,
isInTypeOnlyImport: boolean,
// eslint-disable-next-line no-unused-vars
isMaybeTypeOnly: boolean,
): N.ImportSpecifier {
const firstIdent = specifier.imported;

let specifierTypeKind = null;
if (firstIdent.type === "Identifier") {
Expand Down Expand Up @@ -2669,7 +2675,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
specifier.imported = this.parseIdentifier(true);
specifier.importKind = specifierTypeKind;
} else {
if (firstIdentIsString) {
if (importedIsString) {
/*:: invariant(firstIdent instanceof N.StringLiteral) */
throw this.raise(
specifier.start,
Expand All @@ -2690,25 +2696,24 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
}

const nodeIsTypeImport = hasTypeImportKind(node);
const specifierIsTypeImport = hasTypeImportKind(specifier);

if (nodeIsTypeImport && specifierIsTypeImport) {
if (isInTypeOnlyImport && specifierIsTypeImport) {
this.raise(
specifier.start,
FlowErrors.ImportTypeShorthandOnlyInPureImport,
);
}

if (nodeIsTypeImport || specifierIsTypeImport) {
if (isInTypeOnlyImport || specifierIsTypeImport) {
this.checkReservedType(
specifier.local.name,
specifier.local.start,
/* declaration */ true,
);
}

if (isBinding && !nodeIsTypeImport && !specifierIsTypeImport) {
if (isBinding && !isInTypeOnlyImport && !specifierIsTypeImport) {
this.checkReservedWord(
specifier.local.name,
specifier.start,
Expand All @@ -2718,7 +2723,7 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}

this.checkLVal(specifier.local, "import specifier", BIND_LEXICAL);
node.specifiers.push(this.finishNode(specifier, "ImportSpecifier"));
return this.finishNode(specifier, "ImportSpecifier");
}

parseBindingAtom(): N.Pattern {
Expand Down
132 changes: 132 additions & 0 deletions packages/babel-parser/src/plugins/typescript/index.js
Expand Up @@ -11,6 +11,7 @@ import {
tokenIsTSDeclarationStart,
tokenIsTSTypeOperator,
tokenOperatorPrecedence,
tokenIsKeywordOrIdentifier,
tt,
type TokenType,
} from "../../tokenizer/types";
Expand Down Expand Up @@ -41,6 +42,7 @@ import {
type ErrorTemplate,
ErrorCodes,
} from "../../parser/error";
import { cloneIdentifier } from "../../parser/node";

type TsModifier =
| "readonly"
Expand Down Expand Up @@ -151,6 +153,10 @@ const TSErrors = makeErrorTemplates(
"Type annotations must come before default assignments, e.g. instead of `age = 25: number` use `age: number = 25`.",
TypeImportCannotSpecifyDefaultAndNamed:
"A type-only import can specify a default import or named bindings, but not both.",
TypeModifierIsUsedInTypeExports:
"The 'type' modifier cannot be used on a named export when 'export type' is used on its export statement.",
TypeModifierIsUsedInTypeImports:
"The 'type' modifier cannot be used on a named import when 'import type' is used on its import statement.",
UnexpectedParameterModifier:
"A parameter property is only allowed in a constructor implementation.",
UnexpectedReadonly:
Expand Down Expand Up @@ -3368,4 +3374,130 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
return super.getExpression();
}

parseExportSpecifier(
node: any,
isString: boolean,
isInTypeExport: boolean,
isMaybeTypeOnly: boolean,
) {
if (!isString && isMaybeTypeOnly) {
this.parseTypeOnlyImportExportSpecifier(
node,
/* isImport */ false,
isInTypeExport,
);
return this.finishNode<N.ExportSpecifier>(node, "ExportSpecifier");
}
node.exportKind = "value";
return super.parseExportSpecifier(
node,
isString,
isInTypeExport,
isMaybeTypeOnly,
);
}

parseImportSpecifier(
specifier: any,
importedIsString: boolean,
isInTypeOnlyImport: boolean,
isMaybeTypeOnly: boolean,
): N.ImportSpecifier {
if (!importedIsString && isMaybeTypeOnly) {
this.parseTypeOnlyImportExportSpecifier(
specifier,
/* isImport */ true,
isInTypeOnlyImport,
);
return this.finishNode<N.ImportSpecifier>(specifier, "ImportSpecifier");
}
specifier.importKind = "value";
return super.parseImportSpecifier(
specifier,
importedIsString,
isInTypeOnlyImport,
isMaybeTypeOnly,
);
}

parseTypeOnlyImportExportSpecifier(
node: any,
isImport: boolean,
isInTypeOnlyImportExport: boolean,
): void {
const leftOfAsKey = isImport ? "imported" : "local";
const rightOfAsKey = isImport ? "local" : "exported";

let leftOfAs = node[leftOfAsKey];
let rightOfAs;

let hasTypeSpecifier = false;
let canParseAsKeyword = true;

const pos = leftOfAs.start;

// https://github.com/microsoft/TypeScript/blob/fc4f9d83d5939047aa6bb2a43965c6e9bbfbc35b/src/compiler/parser.ts#L7411-L7456
// import { type } from "mod"; - hasTypeSpecifier: false, leftOfAs: type
// import { type as } from "mod"; - hasTypeSpecifier: true, leftOfAs: as
// import { type as as } from "mod"; - hasTypeSpecifier: false, leftOfAs: type, rightOfAs: as
// import { type as as as } from "mod"; - hasTypeSpecifier: true, leftOfAs: as, rightOfAs: as
if (this.isContextual(tt._as)) {
// { type as ...? }
const firstAs = this.parseIdentifier();
if (this.isContextual(tt._as)) {
// { type as as ...? }
const secondAs = this.parseIdentifier();
if (tokenIsKeywordOrIdentifier(this.state.type)) {
// { type as as something }
hasTypeSpecifier = true;
leftOfAs = firstAs;
rightOfAs = this.parseIdentifier();
canParseAsKeyword = false;
} else {
// { type as as }
rightOfAs = secondAs;
canParseAsKeyword = false;
}
} else if (tokenIsKeywordOrIdentifier(this.state.type)) {
// { type as something }
canParseAsKeyword = false;
rightOfAs = this.parseIdentifier();
} else {
// { type as }
hasTypeSpecifier = true;
leftOfAs = firstAs;
}
} else if (tokenIsKeywordOrIdentifier(this.state.type)) {
// { type something ...? }
hasTypeSpecifier = true;
leftOfAs = this.parseIdentifier();
}
if (hasTypeSpecifier && isInTypeOnlyImportExport) {
this.raise(
pos,
isImport
? TSErrors.TypeModifierIsUsedInTypeImports
: TSErrors.TypeModifierIsUsedInTypeExports,
);
}

node[leftOfAsKey] = leftOfAs;
node[rightOfAsKey] = rightOfAs;

const kindKey = isImport ? "importKind" : "exportKind";
node[kindKey] = hasTypeSpecifier ? "type" : "value";

if (canParseAsKeyword && this.eatContextual(tt._as)) {
node[rightOfAsKey] = isImport
? this.parseIdentifier()
: this.parseModuleExportName();
}
if (!node[rightOfAsKey]) {
node[rightOfAsKey] = cloneIdentifier(node[leftOfAsKey]);
}
if (isImport) {
this.checkLVal(node[rightOfAsKey], "import specifier", BIND_LEXICAL);
}
}
};
2 changes: 2 additions & 0 deletions packages/babel-parser/src/types.js
Expand Up @@ -903,6 +903,7 @@ export type ImportDeclaration = NodeBase & {
export type ImportSpecifier = ModuleSpecifier & {
type: "ImportSpecifier",
imported: Identifier | StringLiteral,
importKind?: "type" | "value",
};

export type ImportDefaultSpecifier = ModuleSpecifier & {
Expand Down Expand Up @@ -930,6 +931,7 @@ export type ExportSpecifier = NodeBase & {
type: "ExportSpecifier",
exported: Identifier | StringLiteral,
local: Identifier,
exportKind?: "type" | "value",
};

export type ExportDefaultSpecifier = NodeBase & {
Expand Down

0 comments on commit d5ba355

Please sign in to comment.