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

[ts5.0] Better inlining of constants in enums #15379

Merged
merged 5 commits into from Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -27,7 +27,7 @@ export default function transpileConstEnum(
);
}

const entries = translateEnumValues(path, t);
const { enumValues: entries } = translateEnumValues(path, t);

if (isExported) {
const obj = t.objectExpression(
Expand Down
226 changes: 148 additions & 78 deletions packages/babel-plugin-transform-typescript/src/enum.ts
@@ -1,10 +1,11 @@
import { template } from "@babel/core";
import { template, types as t } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import type * as t from "@babel/types";
import assert from "assert";

type t = typeof t;

const ENUMS = new WeakMap<t.Identifier, PreviousEnumMembers>();

export default function transpileEnum(
path: NodePath<t.TSEnumDeclaration>,
t: t,
Expand All @@ -17,7 +18,7 @@ export default function transpileEnum(
}

const name = node.id.name;
const fill = enumFill(path, t, node.id);
const { wrapper: fill, data } = enumFill(path, t, node.id);

switch (path.parent.type) {
case "BlockStatement":
Expand All @@ -33,6 +34,7 @@ export default function transpileEnum(
path.scope.registerDeclaration(
path.replaceWith(makeVar(node.id, t, isGlobal ? "var" : "let"))[0],
);
ENUMS.set(path.scope.getBindingIdentifier(name), data);
}
break;
}
Expand Down Expand Up @@ -81,7 +83,7 @@ const buildEnumMember = (isString: boolean, options: Record<string, unknown>) =>
* `(function (E) { ... assignments ... })(E || (E = {}));`
*/
function enumFill(path: NodePath<t.TSEnumDeclaration>, t: t, id: t.Identifier) {
const x = translateEnumValues(path, t);
const { enumValues: x, data } = translateEnumValues(path, t);
const assignments = x.map(([memberName, memberValue]) =>
buildEnumMember(t.isStringLiteral(memberValue), {
ENUM: t.cloneNode(id),
Expand All @@ -90,10 +92,13 @@ function enumFill(path: NodePath<t.TSEnumDeclaration>, t: t, id: t.Identifier) {
}),
);

return buildEnumWrapper({
ID: t.cloneNode(id),
ASSIGNMENTS: assignments,
});
return {
wrapper: buildEnumWrapper({
ID: t.cloneNode(id),
ASSIGNMENTS: assignments,
}),
data: data,
};
}

/**
Expand Down Expand Up @@ -131,109 +136,172 @@ const enumSelfReferenceVisitor = {
ReferencedIdentifier,
};

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

return path.get("members").map(memberPath => {
const member = memberPath.node;
const name = t.isIdentifier(member.id) ? member.id.name : member.id.value;
const initializer = member.initializer;
let value: t.Expression;
if (initializer) {
constValue = computeConstantValue(initializer, seen);
if (constValue !== undefined) {
seen.set(name, constValue);
if (typeof constValue === "number") {
value = t.numericLiteral(constValue);
return {
data: seen,
enumValues: path.get("members").map(memberPath => {
const member = memberPath.node;
const name = t.isIdentifier(member.id) ? member.id.name : member.id.value;
const initializerPath = memberPath.get("initializer");
const initializer = member.initializer;
let value: t.Expression;
if (initializer) {
constValue = computeConstantValue(initializerPath, seen);
if (constValue !== undefined) {
seen.set(name, constValue);
if (typeof constValue === "number") {
value = t.numericLiteral(constValue);
} else {
assert(typeof constValue === "string");
value = t.stringLiteral(constValue);
}
} else {
assert(typeof constValue === "string");
value = t.stringLiteral(constValue);
if (initializerPath.isReferencedIdentifier()) {
ReferencedIdentifier(initializerPath, {
t,
seen,
path,
});
} else {
initializerPath.traverse(enumSelfReferenceVisitor, {
t,
seen,
path,
});
}

value = initializerPath.node;
seen.set(name, undefined);
}
} else if (typeof constValue === "number") {
constValue += 1;
value = t.numericLiteral(constValue);
seen.set(name, constValue);
} else if (typeof constValue === "string") {
throw path.buildCodeFrameError("Enum member must have initializer.");
} else {
const initializerPath = memberPath.get("initializer");

if (initializerPath.isReferencedIdentifier()) {
ReferencedIdentifier(initializerPath, {
t,
seen,
path,
});
} else {
initializerPath.traverse(enumSelfReferenceVisitor, { t, seen, path });
}

value = initializerPath.node;
// create dynamic initializer: 1 + ENUM["PREVIOUS"]
const lastRef = t.memberExpression(
t.cloneNode(path.node.id),
t.stringLiteral(lastName),
true,
);
value = t.binaryExpression("+", t.numericLiteral(1), lastRef);
seen.set(name, undefined);
}
} else if (typeof constValue === "number") {
constValue += 1;
value = t.numericLiteral(constValue);
seen.set(name, constValue);
} else if (typeof constValue === "string") {
throw path.buildCodeFrameError("Enum member must have initializer.");
} else {
// create dynamic initializer: 1 + ENUM["PREVIOUS"]
const lastRef = t.memberExpression(
t.cloneNode(path.node.id),
t.stringLiteral(lastName),
true,
);
value = t.binaryExpression("+", t.numericLiteral(1), lastRef);
seen.set(name, undefined);
}

lastName = name;
return [name, value];
});
lastName = name;
return [name, value];
}) as Array<[name: string, value: t.Expression]>,
};
}

// Based on the TypeScript repository's `computeConstantValue` in `checker.ts`.
function computeConstantValue(
expr: t.Node,
seen: PreviousEnumMembers,
): number | string | typeof undefined {
return evaluate(expr);
path: NodePath,
prevMembers?: PreviousEnumMembers,
seen: Set<t.Identifier> = new Set(),
): number | string | undefined {
return evaluate(path);

function evaluate(expr: t.Node): number | typeof undefined {
function evaluate(path: NodePath): number | string | undefined {
const expr = path.node;
switch (expr.type) {
case "MemberExpression":
return evaluateRef(path, prevMembers, seen);
case "StringLiteral":
return expr.value;
case "UnaryExpression":
return evalUnaryExpression(expr);
return evalUnaryExpression(path as NodePath<t.UnaryExpression>);
case "BinaryExpression":
return evalBinaryExpression(expr);
return evalBinaryExpression(path as NodePath<t.BinaryExpression>);
case "NumericLiteral":
return expr.value;
case "ParenthesizedExpression":
return evaluate(expr.expression);
return evaluate(path.get("expression"));
case "Identifier":
return seen.get(expr.name);
case "TemplateLiteral":
return evaluateRef(path, prevMembers, seen);
case "TemplateLiteral": {
Copy link
Member

Choose a reason for hiding this comment

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

What is the difference between the code in this case and just using path.evaluate()? It seems to me that this code doesn't access the ENUMS weakmap, so they should be equivalent.

Copy link
Member Author

Choose a reason for hiding this comment

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

The main inconsistency comes from evaluateIdentifier, and overall, path.evaluate() will be more aggressive, while ts will be safer. However I guess ts officially supported const var should always work fine.

if (expr.quasis.length === 1) {
return expr.quasis[0].value.cooked;
}
/* falls through */

const paths = (path as NodePath<t.TemplateLiteral>).get("expressions");
const quasis = expr.quasis;
let str = "";

for (let i = 0; i < quasis.length; i++) {
str += quasis[i].value.cooked;

if (i + 1 < quasis.length) {
const value = evaluateRef(paths[i], prevMembers, seen);
if (value === undefined) return undefined;
str += value;
}
}
return str;
}
default:
return undefined;
}
}

function evalUnaryExpression({
argument,
operator,
}: t.UnaryExpression): number | typeof undefined {
const value = evaluate(argument);
function evaluateRef(
path: NodePath,
prevMembers: PreviousEnumMembers,
seen: Set<t.Identifier>,
): number | string | undefined {
if (path.isMemberExpression()) {
const expr = path.node;

const obj = expr.object;
const prop = expr.property;
if (
!t.isIdentifier(obj) ||
(expr.computed ? !t.isStringLiteral(prop) : !t.isIdentifier(prop))
) {
return;
}
const bindingIdentifier = path.scope.getBindingIdentifier(obj.name);
const data = ENUMS.get(bindingIdentifier);
if (!data) return;
// @ts-expect-error checked above
return data.get(prop.computed ? prop.value : prop.name);
} else if (path.isIdentifier()) {
const name = path.node.name;

let value = prevMembers?.get(name);
if (value !== undefined) {
return value;
}

if (seen.has(path.node)) return;

const bindingInitPath = path.resolve(); // It only resolves constant bindings
if (bindingInitPath) {
seen.add(path.node);

value = computeConstantValue(bindingInitPath, undefined, seen);
prevMembers?.set(name, value);
return value;
}
}
}

function evalUnaryExpression(
path: NodePath<t.UnaryExpression>,
): number | string | undefined {
const value = evaluate(path.get("argument"));
if (value === undefined) {
return undefined;
}

switch (operator) {
switch (path.node.operator) {
case "+":
return value;
case "-":
Expand All @@ -245,17 +313,19 @@ function computeConstantValue(
}
}

function evalBinaryExpression(expr: t.BinaryExpression): number | undefined {
const left = evaluate(expr.left);
function evalBinaryExpression(
path: NodePath<t.BinaryExpression>,
): number | string | undefined {
const left = evaluate(path.get("left")) as any;
if (left === undefined) {
return undefined;
}
const right = evaluate(expr.right);
const right = evaluate(path.get("right")) as any;
if (right === undefined) {
return undefined;
}

switch (expr.operator) {
switch (path.node.operator) {
case "|":
return left | right;
case "&":
Expand Down
Expand Up @@ -3,14 +3,14 @@ var Foo;
(function (Foo) {
Foo[Foo["a"] = 10] = "a";
Foo[Foo["b"] = 10] = "b";
Foo[Foo["c"] = Foo.b + x] = "c";
Foo[Foo["c"] = 20] = "c";
})(Foo || (Foo = {}));
var Bar;
(function (Bar) {
Bar[Bar["D"] = Foo.a] = "D";
Bar[Bar["E"] = Bar.D] = "E";
Bar[Bar["D"] = 10] = "D";
Bar[Bar["E"] = 10] = "E";
Bar[Bar["F"] = Math.E] = "F";
Bar[Bar["G"] = Bar.E + Foo.c] = "G";
Bar[Bar["G"] = 30] = "G";
})(Bar || (Bar = {}));
var Baz;
(function (Baz) {
Expand Down
Expand Up @@ -6,9 +6,9 @@ var socketType;
})(socketType || (socketType = {}));
var constants;
(function (constants) {
constants[constants["SOCKET"] = socketType.SOCKET] = "SOCKET";
constants[constants["SERVER"] = socketType.SERVER] = "SERVER";
constants[constants["IPC"] = socketType.IPC] = "IPC";
constants[constants["UV_READABLE"] = 1 + constants["IPC"]] = "UV_READABLE";
constants[constants["UV_WRITABLE"] = 1 + constants["UV_READABLE"]] = "UV_WRITABLE";
constants[constants["SOCKET"] = 0] = "SOCKET";
constants[constants["SERVER"] = 1] = "SERVER";
constants[constants["IPC"] = 2] = "IPC";
constants[constants["UV_READABLE"] = 3] = "UV_READABLE";
constants[constants["UV_WRITABLE"] = 4] = "UV_WRITABLE";
})(constants || (constants = {}));
@@ -0,0 +1,19 @@
// ts 5.0 feature

const BaseValue = 10;
const Prefix = "/data";
const enum Values {
First = BaseValue, // 10
Second, // 11
Third, // 12
}

const xxx = 100 + Values.First;
const yyy = xxx;

const enum Routes {
Parts = `${Prefix}/parts`, // "/data/parts"
Invoices = `${Prefix}/invoices`, // "/data/invoices"
x = `${Values.First}/x`,
y = `${yyy}/y`,
}