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
7 changed files with 187 additions and 129 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

@@ -152,18 +152,16 @@ JSC::JSValue createNodeURLBinding(Zig::GlobalObject* globalObject)
ASSERT(domainToAsciiFunction);
auto domainToUnicodeFunction = JSC::JSFunction::create(vm, globalObject, 1, "domainToUnicode"_s, jsDomainToUnicode, ImplementationVisibility::Public);
ASSERT(domainToUnicodeFunction);
binding->putDirectIndex(
binding->putByIndexInline(
globalObject,
(unsigned)0,
domainToAsciiFunction,
0,
JSC::PutDirectIndexMode::PutDirectIndexLikePutDirect);
binding->putDirectIndex(
false);
binding->putByIndexInline(
globalObject,
(unsigned)1,
domainToUnicodeFunction,
0,
JSC::PutDirectIndexMode::PutDirectIndexLikePutDirect);
false);
return binding;
}

View File

@@ -92,55 +92,43 @@ const kDeferredTimeouts = Symbol("deferredTimeouts");
const kEmptyObject = Object.freeze(Object.create(null));
// These are declared as plain objects instead of `const enum` to prevent the
// TypeScript enum reverse-mapping pattern (e.g. `Enum[Enum["x"] = 0] = "x"`)
// from triggering setters on `Object.prototype` during module initialization.
// See: https://github.com/oven-sh/bun/issues/24336
export const ClientRequestEmitState = {
socket: 1,
prefinish: 2,
finish: 3,
response: 4,
} as const;
export type ClientRequestEmitState = (typeof ClientRequestEmitState)[keyof typeof ClientRequestEmitState];
export const enum ClientRequestEmitState {
socket = 1,
prefinish = 2,
finish = 3,
response = 4,
}
export const NodeHTTPResponseAbortEvent = {
none: 0,
abort: 1,
timeout: 2,
} as const;
export type NodeHTTPResponseAbortEvent = (typeof NodeHTTPResponseAbortEvent)[keyof typeof NodeHTTPResponseAbortEvent];
export const NodeHTTPIncomingRequestType = {
FetchRequest: 0,
FetchResponse: 1,
NodeHTTPResponse: 2,
} as const;
export type NodeHTTPIncomingRequestType =
(typeof NodeHTTPIncomingRequestType)[keyof typeof NodeHTTPIncomingRequestType];
export const NodeHTTPBodyReadState = {
none: 0,
pending: 1 << 1,
done: 1 << 2,
hasBufferedDataDuringPause: 1 << 3,
} as const;
export type NodeHTTPBodyReadState = (typeof NodeHTTPBodyReadState)[keyof typeof NodeHTTPBodyReadState];
export const enum NodeHTTPResponseAbortEvent {
none = 0,
abort = 1,
timeout = 2,
}
export const enum NodeHTTPIncomingRequestType {
FetchRequest,
FetchResponse,
NodeHTTPResponse,
}
export const enum NodeHTTPBodyReadState {
none,
pending = 1 << 1,
done = 1 << 2,
hasBufferedDataDuringPause = 1 << 3,
}
// Must be kept in sync with NodeHTTPResponse.Flags
export const NodeHTTPResponseFlags = {
socket_closed: 1 << 0,
request_has_completed: 1 << 1,
closed_or_completed: (1 << 0) | (1 << 1),
} as const;
export type NodeHTTPResponseFlags = (typeof NodeHTTPResponseFlags)[keyof typeof NodeHTTPResponseFlags];
export const enum NodeHTTPResponseFlags {
socket_closed = 1 << 0,
request_has_completed = 1 << 1,
export const NodeHTTPHeaderState = {
none: 0,
assigned: 1,
sent: 2,
} as const;
export type NodeHTTPHeaderState = (typeof NodeHTTPHeaderState)[keyof typeof NodeHTTPHeaderState];
closed_or_completed = socket_closed | request_has_completed,
}
export const enum NodeHTTPHeaderState {
none,
assigned,
sent,
}
function emitErrorNextTickIfErrorListenerNT(self, err, cb) {
process.nextTick(emitErrorNextTickIfErrorListener, self, err, cb);

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);
});

View File

@@ -1,77 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/24336
// require('http') should not trigger Object.prototype setters during module loading.
// Node.js produces no output for both CJS and ESM, and Bun should match that behavior.
test("require('http') does not trigger Object.prototype[0] setter", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Object.defineProperty(Object.prototype, '0', {
set() { console.log('SETTER_TRIGGERED'); }
});
require('http');
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("require('url') does not trigger Object.prototype[0] setter", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Object.defineProperty(Object.prototype, '0', {
set() { console.log('SETTER_TRIGGERED'); }
});
require('url');
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("require('util') does not trigger Object.prototype[0] setter", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
Object.defineProperty(Object.prototype, '0', {
set() { console.log('SETTER_TRIGGERED'); }
});
require('util');
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("");
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});