Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
2dadc6670b fix(parser): block-scoped function declarations incorrectly hoisted in strict mode
In strict mode, function declarations inside block statements should be
block-scoped (like `let`). The `hoistSymbols` pass had a guard
(`willUseRenamer()`) that prevented the strict mode check from applying
when not bundling or minifying identifiers. This caused block-level
functions to be hoisted to the enclosing scope even in strict mode,
making them accessible from outside the block.

The fix moves the `willUseRenamer()` guard to only protect the sloppy
mode transformation (which creates separate hoisted/let symbols for the
renamer), while always applying the strict mode block-scoping rule.

Closes #14715

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 09:43:47 +00:00
2 changed files with 111 additions and 26 deletions

View File

@@ -2213,37 +2213,39 @@ pub fn NewParser_(
var is_sloppy_mode_block_level_fn_stmt = false;
const original_member_ref = value.ref;
if (p.willUseRenamer() and symbol.kind == .hoisted_function) {
if (symbol.kind == .hoisted_function) {
// Block-level function declarations behave like "let" in strict mode
if (scope.strict_mode != .sloppy_mode) {
continue;
}
// In sloppy mode, block level functions behave like "let" except with
// an assignment to "var", sort of. This code:
//
// if (x) {
// f();
// function f() {}
// }
// f();
//
// behaves like this code:
//
// if (x) {
// let f2 = function() {}
// var f = f2;
// f2();
// }
// f();
//
const hoisted_ref = p.newSymbol(.hoisted, symbol.original_name) catch unreachable;
symbols = p.symbols.items;
bun.handleOom(scope.generated.append(p.allocator, hoisted_ref));
p.hoisted_ref_for_sloppy_mode_block_fn.put(p.allocator, value.ref, hoisted_ref) catch unreachable;
value.ref = hoisted_ref;
symbol = &symbols[hoisted_ref.innerIndex()];
is_sloppy_mode_block_level_fn_stmt = true;
if (p.willUseRenamer()) {
// In sloppy mode, block level functions behave like "let" except with
// an assignment to "var", sort of. This code:
//
// if (x) {
// f();
// function f() {}
// }
// f();
//
// behaves like this code:
//
// if (x) {
// let f2 = function() {}
// var f = f2;
// f2();
// }
// f();
//
const hoisted_ref = p.newSymbol(.hoisted, symbol.original_name) catch unreachable;
symbols = p.symbols.items;
bun.handleOom(scope.generated.append(p.allocator, hoisted_ref));
p.hoisted_ref_for_sloppy_mode_block_fn.put(p.allocator, value.ref, hoisted_ref) catch unreachable;
value.ref = hoisted_ref;
symbol = &symbols[hoisted_ref.innerIndex()];
is_sloppy_mode_block_level_fn_stmt = true;
}
}
if (hash == null) hash = Scope.getMemberHash(name);

View File

@@ -0,0 +1,83 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("block-scoped function declarations not accessible outside block in strict mode", async () => {
using dir = tempDir("issue-14715", {
"index.js": `"use strict";
try { f; console.log("BUG"); } catch(e) { console.log("PASS: " + e.message); }
{ function f() {} }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("PASS:");
expect(stdout).not.toContain("BUG");
expect(exitCode).toBe(0);
});
test("bare reference to block-scoped function throws ReferenceError in strict mode", async () => {
using dir = tempDir("issue-14715", {
"index.js": `"use strict";
f;
{ function f() {} }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("ReferenceError");
expect(exitCode).not.toBe(0);
});
test("block-scoped function in ESM (with export) not accessible outside block", async () => {
using dir = tempDir("issue-14715", {
"index.mjs": `try { f; console.log("BUG"); } catch(e) { console.log("PASS: " + e.message); }
{ function f() {} }
export {};`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.mjs"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("PASS:");
expect(stdout).not.toContain("BUG");
expect(exitCode).toBe(0);
});
test("block-scoped function accessible inside block in strict mode", async () => {
using dir = tempDir("issue-14715", {
"index.js": `"use strict";
{ function f() { return 42; } console.log("RESULT: " + f()); }`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "index.js"],
cwd: String(dir),
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("RESULT: 42");
expect(exitCode).toBe(0);
});