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
5 changed files with 185 additions and 136 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

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

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