mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
## 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>
225 lines
5.3 KiB
TypeScript
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);
|
|
});
|