diff --git a/crates/oxc_minifier/src/peephole/minimize_statements.rs b/crates/oxc_minifier/src/peephole/minimize_statements.rs index a580619ba51ef..976179b89578c 100644 --- a/crates/oxc_minifier/src/peephole/minimize_statements.rs +++ b/crates/oxc_minifier/src/peephole/minimize_statements.rs @@ -449,54 +449,102 @@ impl<'a> PeepholeOptimizations { // "var a; a = b();" => "var a = b();" match &mut expr_stmt.expression { Expression::AssignmentExpression(assign_expr) => { - if assign_expr.operator == AssignmentOperator::Assign - && let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign_expr.left + let merged = Self::merge_assignment_to_declaration(assign_expr, result, ctx); + if merged { + ctx.state.changed = true; + return; + } + } + Expression::SequenceExpression(sequence_expr) => { + if result + .last() + .is_some_and(|stmt| matches!(stmt, Statement::VariableDeclaration(_))) { - if let Some(Statement::VariableDeclaration(var_decl)) = result.last_mut() { - if matches!( - &var_decl.kind, - VariableDeclarationKind::Var | VariableDeclarationKind::Let - ) { - for decl in var_decl.declarations.iter_mut().rev() { - let BindingPatternKind::BindingIdentifier(kind) = &decl.id.kind - else { - break; - }; - if kind.name == id.name { - if decl.init.is_none() { - // "var a; a = b();" => "var a = b();" - decl.init = Some(assign_expr.right.take_in(ctx.ast)); - ctx.state.changed = true; - return; - } - // Note it is not possible to compress like: - // - "var a = b(); a = c();" => "var a = (b(), c());" - // This is not possible as we need to consider cases when `c()` accesses `a` - // - "var a = 1; a = b();" => "var a = b();" - // This is not possible as we need to consider cases when `b()` accesses `a` - break; - } - // should not move assignment above variables with initializer to keep the execution order - if decl.init.is_some() { - break; - } - // should not move assignment above other variables for let - // this could cause TDZ errors (e.g. `let a, b; b = a;`) - if decl.kind == VariableDeclarationKind::Let { - break; - } + let first_non_merged_index = + sequence_expr.expressions.iter_mut().position(|expr| { + if let Expression::AssignmentExpression(assign_expr) = expr { + !Self::merge_assignment_to_declaration(assign_expr, result, ctx) + } else { + true } + }); + let sequence_len = sequence_expr.expressions.len(); + match first_non_merged_index { + None => { + // all elements are merged + ctx.state.changed = true; + return; + } + Some(val) if val == sequence_len - 1 => { + // all elements are merged except for the last expression + let last_expr = sequence_expr.expressions.pop().unwrap(); + result.push(ctx.ast.statement_expression(last_expr.span(), last_expr)); + ctx.state.changed = true; + return; + } + Some(0) => { + // no elements are merged + } + Some(val) => { + sequence_expr.expressions.drain(0..val); + ctx.state.changed = true; } } } } - Expression::SequenceExpression(_exprs) => {} _ => {} } result.push(Statement::ExpressionStatement(expr_stmt)); } + fn merge_assignment_to_declaration( + assign_expr: &mut AssignmentExpression<'a>, + result: &mut Vec<'a, Statement<'a>>, + ctx: &Ctx<'a, '_>, + ) -> bool { + if assign_expr.operator != AssignmentOperator::Assign { + return false; + } + let AssignmentTarget::AssignmentTargetIdentifier(id) = &assign_expr.left else { + return false; + }; + let Some(Statement::VariableDeclaration(var_decl)) = result.last_mut() else { + return false; + }; + if !matches!(&var_decl.kind, VariableDeclarationKind::Var | VariableDeclarationKind::Let) { + return false; + } + for decl in var_decl.declarations.iter_mut().rev() { + let BindingPatternKind::BindingIdentifier(kind) = &decl.id.kind else { + break; + }; + if kind.name == id.name { + if decl.init.is_none() { + // "var a; a = b();" => "var a = b();" + decl.init = Some(assign_expr.right.take_in(ctx.ast)); + return true; + } + // Note it is not possible to compress like: + // - "var a = b(); a = c();" => "var a = (b(), c());" + // This is not possible as we need to consider cases when `c()` accesses `a` + // - "var a = 1; a = b();" => "var a = b();" + // This is not possible as we need to consider cases when `b()` accesses `a` + break; + } + // should not move assignment above variables with initializer to keep the execution order + if decl.init.is_some() { + break; + } + // should not move assignment above other variables for let + // this could cause TDZ errors (e.g. `let a, b; b = a;`) + if decl.kind == VariableDeclarationKind::Let { + break; + } + } + false + } + fn handle_switch_statement( mut switch_stmt: Box<'a, SwitchStatement<'a>>, result: &mut Vec<'a, Statement<'a>>, diff --git a/crates/oxc_minifier/tests/peephole/merge_assignments_to_declarations.rs b/crates/oxc_minifier/tests/peephole/merge_assignments_to_declarations.rs index 61db6863d0051..e93d1bdd79bc5 100644 --- a/crates/oxc_minifier/tests/peephole/merge_assignments_to_declarations.rs +++ b/crates/oxc_minifier/tests/peephole/merge_assignments_to_declarations.rs @@ -9,10 +9,17 @@ fn merge_assignments_to_declarations_var() { test_same("var a, b = 1; a = 0"); // this can be improved to `var a = 0, b = 1` test_same("var a, b = c(); a = 0"); // `c()` may access `a` test("var a, b; a = 0", "var a = 0, b"); + test("var a, b; a = 0, b = 1", "var a = 0, b = 1"); test("var a, b; a = 0; b = 1", "var a = 0, b = 1"); test("var a, b; a = c()", "var a = c(), b"); + test("var a, b; a = c(), b = d()", "var a = c(), b = d()"); test("var a, b; a = c(); b = d()", "var a = c(), b = d()"); test("var a, b; a = b", "var a = b, b"); + + test("var a, b, c; a = 0, b = 1, c = 2", "var a = 0, b = 1, c = 2"); + test("var a, b; a = 0, b = 1, foo()", "var a = 0, b = 1; foo()"); + test("var a; a = 0, foo(), bar()", "var a = 0; foo(), bar()"); + test_same("var a, b; foo(), bar()"); } #[test]