fix(minifier): do not fold a -0 ternary branch to +a (loses the sign) (#23637)

### Summary

The minifier folded `a ? 1 : -0` to `+a` (and `a ? -0 : 1` to `+!a`),
turning a `-0` branch into `+0` and changing the produced value.

### Problem

The fold matched the `-0` operand against the `0.0` literal pattern
(`-0.0 == 0.0` in Rust), so `a ? 1 : -0` became `+a`. When the test
selects the `-0` branch, `+a` / `+!a` yields `+0`. `-0` and `+0` are
equal under `===`, but they are distinct values: `Object.is(-0, +0) ===
false`, `1 / -0 === -Infinity` vs `1 / +0 === +Infinity`, `Math.atan2(0,
-0) === Math.PI` vs `0`. Code relying on SameValue (e.g. React's
`Object.is`-based `useState`/`useMemo` bailout) or on the sign of zero
behaves differently.

### Fix

Bind the zero operand with `@` and guard it with `!is_sign_negative()`,
so only a `+0` branch is folded. Pure `+0` cases (`a ? 1 : 0` → `+!!a`,
`a ? 0 : 1` → `+!a`) are unchanged.

### Testing

- Added cases to `test_minimize_conditional_numeric`: `a ? 1 : -0` and
`a ? -0 : 1` are left unfolded; `!y ? 1 : -0` may still be negated and
branch-swapped to `y ? -0 : 1`, but `-0` is preserved.
- `cargo test -p oxc_minifier` (536 passed) and `cargo lint -- -D
warnings` pass.
This commit is contained in:
Jerry Zhao authored and GitHub committed 2026-07-22 08:12:41 +08:00
1 parent 133bb35dee
commit 4b1284071a
2 files changed
+13 -4

No files matched your search

@@ -516,7 +516,9 @@ impl<'a> PeepholeOptimizations {
.and_then(ConstantValue::into_number)
.filter(|_| !expr.alternate.may_have_side_effects(ctx)),
) {
(Some(1.0), Some(0.0)) => {
// The `0` must be `+0`: `a ? 1 : -0` would become `+a`, which yields `+0` (not `-0`)
// when the test is falsy. `-0.0` matches the `0.0` pattern (`-0.0 == 0.0`), so guard it.
(Some(1.0), Some(alternate @ 0.0)) if !alternate.is_sign_negative() => {
// "a ? 1 : 0"
let is_boolean = expr.test.value_type(ctx).is_boolean();
let needs_parens = Self::test_needs_parens(&expr.test);
@@ -538,10 +540,11 @@ impl<'a> PeepholeOptimizations {
test, ctx));
}
}
(Some(0.0), Some(1.0))
(Some(consequent @ 0.0), Some(1.0))
// "a ? 0 : 1" => "+!a"
// Skip if parens would be needed (e.g., "a+b?0:1" => "+!(a+b)" is same length)
if !Self::test_needs_parens(&expr.test) => {
// Skip if parens would be needed (e.g., "a+b?0:1" => "+!(a+b)" is same length).
// The `0` must be `+0`: `a ? -0 : 1` would become `+!a`, yielding `+0`, not `-0`.
if !consequent.is_sign_negative() && !Self::test_needs_parens(&expr.test) => {
let test = expr.test.take_in(ctx);
let test = Self::minimize_not(expr.span, test, ctx);
return Some(Expression::new_unary_expression(expr.span,
@@ -104,6 +104,12 @@ fn test_minimize_conditional_numeric() {
// "a ? 0 : 1" stays when parens would make it same or longer
test("let x = a + b ? 0 : 1", "let x = a + b ? 0 : 1");
// `-0` must not be folded to `+a`/`+!a`: that would turn the `-0` branch into `+0`.
test_same("let x = a ? 1 : -0");
test_same("let x = a ? -0 : 1");
// The test may still be negated + branches swapped, but `-0` is preserved (not `+0`).
test("let x = !y ? 1 : -0", "let x = y ? -0 : 1");
}
#[test]