Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 84 additions & 36 deletions crates/oxc_minifier/src/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Loading