Compare commits

...

4 Commits

Author SHA1 Message Date
SUZUKI Sosuke
26125888bd Merge branch 'main' into claude/iife-folding 2026-01-30 12:39:46 +09:00
autofix-ci[bot]
8f66535291 [autofix.ci] apply automated fixes 2026-01-29 08:43:49 +00:00
Sosuke Suzuki
4fd14c9086 fix: skip IIFE folding for member access returns
Fixes a bug where inlining (() => obj.foo)() to obj.foo would change
the `this` binding when the result is called.

Also:
- Use Bun.build with files option in tests (per review)
- Rename misleading test name
- Add tests for member access edge cases
2026-01-29 17:41:55 +09:00
Sosuke Suzuki
9c803caf74 feat(minify): add IIFE folding optimization
Simplify immediately invoked function expressions during minification:
- `(() => {})()` → `void 0`
- `(() => expr)()` → `expr`
- `(() => { return expr })()` → `expr`
- `(() => { sideEffect() })()` → `(sideEffect(), void 0)`
- `(function() {})()` → `void 0`

Async/generator functions and functions with parameters are not folded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:41:55 +09:00
2 changed files with 213 additions and 0 deletions

View File

@@ -1509,8 +1509,104 @@ pub fn VisitExpr(
}
};
// IIFE folding optimization: simplify immediately invoked function expressions
// Reference: OXC's substitute_iife_call in substitute_alternate_syntax.rs:1599-1679
if (p.options.features.minify_syntax) {
if (tryFoldIIFE(p, e_, expr.loc)) |folded| {
return folded;
}
}
return expr;
}
/// Optimizes Immediately Invoked Function Expressions (IIFEs)
/// - `(() => {})()` → `void 0`
/// - `(() => expr)()` → `expr`
/// - `(() => { return expr })()` → `expr`
/// - `(() => { sideEffect() })()` → `(sideEffect(), void 0)`
/// - `(function() {})()` → `void 0`
fn tryFoldIIFE(p: *P, call: *E.Call, loc: logger.Loc) ?Expr {
// Condition 1: No arguments in the call
if (call.args.len != 0) return null;
// Condition 2: Not an optional chain
if (call.optional_chain != null) return null;
// Case A: Arrow function (() => ...)()
if (call.target.data.as(.e_arrow)) |arrow| {
// No parameters allowed
if (arrow.args.len != 0) return null;
// Skip async arrows (they return Promises)
if (arrow.is_async) return null;
const stmts = arrow.body.stmts;
// Case A1: Empty body → void 0
// (() => {})() → void 0
if (stmts.len == 0) {
return p.newExpr(E.Undefined{}, loc);
}
// Case A2: Single statement body
if (stmts.len == 1) {
const stmt = stmts[0];
switch (stmt.data) {
.s_return => |ret| {
if (ret.value) |value| {
// Skip if the return value is a member access (e_dot or e_index).
// Inlining (() => obj.foo)() to obj.foo would change `this` binding
// when the result is called: (() => obj.foo)()() should have
// `this === undefined`, but obj.foo() would have `this === obj`.
if (value.data == .e_dot or value.data == .e_index) {
return null;
}
// (() => { return expr })() → expr
// Also handles: (() => expr)() → expr
return value;
}
// (() => { return })() → void 0
return p.newExpr(E.Undefined{}, loc);
},
.s_expr => |expr_stmt| {
// (() => { sideEffect() })() → (sideEffect(), void 0)
return p.newExpr(E.Binary{
.op = .bin_comma,
.left = expr_stmt.value,
.right = p.newExpr(E.Undefined{}, loc),
}, loc);
},
else => return null,
}
}
return null;
}
// Case B: Function expression (function() {})()
if (call.target.data.as(.e_function)) |func| {
const f = &func.func;
// No parameters allowed
if (f.args.len != 0) return null;
// Skip async/generator functions (they return Promise/Generator)
if (f.flags.contains(.is_async) or f.flags.contains(.is_generator)) return null;
// Only handle empty body (to avoid `this` binding issues)
// (function() {})() → void 0
if (f.body.stmts.len == 0) {
return p.newExpr(E.Undefined{}, loc);
}
return null;
}
return null;
}
pub fn e_new(p: *P, expr: Expr, _: ExprIn) Expr {
const e_ = expr.data.e_new;
e_.target = p.visitExpr(e_.target);

View File

@@ -0,0 +1,117 @@
import { describe, expect, test } from "bun:test";
describe("IIFE folding", () => {
async function minify(code: string): Promise<string> {
const result = await Bun.build({
entrypoints: ["/input.js"],
minify: { syntax: true },
files: {
"/input.js": code,
},
});
if (!result.success) {
throw new Error(result.logs.map(l => l.message).join("\n"));
}
return (await result.outputs[0].text()).trim();
}
describe("arrow function IIFEs", () => {
test("empty arrow IIFE to void 0", async () => {
const code = await minify("export const x = (() => {})()");
expect(code).toContain("void 0");
expect(code).not.toContain("=>");
});
test("arrow expression IIFE inlined", async () => {
const code = await minify("export const x = (() => 42)()");
// Variable may be renamed, check the value is inlined
expect(code).toMatch(/=\s*42/);
expect(code).not.toContain("=>");
});
test("arrow expression with call inlined", async () => {
const code = await minify("export const x = (() => foo())()");
// Variable may be renamed, check the call is inlined
expect(code).toMatch(/=\s*foo\(\)/);
expect(code).not.toContain("=>");
});
test("arrow with return statement inlined", async () => {
const code = await minify("export const x = (() => { return 42 })()");
// Variable may be renamed, check the value is inlined
expect(code).toMatch(/=\s*42/);
expect(code).not.toContain("return");
});
test("arrow with return call inlined", async () => {
const code = await minify("export const x = (() => { return foo() })()");
// Variable may be renamed, check the call is inlined
expect(code).toMatch(/=\s*foo\(\)/);
expect(code).not.toContain("return");
});
test("arrow with expression statement becomes sequence", async () => {
const code = await minify("export const x = (() => { sideEffect() })()");
expect(code).toContain("sideEffect()");
expect(code).toContain("void 0");
});
test("nested IIFE in call argument", async () => {
const code = await minify("console.log((() => 42)())");
expect(code).toContain("console.log(42)");
expect(code).not.toContain("=>");
});
});
describe("function expression IIFEs", () => {
test("empty function IIFE to void 0", async () => {
const code = await minify("export const x = (function() {})()");
expect(code).toContain("void 0");
expect(code).not.toContain("function");
});
test("function with parameters NOT folded", async () => {
const code = await minify("export const x = (function(a) { return a + 1 })(5)");
expect(code).toContain("function");
});
});
describe("edge cases - should NOT be folded", () => {
test("async arrow NOT folded (returns Promise)", async () => {
const code = await minify("export const x = (async () => { await foo() })()");
expect(code).toContain("async");
});
test("arrow with arguments NOT folded", async () => {
const code = await minify("export const x = ((a) => a + 1)(5)");
expect(code).toContain("=>");
});
test("function with non-empty body NOT folded", async () => {
const code = await minify("export const x = (function() { return this.x })()");
expect(code).toContain("function");
});
test("generator function NOT folded", async () => {
const code = await minify("export const x = (function*() {})()");
expect(code).toContain("function*");
});
test("async function NOT folded", async () => {
const code = await minify("export const x = (async function() {})()");
expect(code).toContain("async");
});
test("member access return NOT folded (this binding)", async () => {
// (() => obj.foo)()() should have `this === undefined`
// but if inlined to obj.foo(), `this === obj`
const code = await minify("export const x = (() => obj.foo)()");
expect(code).toContain("=>");
});
test("index access return NOT folded (this binding)", async () => {
const code = await minify("export const x = (() => obj['foo'])()");
expect(code).toContain("=>");
});
});
});