Skip to content
Open
73 changes: 72 additions & 1 deletion crates/swc_ecma_minifier/src/compress/optimize/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ impl Optimizer<'_> {
}

if e.op == op!("===") || e.op == op!("!==") {
if (e.left.is_ident() || e.left.is_member()) && e.left.eq_ignore_span(&e.right) {
if (e.left.is_ident() || e.left.is_member())
&& e.left.eq_ignore_span(&e.right)
&& !contains_update_or_assign(&e.left)
{
self.changed = true;
report_change!("Reducing comparison of same variable ({})", e.op);

Expand Down Expand Up @@ -288,3 +291,71 @@ impl Optimizer<'_> {
}
}
}

/// Check if an expression contains update expressions (++, --) or assignments
/// that would make duplicate evaluations produce different results.
fn contains_update_or_assign(expr: &Expr) -> bool {
match expr {
Expr::Update(..) | Expr::Assign(..) => true,

Expr::Bin(BinExpr { left, right, .. }) => {
contains_update_or_assign(left) || contains_update_or_assign(right)
}

Expr::Unary(UnaryExpr { arg, .. }) => contains_update_or_assign(arg),

Expr::Cond(CondExpr {
test, cons, alt, ..
}) => {
contains_update_or_assign(test)
|| contains_update_or_assign(cons)
|| contains_update_or_assign(alt)
}

Expr::Member(MemberExpr { obj, prop, .. }) => {
contains_update_or_assign(obj)
|| match prop {
MemberProp::Computed(ComputedPropName { expr, .. }) => {
contains_update_or_assign(expr)
}
_ => false,
}
}

Expr::Call(CallExpr {
callee: Callee::Expr(callee),
args,
..
}) => {
contains_update_or_assign(callee)
|| args.iter().any(|arg| contains_update_or_assign(&arg.expr))
}
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visitor will incorrectly traverse into nested functions, arrow expressions, and classes, flagging update/assignment expressions that don't affect the outer expression's evaluation. For example, obj.method === obj.method where method is function() { ++i; } would be incorrectly flagged. Add methods to stop traversal at scope boundaries: fn visit_function(&mut self, _: &Function) {}, fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}, and fn visit_class(&mut self, _: &Class) {}.

Suggested change
}
}
fn visit_function(&mut self, _: &Function) {}
fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}
fn visit_class(&mut self, _: &Class) {}

Copilot uses AI. Check for mistakes.

Expr::Seq(SeqExpr { exprs, .. }) => {
exprs.iter().any(|expr| contains_update_or_assign(expr))
}

Expr::Paren(ParenExpr { expr, .. }) => contains_update_or_assign(expr),

Expr::OptChain(OptChainExpr { base, .. }) => match &**base {
OptChainBase::Member(member) => {
contains_update_or_assign(&member.obj)
|| match &member.prop {
MemberProp::Computed(ComputedPropName { expr, .. }) => {
contains_update_or_assign(expr)
}
_ => false,
}
}
OptChainBase::Call(call) => {
contains_update_or_assign(&call.callee)
|| call
.args
.iter()
.any(|arg| contains_update_or_assign(&arg.expr))
}
},

_ => false,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"comparisons": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Issue #11255: compress.comparisons should not optimize comparisons with side effects
let PC = 0;
const Stack = [0, ''];
const Code = [0, 0, 1];
console.log(Stack[Code[++PC]] === Stack[Code[++PC]]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Issue #11255: compress.comparisons should not optimize comparisons with side effects
let PC = 0;
const Stack = [0, ''];
const Code = [0, 0, 1];
console.log(Stack[Code[++PC]] === Stack[Code[++PC]]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Issue #11255: compress.comparisons should not optimize comparisons with side effects
let o = 0;
const c = [
0,
''
];
const l = [
0,
0,
1
];
console.log(c[l[++o]] === c[l[++o]]);
Loading