Files
bun.sh/test/regression/issue/25648.test.ts
robobun 7333500df8 fix(bundler): rename named function expressions when shadowing outer symbol (#26027)
## Summary
- Fixed a bug where named function expressions were not renamed when
their name shadowed an outer symbol that's referenced inside the
function body
- This caused infinite recursion at runtime when namespace imports were
inlined
- Particularly affected Svelte 5 apps in dev mode

## Test plan
- [x] Added regression test that reproduces the issue
- [x] Verified test fails with system bun and passes with fix
- [x] Ran bundler tests (bundler_regressions, bundler_naming,
bundler_edgecase, bundler_minify) - all pass

## Root cause
The bundler was skipping `function_args` scopes when renaming symbols.
This meant named function expression names (which are declared in the
function_args scope) were never considered for renaming when they
collided with outer symbols.

For example, this code:
```javascript
import * as $ from './lib';
$.doSomething(function get() {
  return $.get(123);  // Should call outer get
});
```

Would be bundled as:
```javascript
function get(x) { return x * 2; } // from lib
doSomething(function get() {
  return get(123);  // Calls itself - infinite recursion!
});
```

Instead of:
```javascript
function get(x) { return x * 2; }
doSomething(function get2() {  // Renamed to avoid collision
  return get(123);  // Correctly calls outer get
});
```

Fixes #25648

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-14 12:52:41 -08:00

225 lines
5.3 KiB
TypeScript

import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/25648
// Named function expression names should be renamed when they shadow an outer symbol
// that's referenced inside the function body. This prevents infinite recursion.
test("named function expression should be renamed when shadowing outer symbol", async () => {
using dir = tempDir("issue-25648", {
"lib.ts": `
export function get(x: number) {
return x * 2;
}
export function doSomething(fn: () => number) {
return fn();
}
`,
"index.ts": `
import * as $ from './lib';
export function test() {
return $.doSomething(function get() {
return $.get(123); // This should reference the outer get, not the function expression
});
}
console.log(test());
`,
});
// Bundle and run the code
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.ts", "--bundle", "--outfile=out.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
buildProc.stdout.text(),
buildProc.stderr.text(),
buildProc.exited,
]);
expect(buildStderr).toBe("");
expect(buildExitCode).toBe(0);
// Run the bundled output
await using runProc = Bun.spawn({
cmd: [bunExe(), "out.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [runStdout, runStderr, runExitCode] = await Promise.all([
runProc.stdout.text(),
runProc.stderr.text(),
runProc.exited,
]);
// Should print 246 (123 * 2), NOT cause infinite recursion
expect(runStdout.trim()).toBe("246");
expect(runStderr).toBe("");
expect(runExitCode).toBe(0);
});
test("named function expression with namespace import should not cause infinite recursion", async () => {
using dir = tempDir("issue-25648-2", {
"svelte-mock.ts": `
export function get<T>(store: { value: T }): T {
return store.value;
}
export function set<T>(store: { value: T }, value: T) {
store.value = value;
}
export function bind_value(
element: HTMLElement,
get_fn: () => string,
set_fn: (value: string) => void
) {
return get_fn();
}
`,
"index.ts": `
import * as $ from './svelte-mock';
const query = { value: "hello" };
// This pattern is generated by the Svelte compiler in dev mode
const result = $.bind_value(
{} as HTMLElement,
function get() {
return $.get(query); // Should call outer $.get, not this function
},
function set($$value: string) {
$.set(query, $$value);
}
);
console.log(result);
`,
});
// Bundle and run the code
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.ts", "--bundle", "--outfile=out.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
buildProc.stdout.text(),
buildProc.stderr.text(),
buildProc.exited,
]);
expect(buildStderr).toBe("");
expect(buildExitCode).toBe(0);
// Run the bundled output
await using runProc = Bun.spawn({
cmd: [bunExe(), "out.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [runStdout, runStderr, runExitCode] = await Promise.all([
runProc.stdout.text(),
runProc.stderr.text(),
runProc.exited,
]);
// Should print "hello", NOT cause "Maximum call stack size exceeded"
expect(runStdout.trim()).toBe("hello");
expect(runStderr).toBe("");
expect(runExitCode).toBe(0);
});
test("class expression name should be renamed when shadowing outer symbol", async () => {
using dir = tempDir("issue-25648-3", {
"lib.ts": `
export class Foo {
value = 42;
}
export function makeThing<T>(cls: new () => T): T {
return new cls();
}
`,
"index.ts": `
import * as $ from './lib';
export function test() {
return $.makeThing(class Foo extends $.Foo {
getValue() {
return this.value;
}
// Self-reference: uses the inner class name Foo
static create() {
return new Foo();
}
clone() {
return new Foo();
}
});
}
const instance = test();
console.log(instance.getValue());
// Test self-referencing static method
console.log((instance.constructor as any).create().getValue());
// Test self-referencing instance method
console.log(instance.clone().getValue());
`,
});
// Bundle and run the code
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "index.ts", "--bundle", "--outfile=out.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
buildProc.stdout.text(),
buildProc.stderr.text(),
buildProc.exited,
]);
expect(buildStderr).toBe("");
expect(buildExitCode).toBe(0);
// Run the bundled output
await using runProc = Bun.spawn({
cmd: [bunExe(), "out.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [runStdout, runStderr, runExitCode] = await Promise.all([
runProc.stdout.text(),
runProc.stderr.text(),
runProc.exited,
]);
// Should print 42 three times (getValue, static create().getValue, clone().getValue)
expect(runStdout.trim()).toBe("42\n42\n42");
expect(runStderr).toBe("");
expect(runExitCode).toBe(0);
});