Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
2b54747f0e fix(transpiler): disable const inlining in switch cases to preserve TDZ
Switch cases all share the same scope and can be entered in any order,
so a const declared in one case may not have been initialized when
another case references it. The transpiler was inlining const values
across case boundaries, which incorrectly suppressed the TDZ
ReferenceError that should occur at runtime.

This matches esbuild's behavior of setting IsAfterConstLocalPrefix to
true at the start of a switch body, which prevents the const value
inlining optimization from applying inside switch statements.

Closes #18477

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 12:14:22 +00:00
2 changed files with 109 additions and 0 deletions

View File

@@ -1300,6 +1300,14 @@ pub fn VisitStmt(
const old_is_inside_Swsitch = p.fn_or_arrow_data_visit.is_inside_switch;
p.fn_or_arrow_data_visit.is_inside_switch = true;
defer p.fn_or_arrow_data_visit.is_inside_switch = old_is_inside_Swsitch;
// Disable const value inlining for all cases. Switch cases all
// share the same scope and can be entered in any order, so a const
// declared in one case may not have been initialized when another
// case references it. Inlining the value would incorrectly suppress
// the TDZ ReferenceError that should occur at runtime (issue #18477).
p.current_scope.is_after_const_local_prefix = true;
for (data.cases, 0..) |case, i| {
if (case.value) |val| {
data.cases[i].value = p.visitExpr(val);

View File

@@ -0,0 +1,101 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/18477
// Bun's transpiler was inlining const values across switch case boundaries,
// which suppressed the TDZ ReferenceError that should occur when a const
// declared in one case is referenced from another case that executes without
// the declaring case having been entered.
test("const in switch case should not be inlined across case boundaries", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
try {
switch ('A') {
case 'B':
const message = 'Started with A';
console.log(message);
case 'A':
console.log(message);
}
console.log("NO ERROR");
} catch(e) {
console.log(e.constructor.name);
}
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(stdout.trim()).toBe("ReferenceError");
expect(exitCode).toBe(0);
});
test("const in switch case with default should not be inlined", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
try {
switch ('C') {
case 'A':
const val = 100;
default:
console.log(val);
}
console.log("NO ERROR");
} catch(e) {
console.log(e.constructor.name);
}
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(stdout.trim()).toBe("ReferenceError");
expect(exitCode).toBe(0);
});
test("const inlining still works outside of switch", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const x = 42;
console.log(x);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(stdout.trim()).toBe("42");
expect(exitCode).toBe(0);
});