Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
9239bd79ab fix(parser): downgrade const reassignment in dead code from error to warning (#13992)
Per the ECMAScript specification, reassignment to a const binding is a
runtime TypeError, not a syntax error. Bun was incorrectly treating all
const reassignment as a parse-time error, preventing valid programs from
running when the offending assignment was in unreachable code (after
return, inside if(false), in never-called functions).

Changes:
- Downgrade const reassignment diagnostic from error to warning when not
  bundling and the const value is not inlined (matching esbuild behavior)
- Skip the const assignment check inside `with` statements where the
  identifier may resolve to a property of the `with` object at runtime

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 09:25:15 +00:00
2 changed files with 148 additions and 8 deletions

View File

@@ -99,7 +99,9 @@ pub fn VisitExpr(
// Handle assigning to a constant
if (in.assign_target != .none) {
if (p.symbols.items[result.ref.innerIndex()].kind == .constant) { // TODO: silence this for runtime transpiler
// Skip the const assignment check inside `with` statements because
// the identifier may resolve to a property of the `with` object at runtime
if (p.symbols.items[result.ref.innerIndex()].kind == .constant and !result.is_inside_with_scope) {
const r = js_lexer.rangeOfIdentifier(p.source, expr.loc);
var notes = p.allocator.alloc(logger.Data, 1) catch unreachable;
notes[0] = logger.Data{
@@ -107,25 +109,32 @@ pub fn VisitExpr(
.location = logger.Location.initOrNull(p.source, js_lexer.rangeOfIdentifier(p.source, result.declare_loc.?)),
};
const is_error = p.const_values.contains(result.ref) or p.options.bundle;
switch (is_error) {
true => p.log.addRangeErrorFmtWithNotes(
// Make this an error when bundling because we may need to convert
// this "const" into a "var" during bundling. Also make this an error
// when the constant is inlined because we will otherwise generate code
// with a syntax error.
if (p.const_values.contains(result.ref) or p.options.bundle) {
p.log.addRangeErrorFmtWithNotes(
p.source,
r,
p.allocator,
notes,
"Cannot assign to \"{s}\" because it is a constant",
.{name},
) catch unreachable,
false => p.log.addRangeErrorFmtWithNotes(
) catch unreachable;
} else {
// Per the ECMAScript specification, assignment to a const binding
// is a runtime TypeError, not a syntax error. Emit a warning
// instead of an error so that programs with unreachable const
// assignments can still run.
p.log.addRangeWarningFmtWithNotes(
p.source,
r,
p.allocator,
notes,
"This assignment will throw because \"{s}\" is a constant",
.{name},
) catch unreachable,
) catch unreachable;
}
} else if (p.exports_ref.eql(e_.ref)) {
// Assigning to `exports` in a CommonJS module must be tracked to undo the

View File

@@ -0,0 +1,131 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/13992
// Bun was too aggressive with const reassignment errors in dead/unreachable code.
// Per the ECMAScript specification, reassignment to a const binding is a runtime
// TypeError, not a syntax error. Programs with unreachable const assignments should
// still run.
test("const reassignment after return (unreachable code)", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
function b(){
return;
obj = 2;
}
const obj = 1;
b();
console.log(obj);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("1");
expect(exitCode).toBe(0);
});
test("const reassignment inside if(false) (dead code)", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
function b() {
if (false) {
obj = 2;
}
}
const obj = 1;
b();
console.log(obj);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("1");
expect(exitCode).toBe(0);
});
test("const reassignment in never-called function", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
"use strict";
function a() {
obj = 2;
}
const obj = 1;
console.log(obj);
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("1");
expect(exitCode).toBe(0);
});
test("const reassignment inside with statement resolves to property", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const obj = { obj: 2 };
with (obj) {
obj = 10;
};
console.log(JSON.stringify(obj));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"obj":10}');
expect(exitCode).toBe(0);
});
test("actual const reassignment still throws at runtime", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const obj = Math.random();
obj = 2;
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("TypeError");
expect(exitCode).not.toBe(0);
});