diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs index 260a937e154b..bc0560e39569 100644 --- a/src/js_parser/visit/visit_expr.rs +++ b/src/js_parser/visit/visit_expr.rs @@ -1085,7 +1085,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O return; } } - } else if let Some(array) = target.data.as_e_array() { + } else if !is_call_target && let Some(array) = target.data.as_e_array() { + // `[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' @@ -1105,13 +1109,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O 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; } } diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs index 2eee3816433a..0eae2299c098 100644 --- a/src/jsc/RuntimeTranspilerCache.rs +++ b/src/jsc/RuntimeTranspilerCache.rs @@ -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 diff --git a/test/bundler/transpiler/transpiler.test.js b/test/bundler/transpiler/transpiler.test.js index 75ff51849b6f..643a7f58e398 100644 --- a/test/bundler/transpiler/transpiler.test.js +++ b/test/bundler/transpiler/transpiler.test.js @@ -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"); @@ -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 {} }; @@ -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", ]);