fix(minifier): only merge string literals in try_fold_add when the inner operator is + (#23622)

### Summary

In `try_fold_add`, the `a + 'b' + 'c'` → `a + 'bc'` string-literal merge
fired regardless of the *inner* binary operator, silently changing
semantics when that operator is not `+`.

### Problem

`(x - 'b') + 'c'` was rewritten to `x + 'bc'`. These are not equivalent:
the inner `x - 'b'` coerces the string numerically (`x - NaN`), so for
`x = 5` the original evaluates to `"NaNc"` while the rewrite gives
`"5bc"`. The same holds for `*`, `/`, `%`, `**`, `&`, etc.

### Root cause

The merge only checked that the inner expression's right operand is a
string (`left_binary_expr.right.value_type(ctx).is_string()`), never
that `left_binary_expr.operator` is `+`. It then rebuilt
`left_binary_expr.left <outer +> "merged"`, dropping the inner operator.
The associativity `(a + 'b') + 'c' === a + ('b' + 'c')` only holds when
the inner operator is `+`.

### Fix

Guard the merge with `left_binary_expr.operator ==
BinaryOperator::Addition`. The valid `(x + 'b') + 'c'` → `x + 'bc'` case
is unchanged.

### Testing

- Added regression cases to `test_string_add` in
`crates/oxc_minifier/tests/peephole/fold_constants.rs`: `x - 'b' + 'c'`,
`x * 'b' + 'c'`, `x % 'b' + 'c'`, `(x & 'b') + 'c'` no longer fold.
- `cargo test -p oxc_minifier` passes (536 tests, 0 failures).
This commit is contained in:
Jerry Zhao authored and GitHub committed 2026-07-02 09:57:40 +08:00
1 parent a55e0be25e
commit 59abb307e3
2 files changed
+11

No files matched your search

@@ -407,7 +407,11 @@ impl<'a> PeepholeOptimizations {
}
// a + 'b' + 'c' -> a + 'bc'
// Only sound when the inner operator is also `+`: for e.g. `(x - 'b') + 'c'` the inner
// string operand is numerically coerced (`x - 'b'` is `x - NaN`), so the literals must
// not be pulled out and merged.
if let Expression::BinaryExpression(left_binary_expr) = &mut e.left
&& left_binary_expr.operator == BinaryOperator::Addition
&& left_binary_expr.right.value_type(ctx).is_string()
{
if let (Some(left_str), Some(right_str)) = (
@@ -938,6 +938,13 @@ fn test_string_add() {
fold("x = foo() + 'a' + 'b' + 'cd' + bar()", "x = foo()+'abcd'+bar()");
fold("x = foo() + 2 + 'b'", "x = foo()+2+\"b\""); // don't fold!
// Don't merge string literals across a non-`+` inner operator: the inner string operand
// is coerced numerically, so `(x - 'b') + 'c'` is `(x - NaN) + 'c'`, not `x + 'bc'`.
fold_same("x = x - 'b' + 'c'");
fold_same("x = x * 'b' + 'c'");
fold_same("x = x % 'b' + 'c'");
fold_same("x = (x & 'b') + 'c'");
fold("x = foo() + 'a' + 2", "x = foo()+\"a2\"");
fold("x = '' + null", "x = 'null'");
fold("x = true + '' + false", "x = 'truefalse'");