Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
e1afd658c7 fix(parser): function declarations inside labeled statements should hoist in sloppy mode
Per ECMAScript Annex B, function declarations inside labeled statements
in sloppy mode should hoist like regular function declarations, not like
block-scoped functions.

Previously, Bun incorrectly transformed:
```js
foo:
    function bar() { return "bar"; }
console.log(bar()); // ReferenceError: bar is not defined
```

Into:
```js
foo: {
  let bar = function() { return "bar"; };
}
console.log(bar()); // bar is undefined outside the block
```

Now the function declaration is preserved as-is, matching Node.js/V8 behavior.

Fixes #25737

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 12:20:47 +00:00
4 changed files with 142 additions and 2 deletions

View File

@@ -2204,7 +2204,7 @@ 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 (p.willUseRenamer() and symbol.kind == .hoisted_function and scope.kind != .label) {
// Block-level function declarations behave like "let" in strict mode
if (scope.strict_mode != .sloppy_mode) {
continue;

View File

@@ -810,7 +810,12 @@ pub fn Visit(
// This is only done for function declarations that are not generators
// or async functions, since this is a backwards-compatibility hack from
// Annex B of the JavaScript standard.
//
// However, function declarations inside labeled statements should NOT
// be treated as block-level functions. Per ECMAScript Annex B, they
// should hoist like regular function declarations in sloppy mode.
!p.current_scope.kindStopsHoisting() and
p.current_scope.kind != .label and
p.symbols.items[data.func.name.?.ref.?.innerIndex()].kind == .hoisted_function)
{
break :list_getter &before;

View File

@@ -688,7 +688,19 @@ pub fn VisitStmt(
else => {},
}
data.stmt = p.visitSingleStmt(data.stmt, StmtsKind.none);
// For function declarations inside labels in sloppy mode, we need special handling.
// Per ECMAScript Annex B, they should hoist like regular function declarations,
// not like block-scoped functions. We can't use visitSingleStmt because it would
// wrap the function in a block via stmtsToSingleStmt.
if (data.stmt.data == .s_function and p.current_scope.strict_mode == .sloppy_mode) {
var inner_stmts = ListManaged(Stmt).initCapacity(p.allocator, 1) catch unreachable;
inner_stmts.append(data.stmt) catch unreachable;
p.visitStmts(&inner_stmts, StmtsKind.none) catch unreachable;
// The function should remain as a single statement without block wrapping
data.stmt = if (inner_stmts.items.len == 1) inner_stmts.items[0] else p.stmtsToSingleStmt(data.stmt.loc, inner_stmts.items);
} else {
data.stmt = p.visitSingleStmt(data.stmt, StmtsKind.none);
}
p.popScope();
try stmts.append(stmt.*);

View File

@@ -0,0 +1,123 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("function declaration inside labeled statement should be accessible in sloppy mode", async () => {
using dir = tempDir("issue-25737", {
"test.cjs": `
foo:
function bar() { return "bar"; }
console.log(bar());
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.cjs"],
env: bunEnv,
cwd: String(dir),
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("bar");
expect(exitCode).toBe(0);
});
test("function declaration inside nested labeled statements should be accessible", async () => {
using dir = tempDir("issue-25737-nested", {
"test.cjs": `
outer:
inner:
function baz() { return "baz"; }
console.log(baz());
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.cjs"],
env: bunEnv,
cwd: String(dir),
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("baz");
expect(exitCode).toBe(0);
});
test("function declaration inside labeled statement with break should work", async () => {
using dir = tempDir("issue-25737-break", {
"test.cjs": `
let result = "";
foo: {
function bar() { return "bar"; }
result = bar();
break foo;
}
console.log(result);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.cjs"],
env: bunEnv,
cwd: String(dir),
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("bar");
expect(exitCode).toBe(0);
});
test("transpiler output should not wrap labeled function in block", async () => {
using dir = tempDir("issue-25737-transpile", {
"test.cjs": `
foo:
function bar() { return "bar"; }
console.log(bar());
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "--no-bundle", "test.cjs"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
// The output should NOT contain "{ function bar" or "{ let bar"
// It should be a simple labeled function declaration
expect(stdout).not.toContain("{ function bar");
expect(stdout).not.toContain("{ let bar");
expect(stdout).not.toContain("foo: {");
expect(stdout).toContain("foo:");
expect(stdout).toContain("function bar()");
expect(exitCode).toBe(0);
});