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 17 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";
50 changes: 40 additions & 10 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 @@ -2168,15 +2169,22 @@ export default class StatementParser extends ExpressionParser {
}

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.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..)

const canParseAsKeyword = this.parseTypeOnlyImportExportSpecifier(
node,
/* isImport */ false,
isString,
isInTypeExport,
isMaybeTypeOnly,
);
if (canParseAsKeyword && this.eatContextual(tt._as)) {
node.exported = this.parseModuleExportName();
} else if (isString) {
node.exported = cloneStringLiteral(local);
} else {
node.exported = cloneIdentifier(local);
node.exported = cloneStringLiteral(node.local);
} else if (!node.exported) {
node.exported = cloneIdentifier(node.local);
}
nodes.push(this.finishNode(node, "ExportSpecifier"));
}
Expand Down Expand Up @@ -2436,12 +2444,32 @@ export default class StatementParser extends ExpressionParser {
}
}

parseTypeOnlyImportExportSpecifier(
/* eslint-disable no-unused-vars -- used in typescript plugin */
node: any,
isImport: boolean,
isStringSpecifier: boolean,
isInTypeOnlyImportExport: boolean,
isMaybeTypeOnly: boolean,
/* eslint-enable no-unused-vars */
): boolean {
return true;
}

// https://tc39.es/ecma262/#prod-ImportSpecifier
parseImportSpecifier(node: N.ImportDeclaration): void {
const specifier = this.startNode();
const importedIsString = this.match(tt.string);
const isMaybeTypeOnly = this.isContextual(tt._type);
specifier.imported = this.parseModuleExportName();
if (this.eatContextual(tt._as)) {
const canParseAsKeyword = this.parseTypeOnlyImportExportSpecifier(
specifier,
/* isImport */ true,
importedIsString,
/* isInTypeOnlyImportExport */ node.importKind === "type",
isMaybeTypeOnly,
);
if (canParseAsKeyword && this.eatContextual(tt._as)) {
specifier.local = this.parseIdentifier();
} else {
const { imported } = specifier;
Expand All @@ -2453,7 +2481,9 @@ 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"));
Expand Down
4 changes: 3 additions & 1 deletion 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
79 changes: 79 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 @@ -147,6 +148,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 +3316,78 @@ export default (superClass: Class<Parser>): Class<Parser> =>
}
return super.getExpression();
}

parseTypeOnlyImportExportSpecifier(
node: any,
isImport: boolean,
isStringSpecifier: boolean,
isInTypeOnlyImportExport: boolean,
isMaybeTypeOnly: boolean,
): boolean {
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
if (!isStringSpecifier && isMaybeTypeOnly) {
// 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();
Copy link
Contributor

Choose a reason for hiding this comment

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

When we are parsing export, the right-of-as is a liberal identifier (should be parsed by parseIdentifier(true)), otherwise it should throw unexpected reserved word:

export { type a as if } from "x"; // valid
import { type a as if } from "x"; // invalid

Copy link
Member Author

Choose a reason for hiding this comment

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

We can use parseIdentifier(true/false) for those specifiers. But Babel doesn't check reserved word for TypeScript. Should we still fix for that?

checkReservedWord(
word: string, // eslint-disable-line no-unused-vars
startLoc: number, // eslint-disable-line no-unused-vars
checkKeywords: boolean, // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
isBinding: boolean,
): void {
// Don't bother checking for TypeScript code.
// Strict mode words may be allowed as in `declare namespace N { const static: number; }`.
// And we have a type checker anyway, so don't bother having the parser do it.
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, let's fix that in another PR, then.

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();
}
}
node[leftOfAsKey] = leftOfAs;
node[rightOfAsKey] = rightOfAs;

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

Choose a reason for hiding this comment

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

Can you also update packages/babel-parser/src/types.js?

Copy link
Member Author

Choose a reason for hiding this comment

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

c44171a 👍


if (hasTypeSpecifier && isInTypeOnlyImportExport) {
this.raise(
pos,
isImport
? TSErrors.TypeModifierIsUsedInTypeImports
: TSErrors.TypeModifierIsUsedInTypeExports,
);
}

return canParseAsKeyword;
}
};
Expand Up @@ -20,6 +20,7 @@
"start":14,"end":15,"loc":{"start":{"line":1,"column":14},"end":{"line":1,"column":15},"identifierName":"T"},
"name": "T"
},
"exportKind": "value",
"exported": {
"type": "Identifier",
"start":14,"end":15,"loc":{"start":{"line":1,"column":14},"end":{"line":1,"column":15},"identifierName":"T"},
Expand Down
Expand Up @@ -42,6 +42,7 @@
"start":26,"end":27,"loc":{"start":{"line":2,"column":14},"end":{"line":2,"column":15},"identifierName":"A"},
"name": "A"
},
"exportKind": "value",
"exported": {
"type": "Identifier",
"start":26,"end":27,"loc":{"start":{"line":2,"column":14},"end":{"line":2,"column":15},"identifierName":"A"},
Expand Down