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
Changes from 8 commits
9b91e84
1b20596
8daf3bc
16b4bc1
a2edbe3
6421008
6a596b9
4929209
d337f69
8cb6dc0
4d1cde6
05d5183
31b273c
ec6398e
6b79fd1
72b3c3b
ebf0a58
d5c69eb
d459c8b
519f977
cb0f943
91918b7
d074010
ab1f5c2
c44171a
bfd0f9d
1f933af
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { type foo } from "foo"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { type foo } from "foo"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import { type foo } from "foo"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import { type foo } from "foo"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
||
|
@@ -2169,14 +2170,19 @@ export default class StatementParser extends ExpressionParser { | |
|
||
const node = this.startNode(); | ||
const isString = this.match(tt.string); | ||
const local = this.parseModuleExportName(); | ||
node.local = local; | ||
if (this.eatContextual(tt._as)) { | ||
node.local = this.parseModuleExportName(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 parseExportSpecifiers() {
while () { // loop to parse all the specifiers
const local = this.parseModuleExportName();
nodes.push(this.parseExportSpecifier(local));
}
}
parseExportSpecifier(local) {
// ...
} and in 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
const canParseAsKeyword = this.parseTypeOnlyImportExportSpecifier( | ||
node, | ||
/* isImport */ false, | ||
isString, | ||
isInTypeExport, | ||
); | ||
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")); | ||
} | ||
|
@@ -2436,12 +2442,29 @@ export default class StatementParser extends ExpressionParser { | |
} | ||
} | ||
|
||
parseTypeOnlyImportExportSpecifier( | ||
/* eslint-disable no-unused-vars -- used in typescript plugin */ | ||
node: any, | ||
isImport: boolean, | ||
isStringSpecifier: boolean, | ||
isInTypeOnlyImportExport: 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); | ||
specifier.imported = this.parseModuleExportName(); | ||
if (this.eatContextual(tt._as)) { | ||
const canParseAsKeyworkd = this.parseTypeOnlyImportExportSpecifier( | ||
JLHwung marked this conversation as resolved.
Show resolved
Hide resolved
|
||
specifier, | ||
/* isImport */ true, | ||
importedIsString, | ||
/* isInTypeOnlyImportExport */ node.importKind === "type", | ||
); | ||
if (canParseAsKeyworkd && this.eatContextual(tt._as)) { | ||
specifier.local = this.parseIdentifier(); | ||
} else { | ||
const { imported } = specifier; | ||
|
@@ -2453,7 +2476,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")); | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -147,6 +147,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: | ||||||||||||||||||||||||
|
@@ -3311,4 +3315,77 @@ export default (superClass: Class<Parser>): Class<Parser> => | |||||||||||||||||||||||
} | ||||||||||||||||||||||||
return super.getExpression(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
parseTypeOnlyImportExportSpecifier( | ||||||||||||||||||||||||
node: any, | ||||||||||||||||||||||||
isImport: boolean, | ||||||||||||||||||||||||
isStringSpecifier: boolean, | ||||||||||||||||||||||||
isInTypeOnlyImportExport: boolean, | ||||||||||||||||||||||||
): boolean { | ||||||||||||||||||||||||
const leftOfAsKey = isImport ? "imported" : "local"; | ||||||||||||||||||||||||
const rightOfAsKey = isImport ? "local" : "exported"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
let leftOfAs = node[leftOfAsKey]; | ||||||||||||||||||||||||
let rightOfAs; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
let isTypeOnly = false; | ||||||||||||||||||||||||
JLHwung marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
let canParseAsKeyword = true; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const pos = leftOfAs.start; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// https://github.com/microsoft/TypeScript/blob/fc4f9d83d5939047aa6bb2a43965c6e9bbfbc35b/src/compiler/parser.ts#L7411-L7456 | ||||||||||||||||||||||||
if (!isStringSpecifier && leftOfAs.name === "type") { | ||||||||||||||||||||||||
JLHwung marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
// import { type } from "mod"; - isTypeOnly: false, leftOfAs: type | ||||||||||||||||||||||||
// import { type as } from "mod"; - isTypeOnly: true, leftOfAs: as | ||||||||||||||||||||||||
// import { type as as } from "mod"; - isTypeOnly: false, leftOfAs: type, rightOfAs: as | ||||||||||||||||||||||||
// import { type as as as } from "mod"; - isTypeOnly: 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 (this.match(tt.name)) { | ||||||||||||||||||||||||
// { type as as something } | ||||||||||||||||||||||||
isTypeOnly = true; | ||||||||||||||||||||||||
leftOfAs = firstAs; | ||||||||||||||||||||||||
rightOfAs = this.parseIdentifier(); | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 export { type a as if } from "x"; // valid
import { type a as if } from "x"; // invalid There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can use babel/packages/babel-parser/src/plugins/typescript/index.js Lines 2237 to 2247 in 248aa9d
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 (this.match(tt.name)) { | ||||||||||||||||||||||||
JLHwung marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
// { type as something } | ||||||||||||||||||||||||
canParseAsKeyword = false; | ||||||||||||||||||||||||
rightOfAs = this.parseIdentifier(); | ||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||
// { type as } | ||||||||||||||||||||||||
isTypeOnly = true; | ||||||||||||||||||||||||
leftOfAs = firstAs; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} else if (this.match(tt.name)) { | ||||||||||||||||||||||||
JLHwung marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||
// { type something ...? } | ||||||||||||||||||||||||
isTypeOnly = true; | ||||||||||||||||||||||||
leftOfAs = this.parseIdentifier(); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
node[leftOfAsKey] = leftOfAs; | ||||||||||||||||||||||||
node[rightOfAsKey] = rightOfAs; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
const kindKey = isImport ? "importKind" : "exportKind"; | ||||||||||||||||||||||||
node[kindKey] = isTypeOnly ? "type" : "value"; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
if (isTypeOnly && isInTypeOnlyImportExport) { | ||||||||||||||||||||||||
this.raise( | ||||||||||||||||||||||||
pos, | ||||||||||||||||||||||||
isImport | ||||||||||||||||||||||||
? TSErrors.TypeModifierIsUsedInTypeImports | ||||||||||||||||||||||||
: TSErrors.TypeModifierIsUsedInTypeExports, | ||||||||||||||||||||||||
); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
return canParseAsKeyword; | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
d337f69 👍