diff --git a/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs b/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs index e34e2af48f476..33af05face093 100644 --- a/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs +++ b/crates/oxc_minifier/src/peephole/convert_to_dotted_properties.rs @@ -226,7 +226,7 @@ mod test { #[test] fn test_convert_to_dotted_properties_computed_property_with_default_value() { - test("const {['o']: o = 0} = {};", "const {o:o = 0} = {};"); + test("const {['o']: o = 0} = {};", "const o = 0;"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/minimize_statements.rs b/crates/oxc_minifier/src/peephole/minimize_statements.rs index b19934f862001..ed60f693a1995 100644 --- a/crates/oxc_minifier/src/peephole/minimize_statements.rs +++ b/crates/oxc_minifier/src/peephole/minimize_statements.rs @@ -1,17 +1,22 @@ -use std::{iter, ops::ControlFlow}; - +use crate::{ctx::Ctx, keep_var::KeepVar}; use oxc_allocator::{Box, TakeIn, Vec}; use oxc_ast::ast::*; use oxc_ast_visit::Visit; use oxc_ecmascript::{ + PropName, constant_evaluation::{DetermineValueType, IsLiteralValue, ValueType}, side_effects::MayHaveSideEffects, }; use oxc_semantic::ScopeFlags; -use oxc_span::{ContentEq, GetSpan}; +use oxc_span::{ContentEq, GetSpan, SPAN}; +use oxc_syntax::reference::ReferenceFlags; +use oxc_syntax::symbol::SymbolFlags; use oxc_traverse::Ancestor; - -use crate::{ctx::Ctx, keep_var::KeepVar}; +use std::{ + cmp::{Ordering, min}, + iter, + ops::ControlFlow, +}; use super::PeepholeOptimizations; @@ -399,6 +404,7 @@ impl<'a> PeepholeOptimizations { ctx.state.changed = true; } } + if Self::substitute_single_use_symbol_within_declaration( var_decl.kind, &mut var_decl.declarations, @@ -407,6 +413,10 @@ impl<'a> PeepholeOptimizations { ctx.state.changed = true; } + if Self::simplify_destructuring_assignment(var_decl.kind, &mut var_decl.declarations, ctx) { + ctx.state.changed = true; + } + // If `join_vars` is off, but there are unused declarators ... just join them to make our code simpler. if !ctx.options().join_vars && var_decl.declarations.iter().all(|d| !Self::should_remove_unused_declarator(d, ctx)) @@ -443,6 +453,352 @@ impl<'a> PeepholeOptimizations { } } + /// Determines whether an object destruction assignment can be simplified based on the provided variable declaration. + /// - `let {x, y} = {x: 1, y: 2};` -> true + /// - `let {x, y} = {...arr};` -> false + fn can_simplify_object_to_object_destruction_assignment( + decl: &VariableDeclarator<'a>, + ctx: &Ctx<'a, '_>, + ) -> bool { + let BindingPatternKind::ObjectPattern(id_kind) = &decl.id.kind else { return false }; + // do not process if left side of assignment is empty or has rest `{ ...a }` + if id_kind.properties.is_empty() + || id_kind.rest.is_some() + || id_kind.properties.iter().any(|e| e.key.prop_name().is_none()) + { + return false; + } + + let Some(Expression::ObjectExpression(init_expr)) = &decl.init else { return false }; + if init_expr + .properties + .iter() + .any(|e| e.is_spread() || e.may_have_side_effects(ctx) || e.prop_name().is_none()) + { + return false; + } + + true + } + + fn simplify_object_destruction_assignment( + decl: &mut VariableDeclarator<'a>, + result: &mut Vec<'a, VariableDeclarator<'a>>, + ctx: &mut Ctx<'a, '_>, + ) -> bool { + let BindingPatternKind::ObjectPattern(id_kind) = &mut decl.id.kind else { + return false; + }; + let Some(Expression::ObjectExpression(init_expr)) = &mut decl.init else { + return false; + }; + + id_kind.properties.sort_by(|a, b| { + let a_short = a.shorthand || b.value.kind.is_binding_identifier(); + let b_short = b.shorthand || b.value.kind.is_binding_identifier(); + if a_short == b_short { + return Ordering::Equal; + } + if a_short { + return Ordering::Less; + } + Ordering::Greater + }); + + for id_prop in id_kind.properties.drain(..) { + let id_prop_key = match id_prop.key { + PropertyKey::StaticIdentifier(ident) => (ident.name, ident.span), + PropertyKey::Identifier(ident) => (ident.name, ident.span), + PropertyKey::StringLiteral(lit) => (lit.value, lit.span), + // TODO: is this the best way? + // PropertyKey::NumericLiteral(lit) => { + // (Atom::from(ctx.ast.str(&lit.to_string())), lit.span) + // } + _ => return false, + }; + + let prop_index = init_expr.properties.iter().rposition(|init| { + init.prop_name().is_some_and(|name| name.0.eq(id_prop_key.0.as_str())) + }); + + let init_value = if let Some(index) = prop_index { + if let Some(ObjectPropertyKind::ObjectProperty(prop)) = + init_expr.properties.get_mut(index) + { + if id_prop.value.kind.is_binding_identifier() + || id_prop.value.kind.is_assignment_pattern() + { + let val = prop.value.take_in(ctx.ast); + prop.value = ctx.create_ident_expr( + id_prop_key.1, + id_prop_key.0, + id_prop + .value + .get_binding_identifier() + .map(BindingIdentifier::symbol_id), + ReferenceFlags::Read, + ); + Some(val) + } else { + let unique = ctx.generate_uid_in_current_scope( + &id_prop_key.0, + SymbolFlags::ConstVariable, + ); + let ident = unique.create_binding_pattern(ctx); + let symbol_id = + ident.get_binding_identifier().map(BindingIdentifier::symbol_id); + + result.push(ctx.ast.variable_declarator( + SPAN, + decl.kind, + ident, + Option::from(prop.value.take_in(ctx.ast)), + decl.definite, + )); + + prop.value = ctx.create_ident_expr( + SPAN, + unique.name, + symbol_id, + ReferenceFlags::Read, + ); + + // replace reference for `{ x, x: c } = ....` + Some(ctx.create_ident_expr( + id_prop.value.span(), + unique.name, + symbol_id, + ReferenceFlags::Read, + )) + } + } else { + None + } + } else if id_prop.value.kind.is_assignment_pattern() + || id_prop.value.kind.is_binding_identifier() + { + None + } else { + Some(ctx.ast.void_0(SPAN)) + }; + + result.push(ctx.ast.variable_declarator( + id_prop.span, + decl.kind, + id_prop.value, + init_value, + decl.definite, + )); + } + true + } + + /// Determines whether an array destruction assignment can be simplified based on the provided variable declaration. + /// - `let [x, y] = [1, 2];` -> true + /// - `let [x, y] = [...arr];` -> false + fn can_simplify_array_to_array_destruction_assignment( + decl: &VariableDeclarator<'a>, + _ctx: &Ctx<'a, '_>, + ) -> bool { + let BindingPatternKind::ArrayPattern(id_kind) = &decl.id.kind else { return false }; + // if left side of assignment is empty do not process it + if id_kind.elements.is_empty() { + return false; + } + + let Some(Expression::ArrayExpression(init_expr)) = &decl.init else { return false }; + + // check if the first init is not spread + if init_expr.elements.first().is_some_and(ArrayExpressionElement::is_spread) { + return false; + } + + // check for `[a = b] = [c]`, this could be potentially optimized if we check if `c` is undefined + if init_expr.elements.len() == 1 + && id_kind + .elements + .first() + .is_some_and(|e| e.as_ref().is_none_or(|p| p.kind.is_assignment_pattern())) + { + return false; + } + + // check for side effects + // if init_expr.elements.iter().any(|e| e.may_have_side_effects(ctx)) { + // return false; + // } + + true + } + + fn simplify_array_destruction_assignment( + decl: &mut VariableDeclarator<'a>, + result: &mut Vec<'a, VariableDeclarator<'a>>, + ctx: &Ctx<'a, '_>, + ) -> bool { + let BindingPatternKind::ArrayPattern(id_kind) = &mut decl.id.kind else { + return false; + }; + let Some(Expression::ArrayExpression(init_expr)) = &mut decl.init else { + return false; + }; + + let index = if let Some(spread_index) = + init_expr.elements.iter().position(ArrayExpressionElement::is_spread) + { + min(id_kind.elements.len(), spread_index) + } else { + id_kind.elements.len() + }; + + let mut init_iter = init_expr.elements.drain(..); + + for id_item in id_kind.elements.drain(0..index) { + let init_item = init_iter.next(); + + // check for holes [,] = ???? + if let Some(id) = id_item { + if id.kind.is_assignment_pattern() { + if let Some(init) = init_item { + // this is not ideal as we are producing [a = 1] = [1] here + result.push(ctx.ast.variable_declarator( + id.span(), + decl.kind, + ctx.ast.binding_pattern( + ctx.ast.binding_pattern_kind_array_pattern( + decl.span, + ctx.ast.vec1(Some(id)), + None::>>, + ), + None::>>, + false, + ), + Some(ctx.ast.expression_array(init.span(), ctx.ast.vec1(init))), + decl.definite, + )); + } else if let BindingPatternKind::AssignmentPattern(id_kind) = id.kind { + let id_kind_unbox = id_kind.unbox(); + result.push(ctx.ast.variable_declarator( + id_kind_unbox.span, + decl.kind, + id_kind_unbox.left, + Some(id_kind_unbox.right), + decl.definite, + )); + } + } else { + result.push(ctx.ast.variable_declarator( + id.span(), + decl.kind, + id, + if let Some(init) = init_item + && !init.is_elision() + { + Some(init.into_expression()) + } else { + None + }, + decl.definite, + )); + } + } else if let Some(init_elem) = init_item { + // skip [,] = [,] + if !init_elem.is_elision() { + result.push(ctx.ast.variable_declarator( + init_elem.span(), + decl.kind, + ctx.ast.binding_pattern( + ctx.ast.binding_pattern_kind_array_pattern( + decl.span, + ctx.ast.vec(), + None::>>, + ), + None::>>, + false, + ), + Some(Expression::ArrayExpression(ctx.ast.alloc_array_expression( + init_elem.span(), + ctx.ast.vec_from_iter(Some(init_elem)), + ))), + decl.definite, + )); + } + } + } + + if init_iter.len() == 0 { + if !id_kind.elements.is_empty() { + for id in id_kind.elements.drain(..).flatten() { + result.push(ctx.ast.variable_declarator( + id.span(), + decl.kind, + id, + None, + decl.definite, + )); + } + } + id_kind.rest.is_none() + } else { + init_expr.elements = ctx.ast.vec_from_iter(init_iter); + false + } + } + + /// Simplifies destructuring assignments by transforming array patterns into a sequence of + /// variable declarations, whenever possible. This function modifies the input declarations + /// and returns whether any changes were made. + fn simplify_destructuring_assignment( + _kind: VariableDeclarationKind, + declarations: &mut Vec<'a, VariableDeclarator<'a>>, + ctx: &mut Ctx<'a, '_>, + ) -> bool { + let mut changed = false; + let mut i = declarations.len(); + while i > 0 { + i -= 1; + + let Some(last) = declarations.get(i) else { + continue; + }; + let can_simplify_array = + Self::can_simplify_array_to_array_destruction_assignment(last, ctx); + let can_simplify_object = !can_simplify_array + && Self::can_simplify_object_to_object_destruction_assignment(last, ctx); + + if can_simplify_array || can_simplify_object { + let mut new_var_decl: Vec<'a, VariableDeclarator<'a>> = ctx.ast.vec(); + let to_remove = if can_simplify_array { + Self::simplify_array_destruction_assignment( + declarations.get_mut(i).unwrap(), + &mut new_var_decl, + ctx, + ) + } else { + Self::simplify_object_destruction_assignment( + declarations.get_mut(i).unwrap(), + &mut new_var_decl, + ctx, + ) + }; + + if !new_var_decl.is_empty() { + if to_remove { + declarations.splice(i..=i, new_var_decl.into_iter()); + } else { + declarations.splice(i..i, new_var_decl.into_iter()); + } + changed = true; + } else if to_remove { + declarations.remove(i); + changed = true; + } + } + } + + changed + } + fn handle_expression_statement( mut expr_stmt: Box<'a, ExpressionStatement<'a>>, result: &mut Vec<'a, Statement<'a>>, @@ -1850,7 +2206,7 @@ impl<'a> PeepholeOptimizations { #[cfg(test)] mod test { - use crate::tester::test; + use crate::tester::{test, test_same}; #[test] fn test_for_variable_declaration() { @@ -1884,4 +2240,68 @@ mod test { test("for( a in b ){ c(); continue; }", "for ( a in b ) c();"); test("for( ; ; ){ c(); continue; }", "for ( ; ; ) c();"); } + + #[test] + fn test_simplify_object_destruction_assignment() { + test_same("var {} = {}"); + + test("var {a} = {a: 1}", "var a = 1"); + + test("var a = {a: 1}, b = a;log(a,b)", "var a = {a: 1}, b = a;log(a,b)"); + test("var {a, a: c} = {a: {f: 1}};log(a,f)", "var a = {f: 1}, c = a;log(a,f)"); + test("var {a: {f}, a} = {a: {f: 1}};log(a,f)", "var a = {f: 1}, {f} = a;log(a,f)"); + + test("var a = {a: 1}, b = a", "var b = {a: 1}"); + test("var {a, a: c} = {a: {f: 1}};", "var c = {f: 1}"); + test("var {a: {f}, a} = {a: {f: 1}}", "var f = 1"); + + test("var {a, b} = {a: 1}", "var a = 1, b"); + test("var {a, a: {b}} = {a: {b: 1}}", "var b = 1"); + // ideally `var c = 1, b = 2` + test("var {a:{c},a:{b}}={a:{b:2,c:1}}", "var _a={b:2,c:1},{c}=_a,{b}=_a"); + // Uncaught TypeError: (intermediate value).a is not iterable -> Uncaught TypeError: _a is not iterable + test("var {a:[b],a:{c}}={a:{[0]:b,c}}", "var _a={0:b,c},[b]=_a,{c}=_a"); + } + + #[test] + fn test_array_variable_destruction() { + test_same("var [] = []"); + test("var [a] = [1]", "var a=1"); + test("var [a, b, c, d] = [1, 2, 3, 4]", "var a=1,b=2,c=3,d=4"); + test("var [a, b, c, d] = [1, 2, 3]", "var a=1,b=2,c=3,d"); + test("var [a, b, c = 2, d] = [1]", "var a=1,b,c=2,d"); + test("var [a, b, c] = [1, 2, 3, 4]", "var a=1,b=2,c=3,[]=[4]"); + test("var [a, b, c = 2] = [1, 2, 3, 4]", "var a=1,b=2,[c=2]=[3],[]=[4]"); + test("var [a, b, c = 3] = [1, 2]", "var a=1,b=2,c=3"); + test("var [a, b] = [1, 2, 3]", "var a=1,b=2,[]=[3]"); + test("var [a] = [123, 2222, 2222]", "var a=123,[]=[2222,2222];"); + // spread + test("var [...a] = [...b]", "var [...a] = [...b]"); + test("var [a, a, ...d] = []", "var a, a, [...d] = []"); + test("var [a, ...d] = []", "var a, [...d] = []"); + test("var [a, ...d] = [1, ...f]", "var a=1,[...d]=[...f]"); + test("var [a, ...d] = [1, foo]", "var a=1,[...d]=[foo]"); + test("var [a, b, c, ...d] = [1, 2, ...foo]", "var a=1,b=2,[c,...d]=[...foo]"); + test("var [a, b, ...c] = [1, 2, 3, ...foo]", "var a=1,b=2,[...c]=[3,...foo]"); + test("var [a, b] = [...c, ...d]", "var [a, b] = [...c, ...d]"); + test("var [a, b] = [...c, c, d]", "var [a,b] = [...c,c,d]"); + // defaults + test("var [a = 1] = []", "var a = 1;"); + test_same("var [a = 1] = [void 0]"); + test_same("var [a = 1] = [foo]"); + test_same("var [a = foo] = [2]"); + // holes + test("var [,,,] = [,,,]", ""); + test("var [a, , c, d] = [, 3, , 4]", "var a, [] = [3], c, d = 4"); + test("var [a, , c, d] = [1, 2, 3, 4]", "var a=1,[]=[2],c=3,d=4"); + test("var [ , , a] = [1, 2, 3, 4]", "var []=[1],[]=[2],a=3,[]=[4]"); + test("var [ , , ...t] = [1, 2, 3, 4]", "var []=[1],[]=[2],[...t]=[3,4]"); + test("var [ , , ...t] = [1, ...a, 2, , 4]", "var []=[1],[,...t]=[...a,2,,4]"); + test("var [a, , b] = [,,,]", "var a, b;"); + // nested + test("var [a, [b, c]] = [1, [2, 3]]", "var a=1,b=2,c=3"); + test("var [a, [b, [c, d]]] = [1, ...[2, 3]]", "var a=1,[[b,[c,d]]]=[...[2,3]]"); + test("var [a, [b, [c,]]] = [1, [...2, 3]]", "var a=1,[b,[c]]=[...2,3]"); + test("var [a, [b, [c,]]] = [1, [2, [...3]]]", "var a=1,b=2,[c]=[...3];"); + } } diff --git a/crates/oxc_minifier/src/peephole/normalize.rs b/crates/oxc_minifier/src/peephole/normalize.rs index be63d9643f3e8..27aa3c55dfbfe 100644 --- a/crates/oxc_minifier/src/peephole/normalize.rs +++ b/crates/oxc_minifier/src/peephole/normalize.rs @@ -438,9 +438,9 @@ mod test { test_same("{ const x = 1; x = 2 }"); // keep assign error test_same("{ const x = 1; eval('x = 2') }"); // keep assign error test("{ const x = 1, y = 2 }", "{ let x = 1, y = 2 }"); - test("{ const { x } = { x: 1 } }", "{ let { x } = { x: 1 } }"); - test("{ const [x] = [1] }", "{ let [x] = [1] }"); - test("{ const [x = 1] = [] }", "{ let [x = 1] = [] }"); + test("{ const { x } = { x: 1 } }", "{ let x = 1 }"); + test("{ const [x] = [1] }", "{ let x = 1 }"); + test("{ const [x = 1] = [] }", "{ let x = 1 }"); test("for (const x in y);", "for (let x in y);"); // TypeError: Assignment to constant variable. test_same("for (const i = 0; i < 1; i++);"); diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index 15f3e31c2a725..24f8c576024d1 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -597,7 +597,7 @@ mod test { // Make sure it plays nice with minimizing test("for(;false;) { foo(); continue }", ""); - test("for (var { c, x: [d] } = {}; 0;);", "var { c, x: [d] } = {};"); + test("for (var { c, x: [d] } = {}; 0;);", "var c, [d] = void 0"); test("for (var se = [1, 2]; false;);", "var se = [1, 2];"); test("for (var se = [1, 2]; false;) { var a = 0; }", "var se = [1, 2], a;"); diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 7f1683b3a44e3..1b87e976d6def 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -2240,7 +2240,7 @@ mod test { // Binding Property test( "var { '0': _, 'a': _, [1]: _, ['1']: _, ['b']: _, ['c.c']: _, '1.1': _, '😊': _, 'd.d': _ } = {}", - "var { 0: _, a: _, 1: _, 1: _, b: _, 'c.c': _, '1.1': _, '😊': _, 'd.d': _ } = {}", + "var { 0: _, a: _, 1: _, 1: _, b: _, 'c.c': _, '1.1': _, '😊': _, 'd.d': _ } = {};", ); // Method Definition test( diff --git a/crates/oxc_minifier/tests/peephole/collapse_variable_declarations.rs b/crates/oxc_minifier/tests/peephole/collapse_variable_declarations.rs index 9c867a862a2fe..024a7fb7f1dbf 100644 --- a/crates/oxc_minifier/tests/peephole/collapse_variable_declarations.rs +++ b/crates/oxc_minifier/tests/peephole/collapse_variable_declarations.rs @@ -188,7 +188,7 @@ mod collapse_for { test( "var [a, b] = [1, 2]; for (; a < 2; a = b++) foo();", - "for (var [a, b] = [1, 2]; a < 2; a = b++) foo();", + "for (var a = 1, b = 2; a < 2; a = b++) foo();", ); }