Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support TypeScript 4.5 type-only import/export specifiers #13802

Merged
merged 27 commits into from Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9b91e84
Implement parser
sosukesuzuki Sep 30, 2021
1b20596
Add parser tests
sosukesuzuki Sep 30, 2021
8daf3bc
Implement generator
sosukesuzuki Sep 30, 2021
16b4bc1
Add generator tests
sosukesuzuki Sep 30, 2021
a2edbe3
Implement types
sosukesuzuki Sep 30, 2021
6421008
Implement plugin-transform-typescript
sosukesuzuki Sep 30, 2021
6a596b9
Add plugin-transform-typescript tests
sosukesuzuki Sep 30, 2021
4929209
Remove imported-type from exports
sosukesuzuki Oct 2, 2021
d337f69
Use string literal instead of variable
sosukesuzuki Oct 11, 2021
8cb6dc0
Use set instead of array
sosukesuzuki Oct 11, 2021
4d1cde6
Keep import statement if all of specifiers are type-only one
sosukesuzuki Oct 11, 2021
05d5183
Rename isTypeOnly to hasTypeSpecifier
sosukesuzuki Oct 13, 2021
31b273c
Use isContextual instead of string comparison
sosukesuzuki Oct 15, 2021
ec6398e
Use tokenIsKeywordOrIdentifier instead of match
sosukesuzuki Oct 15, 2021
6b79fd1
Remove import statement if all of specifiers are type-only one
sosukesuzuki Oct 16, 2021
72b3c3b
Merge branch 'main' into type-only-import-specifier
sosukesuzuki Oct 22, 2021
ebf0a58
Fix type errors
sosukesuzuki Oct 22, 2021
d5c69eb
Merge branch 'main' into type-only-import-specifier
sosukesuzuki Oct 25, 2021
d459c8b
Extract parseExportSpecifier
sosukesuzuki Oct 26, 2021
519f977
Extract parameters
sosukesuzuki Oct 28, 2021
cb0f943
Move the logic for error
sosukesuzuki Oct 28, 2021
91918b7
Refactor exports
sosukesuzuki Oct 28, 2021
d074010
Refactor imports
sosukesuzuki Oct 28, 2021
ab1f5c2
Fix lint comments
sosukesuzuki Oct 28, 2021
c44171a
Fix babel-parser/types.js
sosukesuzuki Oct 28, 2021
bfd0f9d
Throw error for string local for import
sosukesuzuki Oct 28, 2021
1f933af
check lval
sosukesuzuki Oct 28, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -1872,7 +1872,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 @@ -2152,7 +2153,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 @@ -2166,24 +2167,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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to avoid mixing TS/flow-specific code in the main parser, when possible.

Would it be possible to move type handling in parseModuleExportName()? Or maybe we reorganize it like this:

parseExportSpecifiers() {
  while () { // loop to parse all the specifiers
    const local = this.parseModuleExportName();
    nodes.push(this.parseExportSpecifier(local));
  }
}

parseExportSpecifier(local) {
  // ...
}

and in typescript/index.js

parseExportSpecifier(local) {
  if (local.type === "Identifier" && local.name === "type") {
    const node = this.startNodeAtNode(local);
    // if this is a type export, parse it as such
    return node;
  }
  return super.parseExprtSpecifier(local);
}

This is similar to what we do for imports.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little different from the interface you suggested, but I modified it based on that. Please review again.

commits:

(Sorry for many commits..)

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 @@ -2432,15 +2450,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 @@ -2453,10 +2485,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 @@ -147,6 +149,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 @@ -3311,4 +3317,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