Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
028cfcb23d fix(parser): error on duplicate function declarations in strict mode block scopes
In strict mode, block-scoped function declarations should behave like
`let` bindings, so duplicate declarations in the same block scope must
be a SyntaxError. The parser's `canMergeSymbols` was incorrectly
allowing two `hoisted_function` symbols to merge in block scopes even
under strict mode.

Closes #14273

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 09:48:51 +00:00
4 changed files with 120 additions and 149 deletions

View File

@@ -137,7 +137,10 @@ pub fn canMergeSymbols(
if (Symbol.isKindHoistedOrFunction(new) and
Symbol.isKindHoistedOrFunction(existing) and
(scope.kind == .entry or scope.kind == .function_body or scope.kind == .function_args or
(new == existing and Symbol.isKindHoisted(existing))))
(new == existing and Symbol.isKindHoisted(existing) and
// In strict mode, block-scoped function declarations behave like `let` bindings
// and duplicates are a SyntaxError (ES2015+ B.3.2.1, 14.1.2).
!(scope.strict_mode != .sloppy_mode and new == .hoisted_function))))
{
return .replace_with_new;
}

View File

@@ -99,9 +99,7 @@ pub fn VisitExpr(
// Handle assigning to a constant
if (in.assign_target != .none) {
// 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) {
if (p.symbols.items[result.ref.innerIndex()].kind == .constant) { // TODO: silence this for runtime transpiler
const r = js_lexer.rangeOfIdentifier(p.source, expr.loc);
var notes = p.allocator.alloc(logger.Data, 1) catch unreachable;
notes[0] = logger.Data{
@@ -109,32 +107,25 @@ pub fn VisitExpr(
.location = logger.Location.initOrNull(p.source, js_lexer.rangeOfIdentifier(p.source, result.declare_loc.?)),
};
// 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(
const is_error = p.const_values.contains(result.ref) or p.options.bundle;
switch (is_error) {
true => p.log.addRangeErrorFmtWithNotes(
p.source,
r,
p.allocator,
notes,
"Cannot assign to \"{s}\" because it is a constant",
.{name},
) 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(
) catch unreachable,
false => p.log.addRangeErrorFmtWithNotes(
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

@@ -1,131 +0,0 @@
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

@@ -0,0 +1,108 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/14273
// In strict mode, duplicate plain function declarations in a block scope
// should be a SyntaxError (they behave like `let` bindings per ES2015+ spec).
test("strict mode: duplicate function declarations in block scope is SyntaxError", async () => {
const cases = [
// Basic block scope
`"use strict"; { function f(){} function f(){} }`,
// Nested block
`"use strict"; { { function f(){} function f(){} } }`,
// Inside if
`"use strict"; if(true){ function f(){} function f(){} }`,
// Inside for
`"use strict"; for(;;){ function f(){} function f(){} break; }`,
// Inside switch case
`"use strict"; switch(1){ case 1: function f(){} function f(){} }`,
// Function body with "use strict"
`function outer(){ "use strict"; { function f(){} function f(){} } }`,
];
for (const code of cases) {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", code],
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect({
code,
exitCode,
hasError: stderr.includes("has already been declared"),
}).toEqual({
code,
exitCode: 1,
hasError: true,
});
}
}, 30_000);
test("sloppy mode: duplicate function declarations in block scope is allowed", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `{ function f(){} function f(){} }`],
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("strict mode: duplicate async function declarations in block scope is SyntaxError", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `"use strict"; { async function f(){} async function f(){} }`],
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toInclude("has already been declared");
expect(exitCode).toBe(1);
});
test("strict mode: duplicate generator function declarations in block scope is SyntaxError", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `"use strict"; { function* f(){} function* f(){} }`],
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toInclude("has already been declared");
expect(exitCode).toBe(1);
});
test("strict mode: duplicate function declarations at top level is allowed", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `"use strict"; function f(){} function f(){}`],
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});
test("strict mode: duplicate var declarations in block scope is allowed", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `"use strict"; { var x = 1; var x = 2; }`],
env: bunEnv,
stderr: "pipe",
});
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
});