Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/ast/visit.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
9 changes: 9 additions & 0 deletions src/ast/visitExpr.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
147 changes: 147 additions & 0 deletions test/regression/issue/12710.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});