Support TypeScript const enums (#13324)

This commit is contained in:
Nicolò Ribaudo 2021-08-03 23:57:09 +02:00 committed by GitHub
parent 0542f0d7da
commit b707842dd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 280 additions and 21 deletions

View File

@ -0,0 +1,89 @@
import type * as t from "@babel/types";
import type { NodePath } from "@babel/traverse";
import { translateEnumValues } from "./enum";
export default function transpileConstEnum(
path: NodePath<t.TSEnumDeclaration & { const: true }>,
t: typeof import("@babel/types"),
) {
const { name } = path.node.id;
const parentIsExport = path.parentPath.isExportNamedDeclaration();
let isExported = parentIsExport;
if (!isExported && t.isProgram(path.parent)) {
isExported = path.parent.body.some(
stmt =>
t.isExportNamedDeclaration(stmt) &&
!stmt.source &&
stmt.specifiers.some(
spec => t.isExportSpecifier(spec) && spec.local.name === name,
),
);
}
const entries = translateEnumValues(path, t);
if (isExported) {
const obj = t.objectExpression(
entries.map(([name, value]) =>
t.objectProperty(
t.isValidIdentifier(name)
? t.identifier(name)
: t.stringLiteral(name),
value,
),
),
);
if (path.scope.hasOwnBinding(name)) {
(parentIsExport ? path.parentPath : path).replaceWith(
t.expressionStatement(
t.callExpression(
t.memberExpression(t.identifier("Object"), t.identifier("assign")),
[path.node.id, obj],
),
),
);
} else {
path.replaceWith(
t.variableDeclaration("var", [t.variableDeclarator(path.node.id, obj)]),
);
path.scope.registerDeclaration(path);
}
return;
}
const entriesMap = new Map(entries);
// TODO: After fixing https://github.com/babel/babel/pull/11065, we can
// use path.scope.getBinding(name).referencePaths rather than doing
// a full traversal.
path.scope.path.traverse({
Scope(path) {
if (path.scope.hasOwnBinding(name)) path.skip();
},
MemberExpression(path) {
if (!t.isIdentifier(path.node.object, { name })) return;
let key: string;
if (path.node.computed) {
if (t.isStringLiteral(path.node.property)) {
key = path.node.property.value;
} else {
return;
}
} else if (t.isIdentifier(path.node.property)) {
key = path.node.property.name;
} else {
return;
}
if (!entriesMap.has(key)) return;
path.replaceWith(t.cloneNode(entriesMap.get(key)));
},
});
path.remove();
}

View File

@ -5,9 +5,6 @@ import type { NodePath } from "@babel/traverse";
export default function transpileEnum(path, t) {
const { node } = path;
if (node.const) {
throw path.buildCodeFrameError("'const' enums are not supported.");
}
if (node.declare) {
path.remove();
@ -105,7 +102,10 @@ type PreviousEnumMembers = {
[name: string]: number | string;
};
function translateEnumValues(path, t) {
export function translateEnumValues(
path: NodePath<t.TSEnumDeclaration>,
t: typeof import("@babel/types"),
): Array<[name: string, value: t.Expression]> {
const seen: PreviousEnumMembers = Object.create(null);
// Start at -1 so the first enum member is its increment, 0.
let prev: number | typeof undefined = -1;

View File

@ -3,6 +3,7 @@ import syntaxTypeScript from "@babel/plugin-syntax-typescript";
import { types as t, template } from "@babel/core";
import { injectInitialization } from "@babel/helper-create-class-features-plugin";
import transpileConstEnum from "./const-enum";
import transpileEnum from "./enum";
import transpileNamespace from "./namespace";
import type { NodePath } from "@babel/traverse";
@ -60,6 +61,7 @@ export default declare((api, opts) => {
jsxPragma = "React.createElement",
jsxPragmaFrag = "React.Fragment",
onlyRemoveTypeImports = false,
optimizeConstEnums = false,
} = opts;
if (!process.env.BABEL_8_BREAKING) {
@ -462,7 +464,11 @@ export default declare((api, opts) => {
},
TSEnumDeclaration(path) {
transpileEnum(path, t);
if (optimizeConstEnums && path.node.const) {
transpileConstEnum(path, t);
} else {
transpileEnum(path, t);
}
},
TSImportEqualsDeclaration(path: NodePath<t.TSImportEqualsDeclaration>) {

View File

@ -1 +0,0 @@
{ "throws": "'const' enums are not supported." }

View File

@ -0,0 +1,3 @@
export declare enum A {}
;

View File

@ -0,0 +1,3 @@
{
"sourceType": "module"
}

View File

@ -1 +1,2 @@
const enum E {}
// With --isolatedModules, TSC ignores the "const" modifier when compiling enums
const enum E {}

View File

@ -1 +0,0 @@
{ "throws": "'const' enums are not supported." }

View File

@ -0,0 +1,4 @@
// With --isolatedModules, TSC ignores the "const" modifier when compiling enums
var E;
(function (E) {})(E || (E = {}));

View File

@ -0,0 +1,11 @@
export const enum A {
x = 3,
y = "f",
z = 4 << 2,
w = y
}
A.x;
A.y;
A.z;
A.w;

View File

@ -0,0 +1,10 @@
export var A = {
x: 3,
y: "f",
z: 16,
w: "f"
};
A.x;
A.y;
A.z;
A.w;

View File

@ -0,0 +1,11 @@
const enum A {
x = 3,
y = "f",
z = 4 << 2,
w = y
}
A.x;
A.y;
A.z;
A.w;

View File

@ -0,0 +1,4 @@
3;
"f";
16;
"f";

View File

@ -0,0 +1,3 @@
declare const enum A { x }
A.x;

View File

@ -0,0 +1,3 @@
export const enum A { y }
let x = A.y;

View File

@ -0,0 +1,4 @@
export var A = {
y: 0
};
let x = A.y;

View File

@ -0,0 +1,5 @@
const enum A { y }
let x = A.y;
export { A };

View File

@ -0,0 +1,5 @@
var A = {
y: 0
};
let x = A.y;
export { A };

View File

@ -0,0 +1,8 @@
const enum A { x }
{
let A = {};
A.x;
}
A.x;

View File

@ -0,0 +1,5 @@
{
let A = {};
A.x;
}
0;

View File

@ -0,0 +1,8 @@
const enum A {
x, y
}
A.x;
A["y"];
A.z;
A;

View File

@ -0,0 +1,4 @@
0;
1;
A.z;
A;

View File

@ -0,0 +1,13 @@
export const enum A {
x, y
}
export const enum A {
z
}
A.x;
A["y"];
A.z;
A.w;
A;

View File

@ -0,0 +1,12 @@
export var A = {
x: 0,
y: 1
};
Object.assign(A, {
z: 0
});
A.x;
A["y"];
A.z;
A.w;
A;

View File

@ -0,0 +1,13 @@
const enum A {
x, y
}
const enum A {
z
}
A.x;
A["y"];
A.z;
A.w;
A;

View File

@ -0,0 +1,5 @@
0;
1;
0;
A.w;
A;

View File

@ -0,0 +1,4 @@
{
"plugins": [["transform-typescript", { "optimizeConstEnums": true }]],
"sourceType": "module"
}

View File

@ -12,6 +12,7 @@ export default declare((api, opts) => {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
} = normalizeOptions(opts);
const pluginOptions = process.env.BABEL_8_BREAKING
@ -21,6 +22,7 @@ export default declare((api, opts) => {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
})
: isTSX => ({
allowDeclareFields: opts.allowDeclareFields,
@ -29,6 +31,7 @@ export default declare((api, opts) => {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
});
return {

View File

@ -4,15 +4,17 @@ const v = new OptionValidator("@babel/preset-typescript");
export default function normalizeOptions(options = {}) {
let { allowNamespaces = true, jsxPragma, onlyRemoveTypeImports } = options;
const TopLevelOptions = {
allExtensions: "allExtensions",
allowNamespaces: "allowNamespaces",
isTSX: "isTSX",
jsxPragma: "jsxPragma",
jsxPragmaFrag: "jsxPragmaFrag",
onlyRemoveTypeImports: "onlyRemoveTypeImports",
optimizeConstEnums: "optimizeConstEnums",
};
if (process.env.BABEL_8_BREAKING) {
const TopLevelOptions = {
allExtensions: "allExtensions",
allowNamespaces: "allowNamespaces",
isTSX: "isTSX",
jsxPragma: "jsxPragma",
jsxPragmaFrag: "jsxPragmaFrag",
onlyRemoveTypeImports: "onlyRemoveTypeImports",
};
v.validateTopLevelOptions(options, TopLevelOptions);
allowNamespaces = v.validateBooleanOption(
TopLevelOptions.allowNamespaces,
@ -32,23 +34,32 @@ export default function normalizeOptions(options = {}) {
}
const jsxPragmaFrag = v.validateStringOption(
"jsxPragmaFrag",
TopLevelOptions.jsxPragmaFrag,
options.jsxPragmaFrag,
"React.Fragment",
);
const allExtensions = v.validateBooleanOption(
"allExtensions",
TopLevelOptions.allExtensions,
options.allExtensions,
false,
);
const isTSX = v.validateBooleanOption("isTSX", options.isTSX, false);
const isTSX = v.validateBooleanOption(
TopLevelOptions.isTSX,
options.isTSX,
false,
);
if (isTSX) {
v.invariant(allExtensions, "isTSX:true requires allExtensions:true");
}
const optimizeConstEnums = v.validateBooleanOption(
TopLevelOptions.optimizeConstEnums,
options.optimizeConstEnums,
false,
);
return {
allExtensions,
allowNamespaces,
@ -56,5 +67,6 @@ export default function normalizeOptions(options = {}) {
jsxPragma,
jsxPragmaFrag,
onlyRemoveTypeImports,
optimizeConstEnums,
};
}

View File

@ -0,0 +1,3 @@
const enum A { x }
A.x;

View File

@ -0,0 +1,3 @@
{
"presets": [["typescript", { "optimizeConstEnums": true }]]
}

View File

@ -11,6 +11,7 @@ describe("normalize options", () => {
"allowNamespaces",
"isTSX",
"onlyRemoveTypeImports",
"optimizeConstEnums",
])("should throw when `%p` is not a boolean", optionName => {
expect(() => normalizeOptions({ [optionName]: 0 })).toThrow(
`@babel/preset-typescript: '${optionName}' option must be a boolean.`,
@ -36,6 +37,7 @@ describe("normalize options", () => {
"jsxPragma": "React",
"jsxPragmaFrag": "React.Fragment",
"onlyRemoveTypeImports": true,
"optimizeConstEnums": false,
}
`);
});
@ -58,7 +60,7 @@ describe("normalize options", () => {
expect(() => normalizeOptions({ [optionName]: 0 })).not.toThrowError();
},
);
it.each(["allExtensions", "isTSX"])(
it.each(["allExtensions", "isTSX", "optimizeConstEnums"])(
"should throw when `%p` is not a boolean",
optionName => {
expect(() => normalizeOptions({ [optionName]: 0 })).toThrow(
@ -83,6 +85,7 @@ describe("normalize options", () => {
"jsxPragma": undefined,
"jsxPragmaFrag": "React.Fragment",
"onlyRemoveTypeImports": undefined,
"optimizeConstEnums": false,
}
`);
});