Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
2d29531527 fix(transpiler): fold const enum members that reference const variables
When a const enum member expression referenced a `const` variable
with a constant initializer (e.g. `const x = 5`), the value was not
being folded because the enum preprocessing pass runs before const
variable declarations are visited. This pre-populates const_values
for simple const declarations before enum preprocessing, and also
checks should_fold_typescript_constant_expressions in handleIdentifier
so const_values are used during enum folding even without the global
inlining feature flag.

Closes #19581

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 10:11:55 +00:00
4 changed files with 150 additions and 1 deletions

View File

@@ -1119,7 +1119,7 @@ pub fn NewParser_(
pub fn handleIdentifier(noalias p: *P, loc: logger.Loc, ident: E.Identifier, original_name: ?string, opts: IdentifierOpts) Expr {
const ref = ident.ref;
if (p.options.features.inlining) {
if (p.options.features.inlining or p.should_fold_typescript_constant_expressions) {
if (p.const_values.get(ref)) |replacement| {
p.ignoreUsage(ref);
return replacement;

View File

@@ -533,6 +533,28 @@ pub const Parser = struct {
var preprocessed_enums: std.ArrayListUnmanaged([]js_ast.Part) = .{};
var preprocessed_enum_i: usize = 0;
if (p.scopes_in_order_for_enum.count() > 0) {
// Pre-populate const_values for simple const declarations that
// appear before enums. This allows const enum members to reference
// const variables with constant initializers, matching TypeScript
// behavior. Without this, the enum preprocessing (which runs before
// the main statement visiting loop) would not see these values.
for (stmts) |*stmt| {
if (stmt.data == .s_local) {
const local = stmt.data.s_local;
if (local.kind == .k_const) {
for (local.decls.slice()) |decl| {
if (decl.binding.data == .b_identifier) {
if (decl.value) |val| {
if (val.data.canBeConstValue()) {
p.const_values.put(p.allocator, decl.binding.data.b_identifier.ref, val) catch unreachable;
}
}
}
}
}
}
}
for (stmts) |*stmt| {
if (stmt.data == .s_enum) {
const old_scopes_in_order = p.scope_order_to_visit;

View File

@@ -797,6 +797,28 @@ pub fn Visit(
var preprocessed_enums: std.ArrayListUnmanaged([]Stmt) = .{};
defer preprocessed_enums.deinit(p.allocator);
if (p.scopes_in_order_for_enum.count() > 0) {
// Pre-populate const_values for simple const declarations that
// appear before enums. This allows const enum members to reference
// const variables with constant initializers, matching TypeScript
// behavior. Without this, the enum preprocessing (which runs before
// the main statement visiting loop) would not see these values.
for (stmts.items) |*stmt| {
if (stmt.data == .s_local) {
const local = stmt.data.s_local;
if (local.kind == .k_const) {
for (local.decls.slice()) |decl| {
if (decl.binding.data == .b_identifier) {
if (decl.value) |val| {
if (val.data.canBeConstValue()) {
p.const_values.put(p.allocator, decl.binding.data.b_identifier.ref, val) catch unreachable;
}
}
}
}
}
}
}
var found: usize = 0;
for (stmts.items) |*stmt| {
if (stmt.data == .s_enum) {

View File

@@ -0,0 +1,105 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("const enum members can reference const variables with constant initializers", async () => {
using dir = tempDir("19581", {
"index.ts": `
const enum First {
A = 1,
B = 2,
C = 3,
}
const multiplier = 5;
const enum Second {
D = First.A * multiplier,
E = First.B * multiplier,
F = First.C * multiplier,
}
console.log(Second.D, Second.E, Second.F, Second.E + Second.F);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", String(dir) + "/index.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The const enum values should be fully folded to numeric literals.
// Second.D = First.A * multiplier = 1 * 5 = 5
// Second.E = First.B * multiplier = 2 * 5 = 10
// Second.F = First.C * multiplier = 3 * 5 = 15
expect(stdout).toContain("5 /* D */");
expect(stdout).toContain("10 /* E */");
expect(stdout).toContain("15 /* F */");
// The multiplier variable should not appear in the enum output
expect(stdout).not.toContain("* multiplier");
expect(exitCode).toBe(0);
});
test("const enum with const variable folds completely with --minify", async () => {
using dir = tempDir("19581-minify", {
"index.ts": `
const enum First { A = 1, B = 2, C = 3 }
const multiplier = 5;
const enum Second {
D = First.A * multiplier,
E = First.B * multiplier,
F = First.C * multiplier,
}
console.log(Second.E + Second.F);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--minify", String(dir) + "/index.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// With minification, 10 + 15 should fold to 25
expect(stdout.trim()).toBe("console.log(25);");
expect(exitCode).toBe(0);
});
test("const enum folding with simple const variable", async () => {
using dir = tempDir("19581-simple", {
"index.ts": `
const base = 10;
const enum MyEnum {
A = base,
B = base + 1,
C = base * 2,
}
console.log(MyEnum.A, MyEnum.B, MyEnum.C);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", String(dir) + "/index.ts"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("10 /* A */");
expect(stdout).toContain("11 /* B */");
expect(stdout).toContain("20 /* C */");
// The enum values should be folded, not left as "base" or "base + 1"
expect(stdout).not.toContain("= base");
expect(exitCode).toBe(0);
});