fix(minifier): preserve optional chain base side effects (#22219)

Currently
```ts
x = (foo(), null)?.y
```

is folded to
```ts
x = void 0;
```

This is incorrect as `foo` may have side effect.

This PR fixes this and insteads folds it to:
```ts
x = (foo(), void 0);
```

This preserves the side effect of the call to `foo`.
This commit is contained in:
camc314 committed 2026-05-07 11:03:33 +00:00
1 parent e554cd8f90
commit 0e13d17c8b
3 files changed
+21 -8

No files matched your search

@@ -74,27 +74,33 @@ impl<'a> PeepholeOptimizations {
pub fn fold_chain_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ChainExpression(e) = expr else { return };
let left_expr = match &e.expression {
let span = e.span;
let left_expr = match &mut e.expression {
match_member_expression!(ChainElement) => {
let member_expr = e.expression.to_member_expression();
let member_expr = e.expression.to_member_expression_mut();
if !member_expr.optional() {
return;
}
member_expr.object()
member_expr.object_mut()
}
ChainElement::CallExpression(call_expr) => {
if !call_expr.optional {
return;
}
&call_expr.callee
&mut call_expr.callee
}
ChainElement::TSNonNullExpression(_) => return,
};
let ty = left_expr.value_type(ctx);
if let Some(changed) = (ty.is_null() || ty.is_undefined())
.then(|| ctx.value_to_expr(e.span, ConstantValue::Undefined))
{
*expr = changed;
if ty.is_null() || ty.is_undefined() {
*expr = if left_expr.may_have_side_effects(ctx) {
ctx.ast.expression_sequence(
span,
ctx.ast.vec_from_array([left_expr.take_in(ctx.ast), ctx.ast.void_0(span)]),
)
} else {
ctx.value_to_expr(span, ConstantValue::Undefined)
};
ctx.state.changed = true;
}
}
@@ -684,6 +684,8 @@ fn test_fold_opt_chain() {
fold("x = null?.[foo]", "x = void 0");
fold("x = undefined?.()", "x = void 0");
fold("x = null?.()", "x = void 0");
fold("x = (foo(), null)?.y", "x = (foo(), void 0)");
fold("x = (foo(), null)?.()", "x = (foo(), void 0)");
}
#[test]
@@ -24,6 +24,11 @@ fn test_remove_unused_expression() {
test("void x", "x");
}
#[test]
fn test_remove_unused_optional_chain_keeps_base_side_effects() {
test("let log = []; (log.push('base'), null)?.x;", "[].push('base')");
}
#[test]
fn test_remove_unused_this() {
// In a derived class constructor, `this` before `super()` throws a ReferenceError,