Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
dc3ba54f26 fix(transpiler): don't inline const values into function bodies
Bun's const-inlining optimization was replacing variable references
with literal values inside function bodies, changing the observable
result of Function.prototype.toString(). This broke patterns that
rely on toString() + eval() (serialization for workers, RPC, etc.).

Only applies to the runtime transpiler (non-bundler mode) to preserve
toString() fidelity. The bundler path is unchanged since users
explicitly opt into minification there.

Closes #12710

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-19 09:59:23 +00:00
3 changed files with 165 additions and 0 deletions

View File

@@ -57,6 +57,14 @@ pub fn Visit(
p.fn_or_arrow_data_visit = FnOrArrowDataVisit{ .is_async = func.flags.contains(.is_async) };
p.fn_only_data_visit = FnOnlyDataVisit{ .is_this_nested = true, .arguments_ref = func.arguments_ref };
// When not bundling, don't carry over const values from the outer scope into
// function bodies. Inlining them would change the observable result of
// Function.prototype.toString(). See https://github.com/oven-sh/bun/issues/12710
const old_const_values = p.const_values;
if (!p.options.bundle) {
p.const_values = .{};
}
if (func.name) |name| {
if (name.ref) |name_ref| {
p.recordDeclaredSymbol(name_ref) catch unreachable;
@@ -100,6 +108,7 @@ pub fn Visit(
p.fn_or_arrow_data_visit = old_fn_or_arrow_data;
p.fn_only_data_visit = old_fn_only_data;
p.const_values = old_const_values;
return func;
}

View File

@@ -1568,6 +1568,14 @@ pub fn VisitExpr(
.is_async = e_.is_async,
};
// When not bundling, don't carry over const values from the outer scope into
// arrow function bodies. Inlining them would change the observable result of
// Function.prototype.toString(). See https://github.com/oven-sh/bun/issues/12710
const old_const_values = p.const_values;
if (!p.options.bundle) {
p.const_values = .{};
}
// Mark if we're inside an async arrow function. This value should be true
// even if we're inside multiple arrow functions and the closest inclosing
// arrow function isn't async, as long as at least one enclosing arrow
@@ -1600,6 +1608,7 @@ pub fn VisitExpr(
p.fn_only_data_visit.is_inside_async_arrow_fn = old_inside_async_arrow_fn;
p.fn_or_arrow_data_visit = std.mem.bytesToValue(@TypeOf(p.fn_or_arrow_data_visit), &old_fn_or_arrow_data);
p.const_values = old_const_values;
if (react_hook_data) |*hook| try_mark_hook: {
const stmts = p.nearest_stmt_list orelse break :try_mark_hook;

View File

@@ -0,0 +1,147 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/12710
// Const-inlining should not change the observable result of Function.prototype.toString()
// by replacing variable references with literal values inside function bodies.
test("const values are not inlined into function bodies (require + eval toString)", async () => {
using dir = tempDir("issue-12710", {
"entry.js": `
const { log } = require("./helper");
const hi = "hi";
log(() => console.log(hi));
`,
"helper.js": `
export const log = (fun) => {
try {
eval("(" + fun.toString() + ")()");
console.log("NO_ERROR");
} catch (e) {
console.log(e.constructor.name + ": " + e.message);
}
};
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The eval'd function should throw ReferenceError because `hi` is not
// defined in the eval scope. If const inlining replaced `hi` with `"hi"`,
// this would incorrectly print "hi" instead.
expect(stdout.trim()).toBe("ReferenceError: hi is not defined");
expect(exitCode).toBe(0);
});
test("const values are still inlined at the same scope level", async () => {
using dir = tempDir("issue-12710-same-scope", {
"entry.js": `
const hi = "hi";
console.log(hi);
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("hi");
expect(exitCode).toBe(0);
});
test("const values declared inside function bodies are still inlined within that function", async () => {
using dir = tempDir("issue-12710-inner", {
"entry.js": `
function foo() {
const x = "hello";
console.log(x);
}
foo();
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("hello");
expect(exitCode).toBe(0);
});
test("toString() preserves variable references in arrow functions", async () => {
using dir = tempDir("issue-12710-tostring", {
"entry.js": `
const hi = "hi";
const fn = () => console.log(hi);
// The toString should contain the identifier 'hi', not the literal '"hi"'
const str = fn.toString();
console.log(str.includes("hi") ? "has_reference" : "no_reference");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("has_reference");
expect(exitCode).toBe(0);
});
test("let variables are not inlined (unchanged behavior)", async () => {
using dir = tempDir("issue-12710-let", {
"entry.js": `
const { log } = require("./helper");
let hi = "hi";
log(() => console.log(hi));
`,
"helper.js": `
export const log = (fun) => {
try {
eval("(" + fun.toString() + ")()");
console.log("NO_ERROR");
} catch (e) {
console.log(e.constructor.name + ": " + e.message);
}
};
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "entry.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe("ReferenceError: hi is not defined");
expect(exitCode).toBe(0);
});