Skip to content
Merged
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
5 changes: 5 additions & 0 deletions src/ast/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ impl Expr {
// https://github.com/oven-sh/bun/issues/2594
Data::ESpread(_) => false,
Data::EMissing(_) => false,
// `[[a?.b]][0]?.[0].c` must not become `a?.b.c`: inlining would
// splice this chain onto the parent's `?.` continuation.
Comment thread
robobun marked this conversation as resolved.
Data::EDot(e) => e.optional_chain.is_none(),
Data::EIndex(e) => e.optional_chain.is_none(),
Data::ECall(e) => e.optional_chain.is_none(),
_ => true,
}
}
Expand Down
1 change: 1 addition & 0 deletions src/js_parser/fold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
name,
)
&& name != b"__proto__"
&& value.can_be_inlined_from_property_access()
{
return Some(value);
}
Expand Down
36 changes: 21 additions & 15 deletions src/js_parser/visit/visit_expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
let target = e_.target.unwrap_inlined();
let index = e_.index.unwrap_inlined();

if p.options.features.minify_syntax {
// `[x][0] = v` writes into the temporary, not `x`.
if p.options.features.minify_syntax && in_.assign_target == js_ast::AssignTarget::None {
if let Some(number) = index.data.as_e_number() {
if number.value() >= 0.0
&& number.value() < (usize::MAX as f64)
Expand Down Expand Up @@ -1085,29 +1086,34 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O
}
}
} else if let Some(array) = target.data.as_e_array() {
let int: usize = number.value() as usize;
// [x][0] -> x
if array.items.len_u32() == 1 && number.value() == 0.0 {
let inlined = *array.items.at(0);
if inlined.can_be_inlined_from_property_access() {
*e = inlined;
return;
}
}

// ['a', 'b', 'c'][1] -> 'b'
let int: usize = number.value() as usize;
if int < array.items.len_u32() as usize
let inlined = if array.items.len_u32() == 1 && int == 0 {
Some(*array.items.at(0))
} else if int < array.items.len_u32() as usize
&& p.expr_can_be_removed_if_unused(&target)
{
let inlined = *array.items.at(int);
Some(*array.items.at(int))
} else {
None
};
if let Some(inlined) = inlined {
// ['a', , 'c'][1] -> undefined
if matches!(inlined.data, Data::EMissing(..)) {
*e = p.new_expr(E::Undefined {}, inlined.loc);
return;
}
debug_assert!(inlined.can_be_inlined_from_property_access());
*e = inlined;
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
};
return;
}
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
}
}
}
Expand Down
118 changes: 118 additions & 0 deletions test/bundler/transpiler/transpiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,124 @@ describe("Bun.Transpiler", () => {
it("works nested", () => {
ts.expectPrintedMin_('const a = ["hey"][0][0];', 'const a = "h"');
});
it("bails out when the array item is an optional chain", () => {
// Folding `[a?.b][0]` to `a?.b` is unsafe when the result lands as the
// target of a surrounding optional-chain continuation: the two chains
// would be spliced into one. `[[a?.b]][0]?.[0].c` must not become
// `a?.b.c`, which short-circuits to `undefined` for `a == null` instead
// of throwing on the trailing `.c`.
ts.expectPrintedMin_("x = [[a?.b]][0]?.[0].c", "x = [a?.b]?.[0].c");
ts.expectPrintedMin_("x = [[a?.b]][0]?.[0]()", "x = [a?.b]?.[0]()");
ts.expectPrintedMin_("x = [[a?.b]][0]?.[0][c]", "x = [a?.b]?.[0][c]");
ts.expectPrintedMin_("x = ({ f: [a?.b] }).f?.[0].c", "x = [a?.b]?.[0].c");
ts.expectPrintedMin_("x = [[a?.[b]]][0]?.[0].c", "x = [a?.[b]]?.[0].c");
ts.expectPrintedMin_("x = [[a?.()]][0]?.[0].c", "x = [a?.()]?.[0].c");
// Continuation (not Start) on the inlined item's outermost node:
ts.expectPrintedMin_("x = [[a?.b.c]][0]?.[0].d", "x = [a?.b.c]?.[0].d");
ts.expectPrintedMin_("x = [[a?.b[c]]][0]?.[0].d", "x = [a?.b[c]]?.[0].d");
ts.expectPrintedMin_("x = [[a?.b()]][0]?.[0].d", "x = [a?.b()]?.[0].d");
// Multi-item path (expr_can_be_removed_if_unused): a @__PURE__ optional
// call is removable, so the second fold arm sees it.
ts.expectPrintedMin_("x = [[0, /* @__PURE__ */ a?.()]][0]?.[1].c", "x = [0, a?.()]?.[1].c");
ts.expectPrintedMin_("x = [0, /* @__PURE__ */ a?.()][1]", "x = [0, a?.()][1]");

// The outer `?.` on an array literal is dropped at parse time, so these
// reach the fold with `optional_chain == None` on the index and the
// printer adds the `(a?.b)` wrapper itself. Keep bailing on the fold so
// the wrapper isn't load-bearing.
ts.expectPrintedMin_("x = [a?.b][0]", "x = [a?.b][0]");
ts.expectPrintedMin_("x = [a?.b]?.[0]", "x = [a?.b][0]");
ts.expectPrintedMin_("x = [a?.b][0].c", "x = [a?.b][0].c");
ts.expectPrintedMin_("x = [a?.b]?.[0].c", "x = [a?.b][0].c");
ts.expectPrintedMin_("x = [a?.b][0]()", "x = [a?.b][0]()");

// Same bailout protects LHS / delete / new / tagged-template positions
// from becoming `a?.b = v` / `delete a?.b` / `new a?.b()`.
ts.expectPrintedMin_("[a?.b][0] = v", "[a?.b][0] = v");
ts.expectPrintedMin_("delete [a?.b][0]", "delete [a?.b][0]");
ts.expectPrintedMin_("x = new [a?.b][0]()", "x = new [a?.b][0]");
ts.expectPrintedMin_("[a?.b][0]`x`", "[a?.b][0]`x`");

// Same predicate on the sibling `{f: x}.f -> x` fold: `new a?.b` /
// `a?.b`x`` are syntax errors, so bail there too.
Comment thread
robobun marked this conversation as resolved.
ts.expectPrintedMin_("x = new ({f: a?.b}).f()", "x = new { f: a?.b }.f");
ts.expectPrintedMin_("x = new ({f: a?.[b]}).f()", "x = new { f: a?.[b] }.f");
ts.expectPrintedMin_("x = new ({f: a?.b.c}).f()", "x = new { f: a?.b.c }.f");
ts.expectPrintedMin_("({f: a?.b}).f`x`", "({ f: a?.b }).f`x`");
ts.expectPrintedMin_("x = ({f: a?.b}).f", "x = { f: a?.b }.f");

// Non-chain items are still inlined.
ts.expectPrintedMin_("x = [[y]][0]?.[0].c", "x = y.c");
ts.expectPrintedMin_("x = [a.b][0].c", "x = a.b.c");
ts.expectPrintedMin_("x = [(a?.b)][0]", "x = [a?.b][0]");
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", () => {
// `[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])()");
ts.expectPrintedMin_("x = [obj.m][0]", "x = obj.m");
ts.expectPrintedMin_("x = [y][0]()", "x = y()");
ts.expectPrintedMin_("x = [() => y][0]()", "x = (() => y)()");

// `[x][0] = v` writes into the temporary, not `x`. Same for `"s"[n]`.
ts.expectPrintedMin_("[obj.p][0] = 5", "[obj.p][0] = 5");
ts.expectPrintedMin_("[obj.p][0] += 5", "[obj.p][0] += 5");
ts.expectPrintedMin_("[obj.p][0]++", "[obj.p][0]++");
ts.expectPrintedMin_("[x][0] = 1", "[x][0] = 1");
ts.expectPrintedMin_("[,][0] = 1", "[,][0] = 1");
ts.expectPrintedMin_('"foo"[2] = 1', '"foo"[2] = 1');
ts.expectPrintedMin_('["a", "b"][1] = 1', '["a", "b"][1] = 1');
ts.expectPrintedMin_("({ a: [obj.p][0] } = {})", "({ a: [obj.p][0] } = {})");
});
it("preserves runtime semantics when inlining from a literal index", async () => {
const src = `
var a = null;
function check(label, fn, expected) {
var got;
try { got = "=> " + fn(); } catch (e) { got = e.constructor.name; }
console.log(label + ": " + (got === expected ? "ok" : got + " (want " + expected + ")"));
}
check("chain .c", () => [[a?.b]][0]?.[0].c, "TypeError");
check("chain ()", () => [[a?.b]][0]?.[0](), "TypeError");
check("chain [0]", () => [[a?.b]][0]?.[0][0], "TypeError");
check("chain obj", () => ({ f: [a?.b] }).f?.[0].c, "TypeError");
check("chain flat", () => [a?.b][0].c, "TypeError");
check("chain ?.[", () => [a?.b]?.[0].c, "TypeError");
check("chain cont", () => [[a?.b.c]][0]?.[0].d, "TypeError");
check("chain pure", () => [[0, /* @__PURE__ */ a?.()]][0]?.[1].c, "TypeError");
var obj = { n: "obj", m() { return this === obj; } };
check("this", () => [obj.m][0](), "=> false");
var o2 = { p: 1 };
check("assign", () => ([o2.p][0] = 5, o2.p), "=> 1");
var ab = { b: class {} };
check("new obj", () => new ({ f: ab?.b }).f() instanceof ab.b, "=> true");
`;
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", src],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
Comment thread
robobun marked this conversation as resolved.
expect(stdout.trim().split("\n")).toEqual([
"chain .c: ok",
"chain (): ok",
"chain [0]: ok",
"chain obj: ok",
"chain flat: ok",
"chain ?.[: ok",
"chain cont: ok",
"chain pure: ok",
"this: ok",
"assign: ok",
"new obj: ok",
]);
expect(exitCode).toBe(0);
});
it("bails out on optional-chain index into enum", () => {
const pre = "enum Foo { A }\nenum Bar { 'a-b' = 1 }\n";
const lastLine = out => out.trimEnd().split("\n").at(-1);
Expand Down
Loading