diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index cf66d2a8eb2c1..14d8b274eaafd 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -140,6 +140,9 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { Self::swap_binary_expressions(e); self.try_compress_type_of_equal_string(e); } + Expression::AssignmentExpression(e) => { + self.try_compress_normal_assignment_to_combined_assignment(e, ctx); + } _ => {} } @@ -147,7 +150,9 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { if let Some(folded_expr) = match expr { Expression::Identifier(ident) => self.try_compress_undefined(ident, ctx), Expression::BooleanLiteral(_) => self.try_compress_boolean(expr, ctx), - Expression::AssignmentExpression(e) => Self::try_compress_assignment_expression(e, ctx), + Expression::AssignmentExpression(e) => { + Self::try_compress_assignment_to_update_expression(e, ctx) + } Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx), Expression::NewExpression(e) => Self::try_fold_new_expression(e, ctx), Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx), @@ -525,7 +530,32 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax { } } - fn try_compress_assignment_expression( + /// Compress `a = a + b` to `a += b` + fn try_compress_normal_assignment_to_combined_assignment( + &mut self, + expr: &mut AssignmentExpression<'a>, + ctx: Ctx<'a, 'b>, + ) { + if !matches!(expr.operator, AssignmentOperator::Assign) { + return; + } + let AssignmentTarget::AssignmentTargetIdentifier(write_id_ref) = &mut expr.left else { + return; + }; + + let Expression::BinaryExpression(binary_expr) = &mut expr.right else { return }; + let Some(new_op) = binary_expr.operator.to_assignment_operator() else { return }; + let Expression::Identifier(read_id_ref) = &mut binary_expr.left else { return }; + if write_id_ref.name != read_id_ref.name { + return; + } + + expr.operator = new_op; + expr.right = ctx.ast.move_expression(&mut binary_expr.right); + self.changed = true; + } + + fn try_compress_assignment_to_update_expression( expr: &mut AssignmentExpression<'a>, ctx: Ctx<'a, 'b>, ) -> Option> { @@ -1020,6 +1050,58 @@ mod test { test("x !== false", "x !== !1"); } + /// Based on https://github.com/terser/terser/blob/58ba5c163fa1684f2a63c7bc19b7ebcf85b74f73/test/compress/assignment.js + #[test] + fn test_fold_normal_assignment_to_combined_assignment() { + test("x = x + 3", "x += 3"); + test("x = x - 3", "x -= 3"); + test("x = x / 3", "x /= 3"); + test("x = x * 3", "x *= 3"); + test("x = x >> 3", "x >>= 3"); + test("x = x << 3", "x <<= 3"); + test("x = x >>> 3", "x >>>= 3"); + test("x = x | 3", "x |= 3"); + test("x = x ^ 3", "x ^= 3"); + test("x = x % 3", "x %= 3"); + test("x = x & 3", "x &= 3"); + test("x = x + g()", "x += g()"); + test("x = x - g()", "x -= g()"); + test("x = x / g()", "x /= g()"); + test("x = x * g()", "x *= g()"); + test("x = x >> g()", "x >>= g()"); + test("x = x << g()", "x <<= g()"); + test("x = x >>> g()", "x >>>= g()"); + test("x = x | g()", "x |= g()"); + test("x = x ^ g()", "x ^= g()"); + test("x = x % g()", "x %= g()"); + test("x = x & g()", "x &= g()"); + + test_same("x = 3 + x"); + test_same("x = 3 - x"); + test_same("x = 3 / x"); + test_same("x = 3 * x"); + test_same("x = 3 >> x"); + test_same("x = 3 << x"); + test_same("x = 3 >>> x"); + test_same("x = 3 | x"); + test_same("x = 3 ^ x"); + test_same("x = 3 % x"); + test_same("x = 3 & x"); + test_same("x = g() + x"); + test_same("x = g() - x"); + test_same("x = g() / x"); + test_same("x = g() * x"); + test_same("x = g() >> x"); + test_same("x = g() << x"); + test_same("x = g() >>> x"); + test_same("x = g() | x"); + test_same("x = g() ^ x"); + test_same("x = g() % x"); + test_same("x = g() & x"); + + test_same("x = (x -= 2) ^ x"); + } + #[test] fn test_fold_subtraction_assignment() { test("x -= 1", "--x"); diff --git a/crates/oxc_syntax/src/operator.rs b/crates/oxc_syntax/src/operator.rs index 0d295d9204bf0..3033aba99694f 100644 --- a/crates/oxc_syntax/src/operator.rs +++ b/crates/oxc_syntax/src/operator.rs @@ -313,6 +313,25 @@ impl BinaryOperator { } } + /// Get [`AssignmentOperator`] corresponding to this [`BinaryOperator`]. + pub fn to_assignment_operator(self) -> Option { + match self { + Self::Addition => Some(AssignmentOperator::Addition), + Self::Subtraction => Some(AssignmentOperator::Subtraction), + Self::Multiplication => Some(AssignmentOperator::Multiplication), + Self::Division => Some(AssignmentOperator::Division), + Self::Remainder => Some(AssignmentOperator::Remainder), + Self::Exponential => Some(AssignmentOperator::Exponential), + Self::ShiftLeft => Some(AssignmentOperator::ShiftLeft), + Self::ShiftRight => Some(AssignmentOperator::ShiftRight), + Self::ShiftRightZeroFill => Some(AssignmentOperator::ShiftRightZeroFill), + Self::BitwiseOR => Some(AssignmentOperator::BitwiseOR), + Self::BitwiseXOR => Some(AssignmentOperator::BitwiseXOR), + Self::BitwiseAnd => Some(AssignmentOperator::BitwiseAnd), + _ => None, + } + } + /// The string representation of this operator as it appears in source code. pub fn as_str(&self) -> &'static str { match self { diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 8194418b259af..675ea316d43a0 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -3,9 +3,9 @@ Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- 72.14 kB | 23.68 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 59.79 kB | 59.82 kB | 19.42 kB | 19.33 kB | moment.js +173.90 kB | 59.78 kB | 59.82 kB | 19.42 kB | 19.33 kB | moment.js -287.63 kB | 90.06 kB | 90.07 kB | 32.07 kB | 31.95 kB | jquery.js +287.63 kB | 90.05 kB | 90.07 kB | 32.07 kB | 31.95 kB | jquery.js 342.15 kB | 118.16 kB | 118.14 kB | 44.52 kB | 44.37 kB | vue.js @@ -19,9 +19,9 @@ Original | minified | minified | gzip | gzip | Fixture 2.14 MB | 726.02 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.84 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.82 kB | 331.56 kB | echarts.js 6.69 MB | 2.32 MB | 2.31 MB | 492.75 kB | 488.28 kB | antd.js -10.95 MB | 3.50 MB | 3.49 MB | 909.11 kB | 915.50 kB | typescript.js +10.95 MB | 3.50 MB | 3.49 MB | 909.10 kB | 915.50 kB | typescript.js