mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
4 Commits
claude/fix
...
claude/arr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6441149674 | ||
|
|
e5c8da3c99 | ||
|
|
55fe522910 | ||
|
|
0b13ba1b7e |
@@ -269,6 +269,12 @@ pub const Arrow = struct {
|
||||
has_rest_arg: bool = false,
|
||||
prefer_expr: bool = false, // Use shorthand if true and "Body" is a single return statement
|
||||
|
||||
/// When minify_syntax is enabled and this arrow is a candidate for the
|
||||
/// `() => obj.method()` -> `obj.method.bind(obj)` transformation, this
|
||||
/// stores the ref of the receiver identifier (obj). The printer will check
|
||||
/// if this symbol was_assigned_to and only apply the transformation if not.
|
||||
bind_call_target_ref: ?Ref = null,
|
||||
|
||||
pub const noop_return_undefined: Arrow = .{
|
||||
.args = &.{},
|
||||
.body = .{
|
||||
|
||||
@@ -122,6 +122,7 @@ pub fn NewParser_(
|
||||
pub const visitClass = astVisit.visitClass;
|
||||
pub const visitStmts = astVisit.visitStmts;
|
||||
pub const visitAndAppendStmt = astVisit.visitAndAppendStmt;
|
||||
pub const tryMarkArrowForBindCallTransform = astVisit.tryMarkArrowForBindCallTransform;
|
||||
|
||||
pub const BinaryExpressionVisitor = @import("./visitBinaryExpression.zig").CreateBinaryExpressionVisitor(parser_feature__typescript, parser_feature__jsx, parser_feature__scan_only).BinaryExpressionVisitor;
|
||||
|
||||
|
||||
@@ -154,6 +154,31 @@ pub fn Visit(
|
||||
return decs;
|
||||
}
|
||||
|
||||
/// Check if this arrow is a candidate for the `() => obj.method()` to
|
||||
/// `obj.method.bind(obj)` transformation. If so, store the receiver ref
|
||||
/// in the arrow so the printer can check if the symbol was assigned to.
|
||||
///
|
||||
/// NOTE: This optimization is currently DISABLED because it can change
|
||||
/// semantics in cases where:
|
||||
/// 1. The property (method) is reassigned after the arrow is created
|
||||
/// 2. The property is a getter that returns different values on each access
|
||||
/// 3. Constructability differs: arrows are not constructable but bound
|
||||
/// functions derived from regular methods may be
|
||||
///
|
||||
/// To safely enable this optimization, we would need to track:
|
||||
/// - Property assignments to the receiver object
|
||||
/// - Whether the property is defined as a getter
|
||||
/// - Whether the object escapes to code that could modify it
|
||||
/// - Whether the arrow could be used with `new`
|
||||
///
|
||||
/// For now, we conservatively disable the transformation entirely.
|
||||
pub fn tryMarkArrowForBindCallTransform(p: *P, arrow: *E.Arrow) void {
|
||||
_ = p;
|
||||
_ = arrow;
|
||||
// Disabled - see comment above for rationale
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn visitDecls(noalias p: *P, decls: []G.Decl, was_const: bool, comptime is_possibly_decl_to_remove: bool) usize {
|
||||
var j: usize = 0;
|
||||
var out_decls = decls;
|
||||
|
||||
@@ -1571,6 +1571,18 @@ 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);
|
||||
|
||||
// Mark arrows that are candidates for the `() => obj.method()` to
|
||||
// `obj.method.bind(obj)` transformation. The actual transformation is
|
||||
// deferred to the printer, which can check if the captured symbol was
|
||||
// assigned to anywhere in the code.
|
||||
if (p.options.features.minify_syntax and
|
||||
e_.args.len == 0 and
|
||||
!e_.is_async and
|
||||
e_.body.stmts.len == 1)
|
||||
{
|
||||
p.tryMarkArrowForBindCallTransform(e_);
|
||||
}
|
||||
|
||||
if (react_hook_data) |*hook| try_mark_hook: {
|
||||
const stmts = p.nearest_stmt_list orelse break :try_mark_hook;
|
||||
bun.handleOom(stmts.append(p.getReactRefreshHookSignalDecl(hook.signature_cb)));
|
||||
|
||||
@@ -2565,6 +2565,26 @@ fn NewPrinter(
|
||||
}
|
||||
},
|
||||
.e_arrow => |e| {
|
||||
// Optimization: Convert `() => obj.method()` to `obj.method.bind(obj)`
|
||||
// when the receiver symbol was never assigned to.
|
||||
if (e.bind_call_target_ref) |target_ref| {
|
||||
if (p.symbols().get(target_ref)) |symbol| {
|
||||
// Only transform if the symbol was never assigned to.
|
||||
// For const/unbound, has_been_assigned_to is always false.
|
||||
// For hoisted (function params, var), it's true if assigned anywhere.
|
||||
if (!symbol.has_been_assigned_to) {
|
||||
// Get the call expression from the body
|
||||
const call = e.body.stmts[0].data.s_return.value.?.data.e_call;
|
||||
// Print: target.method.bind(target)
|
||||
p.printExpr(call.target, .postfix, ExprFlag.Set{});
|
||||
p.print(".bind(");
|
||||
p.printSymbol(target_ref);
|
||||
p.print(")");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrap = level.gte(.assign);
|
||||
|
||||
if (wrap) {
|
||||
|
||||
@@ -1192,4 +1192,391 @@ describe("bundler", () => {
|
||||
stdout: "object\nobject\nobject",
|
||||
},
|
||||
});
|
||||
|
||||
// Arrow to bind optimization tests
|
||||
// NOTE: The arrow-to-bind transformation is currently DISABLED because
|
||||
// it can change semantics when properties are reassigned or are getters.
|
||||
// These tests verify the transformation is NOT applied.
|
||||
itBundled("minify/ArrowToBindConstIdentifier", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { value: 42, method() { return this.value; } };
|
||||
const fn = () => obj.method();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformUnboundGlobal", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const fn = () => console.log();
|
||||
fn();
|
||||
console.log("done");
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform unbound globals - they could be reassigned externally
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "\ndone",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindComputedProperty", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { myMethod() { return this.value; }, value: 99 };
|
||||
const key = "myMethod";
|
||||
const fn = () => obj[key]();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "99",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformLetReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
let obj = { method() { return "first"; } };
|
||||
const fn = () => obj.method();
|
||||
obj = { method() { return "second"; } };
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because obj is reassigned
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "second",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindLetNotReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
let obj = { value: 42, method() { return this.value; } };
|
||||
const fn = () => obj.method();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindFunctionParam", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
function fetch(init) {
|
||||
return () => init.signal();
|
||||
}
|
||||
console.log(fetch({ signal: () => 555 })());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Optimization disabled - should NOT transform
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "555",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformFunctionParamReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
function fetch(init) {
|
||||
const cb = () => init.signal();
|
||||
init = { signal: () => 666 };
|
||||
return cb;
|
||||
}
|
||||
console.log(fetch({ signal: () => 555 })());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because init is reassigned
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "666",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformWithArgs", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method(x) { return x * 2; } };
|
||||
const fn = () => obj.method(21);
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because the call has arguments
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformArrowWithParams", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return 123; } };
|
||||
const fn = (x) => obj.method();
|
||||
console.log(fn(1));
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because the arrow has parameters
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "123",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformAsync", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { async method() { return 456; } };
|
||||
const fn = async () => obj.method();
|
||||
fn().then(console.log);
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because the arrow is async
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/async\s*\(\)\s*=>/);
|
||||
},
|
||||
run: {
|
||||
stdout: "456",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformOptionalChain", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return 789; } };
|
||||
const fn = () => obj?.method();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because of optional chaining
|
||||
expect(code).not.toContain(".bind(");
|
||||
expect(code).toMatch(/\?\.\w+\(\)/);
|
||||
},
|
||||
run: {
|
||||
stdout: "789",
|
||||
},
|
||||
});
|
||||
|
||||
itBundled("minify/ArrowToBindNoTransformOptionalCall", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return 321; } };
|
||||
const fn = () => obj.method?.();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because of optional call
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "321",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformThisTarget", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
class MyClass {
|
||||
value = 42;
|
||||
method() { return this.value; }
|
||||
getMethod() {
|
||||
// Arrow captures 'this' from enclosing context
|
||||
// Cannot transform because 'this' is not an identifier
|
||||
return () => this.method();
|
||||
}
|
||||
}
|
||||
const obj = new MyClass();
|
||||
const fn = obj.getMethod();
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because target is 'this', not a bound identifier
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "42",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformArgumentsAccess", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { greet(name) { return "Hello, " + name; } };
|
||||
function test() {
|
||||
// Arrow that uses arguments from enclosing function
|
||||
const fn = () => obj.greet(arguments[0]);
|
||||
return fn();
|
||||
}
|
||||
console.log(test("World"));
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because call has arguments
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "Hello, World",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformNewTarget", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = {
|
||||
check() { return "method called"; }
|
||||
};
|
||||
function MyConstructor() {
|
||||
// Arrow that references new.target
|
||||
this.fn = () => {
|
||||
if (new.target) {
|
||||
return obj.check();
|
||||
}
|
||||
return "no new.target";
|
||||
};
|
||||
}
|
||||
const instance = new MyConstructor();
|
||||
console.log(instance.fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform - arrow body has multiple statements (if/else logic)
|
||||
// and references new.target which arrows inherit from enclosing scope
|
||||
expect(code).not.toContain("obj.check.bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "method called",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformPropertyReassigned", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
const obj = { method() { return "original"; } };
|
||||
const fn = () => obj.method();
|
||||
obj.method = () => "reassigned";
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because obj.method is reassigned after the arrow is created
|
||||
// If we transform to obj.method.bind(obj), it would capture the original method
|
||||
// But the arrow should call the reassigned method
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "reassigned",
|
||||
},
|
||||
});
|
||||
itBundled("minify/ArrowToBindNoTransformGetterProperty", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
let callCount = 0;
|
||||
const obj = {
|
||||
get method() {
|
||||
callCount++;
|
||||
return () => "from getter " + callCount;
|
||||
}
|
||||
};
|
||||
const fn = () => obj.method();
|
||||
console.log(fn());
|
||||
console.log(fn());
|
||||
`,
|
||||
},
|
||||
minifySyntax: true,
|
||||
target: "bun",
|
||||
onAfterBundle(api) {
|
||||
const code = api.readFile("/out.js");
|
||||
// Should NOT transform because method is a getter
|
||||
// bind() would only call the getter once, but arrow calls it each time
|
||||
expect(code).not.toContain(".bind(");
|
||||
},
|
||||
run: {
|
||||
stdout: "from getter 1\nfrom getter 2",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user