Skip to content
Open
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
14 changes: 6 additions & 8 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,7 +1085,11 @@
return;
}
}
} else if let Some(array) = target.data.as_e_array() {
} else if !is_call_target && let Some(array) = target.data.as_e_array() {

Check failure on line 1088 in src/js_parser/visit/visit_expr.rs

View check run for this annotation

Claude / Claude Code Review

Array-index fold still fires for delete targets: `delete [obj.m][0]` becomes `delete obj.m`

The same fold still fires for `delete` targets: `delete [obj.m][0]` (a no-op on `obj`) folds to `delete obj.m` (deletes the property), and `delete [x][0]` folds to `delete x`, a strict-mode SyntaxError. The sibling `{f: x}.f` fold this PR cites already gates on `!is_delete_target()`; adding the same gate here also requires setting `p.delete_target = e_.value.data` in the `Op::UnDelete` visit arm — it's currently never assigned, so `is_delete_target` at line 888 is always false.
Comment thread
robobun marked this conversation as resolved.
// `[x][0]()` calls `x` with `this` bound to the array
// literal itself (GetThisValue of the Reference), which
// neither `x()` nor `(0, x)()` reproduces, so the fold
// must not fire for call targets.
let int: usize = number.value() as usize;
// [x][0] -> x
// ['a', 'b', 'c'][1] -> 'b'
Expand All @@ -1105,13 +1109,7 @@
return;
}
if inlined.can_be_inlined_from_property_access() {
// "[obj.m][0]()" => "(0, obj.m)()"
*e = if is_call_target && inlined.has_value_for_this_in_call() {
p.new_expr(E::Number::new(0.0), expr.loc)
.join_with_comma(inlined)
} else {
inlined
};
*e = inlined;
return;
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/jsc/RuntimeTranspilerCache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ bun_core::declare_scope!(cache, visible);
/// Version 25: Every ModuleInfo record carries a trailing FetchParameters slot
/// so ImportEntry/ExportEntry/StarExportEntry moduleRequestType matches JSC's
/// after WebKit 90b2ecf79ae3 keyed m_loadedModules on (specifier, type).
const EXPECTED_VERSION: u32 = 25;
/// Version 26: The `[x][0] -> x` fold no longer fires in call position, so
/// `[obj.m][0]()` keeps the array literal as `this` instead of `(0, obj.m)()`.
const EXPECTED_VERSION: u32 = 26;

/// Source files smaller than this are not written to / read from the on-disk
/// transpiler cache. Originally 50 KiB, which excluded almost every file in a
Expand Down
34 changes: 27 additions & 7 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,15 +192,23 @@ describe("Bun.Transpiler", () => {
ts.expectPrintedMin_("x = ({f: y}).f", "x = y");
ts.expectPrintedMin_("x = new ({f: C}).f()", "x = new C");
});
it("bails out or strips `this` when the index is a call/assignment target", () => {
it("bails out when the index is a call/assignment target", () => {
// `[obj.m][0]()` calls through a Reference into the temporary array,
// so `this` is the array; inlining to `obj.m()` would bind `this` to
// `obj`. Match the sibling folds and emit `(0, obj.m)()`.
ts.expectPrintedMin_("x = [obj.m][0]()", "x = (0, obj.m)()");
ts.expectPrintedMin_("x = [obj[m]][0]()", "x = (0, obj[m])()");
// so `this` is the array literal itself. Neither `obj.m()` nor
// `(0, obj.m)()` reproduces that, so the fold must not fire in call
// position at all. That includes plain identifiers and function
// expressions: `[y][0]()` calls `y` with `this` set to the array.
ts.expectPrintedMin_("x = [obj.m][0]()", "x = [obj.m][0]()");
ts.expectPrintedMin_("x = [obj[m]][0]()", "x = [obj[m]][0]()");
ts.expectPrintedMin_("x = [obj.m][0]", "x = obj.m");
ts.expectPrintedMin_("x = [y][0]()", "x = y()");
ts.expectPrintedMin_("x = [() => y][0]()", "x = (() => y)()");
ts.expectPrintedMin_("x = [y][0]()", "x = [y][0]()");
ts.expectPrintedMin_("x = [() => y][0]()", "x = [() => y][0]()");
ts.expectPrintedMin_("x = [obj.m][0]?.()", "x = [obj.m][0]?.()");
ts.expectPrintedMin_("x = [obj.m]?.[0]()", "x = [obj.m][0]()");
ts.expectPrintedMin_("x = [0, y][1]()", "x = [0, y][1]()");
// `new` does not thread `this` through the callee Reference, so the
// fold still fires there.
ts.expectPrintedMin_("x = new [C][0]()", "x = new C");

// `[x][0] = v` writes into the temporary, not `x`. Same for `"s"[n]`.
ts.expectPrintedMin_("[obj.p][0] = 5", "[obj.p][0] = 5");
Expand Down Expand Up @@ -230,6 +238,13 @@ describe("Bun.Transpiler", () => {
check("chain pure", () => [[0, /* @__PURE__ */ a?.()]][0]?.[1].c, "TypeError");
var obj = { n: "obj", m() { return this === obj; } };
check("this", () => [obj.m][0](), "=> false");
obj.who = function () { return Array.isArray(this); };
var who = obj.who;
check("this arr dot", () => [obj.who][0](), "=> true");
check("this arr ident", () => [who][0](), "=> true");
check("this arr opt", () => [obj.who][0]?.(), "=> true");
check("this arr multi", () => [0, who][1](), "=> true");
check("this arr fn", () => [function () { return Array.isArray(this); }][0](), "=> true");
var o2 = { p: 1 };
check("assign", () => ([o2.p][0] = 5, o2.p), "=> 1");
var ab = { b: class {} };
Expand All @@ -252,6 +267,11 @@ describe("Bun.Transpiler", () => {
"chain cont: ok",
"chain pure: ok",
"this: ok",
"this arr dot: ok",
"this arr ident: ok",
"this arr opt: ok",
"this arr multi: ok",
"this arr fn: ok",
"assign: ok",
"new obj: ok",
]);
Expand Down