Skip to content

Commit 52ccb06

Browse files
authored
[ts5.0] Better inlining of constants in enums (#15379)
1 parent ca52e08 commit 52ccb06

6 files changed

Lines changed: 196 additions & 88 deletions

File tree

packages/babel-plugin-transform-typescript/src/const-enum.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export default function transpileConstEnum(
2727
);
2828
}
2929

30-
const entries = translateEnumValues(path, t);
30+
const { enumValues: entries } = translateEnumValues(path, t);
3131

3232
if (isExported) {
3333
const obj = t.objectExpression(

packages/babel-plugin-transform-typescript/src/enum.ts

Lines changed: 148 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { template } from "@babel/core";
1+
import { template, types as t } from "@babel/core";
22
import type { NodePath } from "@babel/traverse";
3-
import type * as t from "@babel/types";
43
import assert from "assert";
54

65
type t = typeof t;
76

7+
const ENUMS = new WeakMap<t.Identifier, PreviousEnumMembers>();
8+
89
export default function transpileEnum(
910
path: NodePath<t.TSEnumDeclaration>,
1011
t: t,
@@ -17,7 +18,7 @@ export default function transpileEnum(
1718
}
1819

1920
const name = node.id.name;
20-
const fill = enumFill(path, t, node.id);
21+
const { wrapper: fill, data } = enumFill(path, t, node.id);
2122

2223
switch (path.parent.type) {
2324
case "BlockStatement":
@@ -33,6 +34,7 @@ export default function transpileEnum(
3334
path.scope.registerDeclaration(
3435
path.replaceWith(makeVar(node.id, t, isGlobal ? "var" : "let"))[0],
3536
);
37+
ENUMS.set(path.scope.getBindingIdentifier(name), data);
3638
}
3739
break;
3840
}
@@ -81,7 +83,7 @@ const buildEnumMember = (isString: boolean, options: Record<string, unknown>) =>
8183
* `(function (E) { ... assignments ... })(E || (E = {}));`
8284
*/
8385
function enumFill(path: NodePath<t.TSEnumDeclaration>, t: t, id: t.Identifier) {
84-
const x = translateEnumValues(path, t);
86+
const { enumValues: x, data } = translateEnumValues(path, t);
8587
const assignments = x.map(([memberName, memberValue]) =>
8688
buildEnumMember(t.isStringLiteral(memberValue), {
8789
ENUM: t.cloneNode(id),
@@ -90,10 +92,13 @@ function enumFill(path: NodePath<t.TSEnumDeclaration>, t: t, id: t.Identifier) {
9092
}),
9193
);
9294

93-
return buildEnumWrapper({
94-
ID: t.cloneNode(id),
95-
ASSIGNMENTS: assignments,
96-
});
95+
return {
96+
wrapper: buildEnumWrapper({
97+
ID: t.cloneNode(id),
98+
ASSIGNMENTS: assignments,
99+
}),
100+
data: data,
101+
};
97102
}
98103

99104
/**
@@ -131,109 +136,172 @@ const enumSelfReferenceVisitor = {
131136
ReferencedIdentifier,
132137
};
133138

134-
export function translateEnumValues(
135-
path: NodePath<t.TSEnumDeclaration>,
136-
t: t,
137-
): Array<[name: string, value: t.Expression]> {
139+
export function translateEnumValues(path: NodePath<t.TSEnumDeclaration>, t: t) {
138140
const seen: PreviousEnumMembers = new Map();
139141
// Start at -1 so the first enum member is its increment, 0.
140142
let constValue: number | string | undefined = -1;
141143
let lastName: string;
142144

143-
return path.get("members").map(memberPath => {
144-
const member = memberPath.node;
145-
const name = t.isIdentifier(member.id) ? member.id.name : member.id.value;
146-
const initializer = member.initializer;
147-
let value: t.Expression;
148-
if (initializer) {
149-
constValue = computeConstantValue(initializer, seen);
150-
if (constValue !== undefined) {
151-
seen.set(name, constValue);
152-
if (typeof constValue === "number") {
153-
value = t.numericLiteral(constValue);
145+
return {
146+
data: seen,
147+
enumValues: path.get("members").map(memberPath => {
148+
const member = memberPath.node;
149+
const name = t.isIdentifier(member.id) ? member.id.name : member.id.value;
150+
const initializerPath = memberPath.get("initializer");
151+
const initializer = member.initializer;
152+
let value: t.Expression;
153+
if (initializer) {
154+
constValue = computeConstantValue(initializerPath, seen);
155+
if (constValue !== undefined) {
156+
seen.set(name, constValue);
157+
if (typeof constValue === "number") {
158+
value = t.numericLiteral(constValue);
159+
} else {
160+
assert(typeof constValue === "string");
161+
value = t.stringLiteral(constValue);
162+
}
154163
} else {
155-
assert(typeof constValue === "string");
156-
value = t.stringLiteral(constValue);
164+
if (initializerPath.isReferencedIdentifier()) {
165+
ReferencedIdentifier(initializerPath, {
166+
t,
167+
seen,
168+
path,
169+
});
170+
} else {
171+
initializerPath.traverse(enumSelfReferenceVisitor, {
172+
t,
173+
seen,
174+
path,
175+
});
176+
}
177+
178+
value = initializerPath.node;
179+
seen.set(name, undefined);
157180
}
181+
} else if (typeof constValue === "number") {
182+
constValue += 1;
183+
value = t.numericLiteral(constValue);
184+
seen.set(name, constValue);
185+
} else if (typeof constValue === "string") {
186+
throw path.buildCodeFrameError("Enum member must have initializer.");
158187
} else {
159-
const initializerPath = memberPath.get("initializer");
160-
161-
if (initializerPath.isReferencedIdentifier()) {
162-
ReferencedIdentifier(initializerPath, {
163-
t,
164-
seen,
165-
path,
166-
});
167-
} else {
168-
initializerPath.traverse(enumSelfReferenceVisitor, { t, seen, path });
169-
}
170-
171-
value = initializerPath.node;
188+
// create dynamic initializer: 1 + ENUM["PREVIOUS"]
189+
const lastRef = t.memberExpression(
190+
t.cloneNode(path.node.id),
191+
t.stringLiteral(lastName),
192+
true,
193+
);
194+
value = t.binaryExpression("+", t.numericLiteral(1), lastRef);
172195
seen.set(name, undefined);
173196
}
174-
} else if (typeof constValue === "number") {
175-
constValue += 1;
176-
value = t.numericLiteral(constValue);
177-
seen.set(name, constValue);
178-
} else if (typeof constValue === "string") {
179-
throw path.buildCodeFrameError("Enum member must have initializer.");
180-
} else {
181-
// create dynamic initializer: 1 + ENUM["PREVIOUS"]
182-
const lastRef = t.memberExpression(
183-
t.cloneNode(path.node.id),
184-
t.stringLiteral(lastName),
185-
true,
186-
);
187-
value = t.binaryExpression("+", t.numericLiteral(1), lastRef);
188-
seen.set(name, undefined);
189-
}
190197

191-
lastName = name;
192-
return [name, value];
193-
});
198+
lastName = name;
199+
return [name, value];
200+
}) as Array<[name: string, value: t.Expression]>,
201+
};
194202
}
195203

196204
// Based on the TypeScript repository's `computeConstantValue` in `checker.ts`.
197205
function computeConstantValue(
198-
expr: t.Node,
199-
seen: PreviousEnumMembers,
200-
): number | string | typeof undefined {
201-
return evaluate(expr);
206+
path: NodePath,
207+
prevMembers?: PreviousEnumMembers,
208+
seen: Set<t.Identifier> = new Set(),
209+
): number | string | undefined {
210+
return evaluate(path);
202211

203-
function evaluate(expr: t.Node): number | typeof undefined {
212+
function evaluate(path: NodePath): number | string | undefined {
213+
const expr = path.node;
204214
switch (expr.type) {
215+
case "MemberExpression":
216+
return evaluateRef(path, prevMembers, seen);
205217
case "StringLiteral":
206218
return expr.value;
207219
case "UnaryExpression":
208-
return evalUnaryExpression(expr);
220+
return evalUnaryExpression(path as NodePath<t.UnaryExpression>);
209221
case "BinaryExpression":
210-
return evalBinaryExpression(expr);
222+
return evalBinaryExpression(path as NodePath<t.BinaryExpression>);
211223
case "NumericLiteral":
212224
return expr.value;
213225
case "ParenthesizedExpression":
214-
return evaluate(expr.expression);
226+
return evaluate(path.get("expression"));
215227
case "Identifier":
216-
return seen.get(expr.name);
217-
case "TemplateLiteral":
228+
return evaluateRef(path, prevMembers, seen);
229+
case "TemplateLiteral": {
218230
if (expr.quasis.length === 1) {
219231
return expr.quasis[0].value.cooked;
220232
}
221-
/* falls through */
233+
234+
const paths = (path as NodePath<t.TemplateLiteral>).get("expressions");
235+
const quasis = expr.quasis;
236+
let str = "";
237+
238+
for (let i = 0; i < quasis.length; i++) {
239+
str += quasis[i].value.cooked;
240+
241+
if (i + 1 < quasis.length) {
242+
const value = evaluateRef(paths[i], prevMembers, seen);
243+
if (value === undefined) return undefined;
244+
str += value;
245+
}
246+
}
247+
return str;
248+
}
222249
default:
223250
return undefined;
224251
}
225252
}
226253

227-
function evalUnaryExpression({
228-
argument,
229-
operator,
230-
}: t.UnaryExpression): number | typeof undefined {
231-
const value = evaluate(argument);
254+
function evaluateRef(
255+
path: NodePath,
256+
prevMembers: PreviousEnumMembers,
257+
seen: Set<t.Identifier>,
258+
): number | string | undefined {
259+
if (path.isMemberExpression()) {
260+
const expr = path.node;
261+
262+
const obj = expr.object;
263+
const prop = expr.property;
264+
if (
265+
!t.isIdentifier(obj) ||
266+
(expr.computed ? !t.isStringLiteral(prop) : !t.isIdentifier(prop))
267+
) {
268+
return;
269+
}
270+
const bindingIdentifier = path.scope.getBindingIdentifier(obj.name);
271+
const data = ENUMS.get(bindingIdentifier);
272+
if (!data) return;
273+
// @ts-expect-error checked above
274+
return data.get(prop.computed ? prop.value : prop.name);
275+
} else if (path.isIdentifier()) {
276+
const name = path.node.name;
277+
278+
let value = prevMembers?.get(name);
279+
if (value !== undefined) {
280+
return value;
281+
}
282+
283+
if (seen.has(path.node)) return;
284+
285+
const bindingInitPath = path.resolve(); // It only resolves constant bindings
286+
if (bindingInitPath) {
287+
seen.add(path.node);
288+
289+
value = computeConstantValue(bindingInitPath, undefined, seen);
290+
prevMembers?.set(name, value);
291+
return value;
292+
}
293+
}
294+
}
295+
296+
function evalUnaryExpression(
297+
path: NodePath<t.UnaryExpression>,
298+
): number | string | undefined {
299+
const value = evaluate(path.get("argument"));
232300
if (value === undefined) {
233301
return undefined;
234302
}
235303

236-
switch (operator) {
304+
switch (path.node.operator) {
237305
case "+":
238306
return value;
239307
case "-":
@@ -245,17 +313,19 @@ function computeConstantValue(
245313
}
246314
}
247315

248-
function evalBinaryExpression(expr: t.BinaryExpression): number | undefined {
249-
const left = evaluate(expr.left);
316+
function evalBinaryExpression(
317+
path: NodePath<t.BinaryExpression>,
318+
): number | string | undefined {
319+
const left = evaluate(path.get("left")) as any;
250320
if (left === undefined) {
251321
return undefined;
252322
}
253-
const right = evaluate(expr.right);
323+
const right = evaluate(path.get("right")) as any;
254324
if (right === undefined) {
255325
return undefined;
256326
}
257327

258-
switch (expr.operator) {
328+
switch (path.node.operator) {
259329
case "|":
260330
return left | right;
261331
case "&":

packages/babel-plugin-transform-typescript/test/fixtures/enum/mix-references/output.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ var Foo;
33
(function (Foo) {
44
Foo[Foo["a"] = 10] = "a";
55
Foo[Foo["b"] = 10] = "b";
6-
Foo[Foo["c"] = Foo.b + x] = "c";
6+
Foo[Foo["c"] = 20] = "c";
77
})(Foo || (Foo = {}));
88
var Bar;
99
(function (Bar) {
10-
Bar[Bar["D"] = Foo.a] = "D";
11-
Bar[Bar["E"] = Bar.D] = "E";
10+
Bar[Bar["D"] = 10] = "D";
11+
Bar[Bar["E"] = 10] = "E";
1212
Bar[Bar["F"] = Math.E] = "F";
13-
Bar[Bar["G"] = Bar.E + Foo.c] = "G";
13+
Bar[Bar["G"] = 30] = "G";
1414
})(Bar || (Bar = {}));
1515
var Baz;
1616
(function (Baz) {

packages/babel-plugin-transform-typescript/test/fixtures/enum/outer-references/output.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ var socketType;
66
})(socketType || (socketType = {}));
77
var constants;
88
(function (constants) {
9-
constants[constants["SOCKET"] = socketType.SOCKET] = "SOCKET";
10-
constants[constants["SERVER"] = socketType.SERVER] = "SERVER";
11-
constants[constants["IPC"] = socketType.IPC] = "IPC";
12-
constants[constants["UV_READABLE"] = 1 + constants["IPC"]] = "UV_READABLE";
13-
constants[constants["UV_WRITABLE"] = 1 + constants["UV_READABLE"]] = "UV_WRITABLE";
9+
constants[constants["SOCKET"] = 0] = "SOCKET";
10+
constants[constants["SERVER"] = 1] = "SERVER";
11+
constants[constants["IPC"] = 2] = "IPC";
12+
constants[constants["UV_READABLE"] = 3] = "UV_READABLE";
13+
constants[constants["UV_WRITABLE"] = 4] = "UV_WRITABLE";
1414
})(constants || (constants = {}));
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// ts 5.0 feature
2+
3+
const BaseValue = 10;
4+
const Prefix = "/data";
5+
const enum Values {
6+
First = BaseValue, // 10
7+
Second, // 11
8+
Third, // 12
9+
}
10+
11+
const xxx = 100 + Values.First;
12+
const yyy = xxx;
13+
14+
const enum Routes {
15+
Parts = `${Prefix}/parts`, // "/data/parts"
16+
Invoices = `${Prefix}/invoices`, // "/data/invoices"
17+
x = `${Values.First}/x`,
18+
y = `${yyy}/y`,
19+
}

0 commit comments

Comments
 (0)