Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
bf25912
feat(minifier): simplify assigment with array destructuring
armano2 Dec 6, 2025
741d76c
fix: support [a=1]=[1] syntax
armano2 Dec 8, 2025
44c804a
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 9, 2025
4af362a
fix: simplify code
armano2 Dec 9, 2025
03761c5
fix: simplify code more
armano2 Dec 9, 2025
37f56af
fix: update unit tests
armano2 Dec 9, 2025
b044ab4
Merge branch 'fix/inline-statements' of github.com:armano2/oxc into f…
armano2 Dec 9, 2025
672ad04
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 9, 2025
8f048d5
fix: simplify code
armano2 Dec 9, 2025
02b2278
refactor: refactor code
armano2 Dec 9, 2025
b16d9da
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 10, 2025
cd98bd1
fix: skip elision in init
armano2 Dec 10, 2025
388a8ef
Merge branch 'main' into fix/inline-statements
armano2 Dec 10, 2025
9abf038
feat: implement minification of objects
armano2 Dec 10, 2025
01738dd
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 13, 2025
f44749a
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 14, 2025
e3432df
fix: improve inlining numeric literals
armano2 Dec 14, 2025
5ccabce
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 17, 2025
1cbea81
fix: do not change computed properties
armano2 Dec 17, 2025
4ee3fda
Merge branch 'main' into fix/inline-statements
armano2 Dec 22, 2025
b908dfa
fix: update code with latest main changes and remove object destruction
armano2 Dec 22, 2025
791b843
fix: simplify get
armano2 Dec 22, 2025
bd36ecf
fix: add more comments
armano2 Dec 22, 2025
129c07d
chore: revert test changes
armano2 Dec 22, 2025
0b0cee4
fix: simplify expressions with arguments
armano2 Dec 22, 2025
b5da918
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 22, 2025
e784a67
Merge branch 'main' into fix/inline-statements
armano2 Dec 22, 2025
6a567e4
fix: reduce cognitive complexity
armano2 Dec 22, 2025
7a06b50
fix: cleanup value_type
armano2 Dec 22, 2025
3db6589
feat: first pass of removing spread
armano2 Dec 22, 2025
b804018
fix: improve handling of empty rests
armano2 Dec 22, 2025
02dda4f
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 23, 2025
57a00d0
fix: disable inlining of var's
armano2 Dec 24, 2025
e80cba6
fix: disable inlining var's
armano2 Dec 24, 2025
8d44e2f
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 24, 2025
a1806c9
fix: introduce iterim variables
armano2 Dec 24, 2025
cd94ef9
refactor code for better inlining
armano2 Dec 24, 2025
2fef058
[autofix.ci] apply automated fixes
autofix-ci[bot] Dec 24, 2025
1d33cd9
fix: formatting
armano2 Dec 24, 2025
4e9ac4e
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Dec 24, 2025
0a72fed
do not create variables if there is only one init
armano2 Dec 24, 2025
788f6fa
fix: correct regresion
armano2 Dec 24, 2025
ddb7b94
Merge remote-tracking branch 'origin/fix/inline-statements' into fix/…
armano2 Dec 24, 2025
cdbb01f
fix: apply feedback from code review
armano2 Dec 25, 2025
e67284e
Merge branch 'main' into fix/inline-statements
armano2 Dec 25, 2025
4a25488
test: update unit tests
armano2 Dec 25, 2025
795da47
fix: correct issue with inlining spread for var
armano2 Dec 25, 2025
01e6ea3
improve comments and add missing test case
armano2 Dec 25, 2025
490806a
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Jan 2, 2026
21382e9
fix: check if binding identifier is used within init or it may be use…
armano2 Jan 2, 2026
cbbc094
fix: correct lint issues
armano2 Jan 2, 2026
9800c9b
fix: more lint issues
armano2 Jan 2, 2026
033ac06
Merge branch 'main' into fix/inline-statements
armano2 Jan 8, 2026
928a318
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Jan 8, 2026
8d8667d
Merge remote-tracking branch 'upstream/main' into fix/inline-statements
armano2 Jan 17, 2026
21d1b2d
Merge branch 'main' into fix/inline-statements
armano2 Jan 19, 2026
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
266 changes: 263 additions & 3 deletions crates/oxc_minifier/src/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use std::{iter, ops::ControlFlow};
use std::{cmp::min, iter, ops::ControlFlow};

use oxc_allocator::{Box, TakeIn, Vec};
use oxc_ast::ast::*;
use oxc_ast::{NONE, ast::*};
use oxc_ast_visit::Visit;
use oxc_ecmascript::{
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_traverse::Ancestor;

use crate::{ctx::Ctx, keep_var::KeepVar};
Expand Down Expand Up @@ -407,6 +407,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))
Expand Down Expand Up @@ -443,6 +447,262 @@ impl<'a> PeepholeOptimizations {
}
}

/// 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 BindingPattern::ArrayPattern(id_kind) = &decl.id else { return false };
// if left side of assignment is empty do not process it
if id_kind.is_empty() {
return false;
}

let Some(Expression::ArrayExpression(init_expr)) = &decl.init else { return false };

let init_len = init_expr.elements.len();
// [???] = [] or [...rest] = [??]
if init_len == 0 || (id_kind.rest.is_some() && id_kind.elements.is_empty()) {
return true;
}

let first_init = init_expr.elements.first();

// check if the first init is not spread when rest is present without elements
// [] = [...rest] | [a, ...rest] = [...rest]
if first_init.is_some_and(ArrayExpressionElement::is_spread)
&& id_kind.rest.is_none()
&& !id_kind.elements.is_empty()
{
return false;
}

// check for `[a = b] = [c]`
if init_len == 1
&& first_init.is_some_and(|expr| !expr.is_literal_value(false, ctx))
&& id_kind
.elements
.first()
.is_some_and(|e| e.as_ref().is_none_or(BindingPattern::is_assignment_pattern))
{
return false;
}

if decl.kind.is_var() && init_len > 1 {
let binding_identifiers = decl.id.get_binding_identifiers();
if !binding_identifiers.is_empty() {
return !init_expr.elements.iter().any(|e| {
match e.as_expression() {
Some(Expression::Identifier(ident)) => {
let Some(ref_symbol) =
&ctx.scoping().get_reference(ident.reference_id()).symbol_id()
else {
return false; // global reference
};
// check whatever id is present in init [a] = [b]
binding_identifiers.iter().any(|id| id.symbol_id().eq(ref_symbol))
}
_ => !e.is_literal_value(false, ctx),
}
});
}
}

true
}

fn simplify_array_destruction_assignment(
decl: &mut VariableDeclarator<'a>,
result: &mut Vec<'a, VariableDeclarator<'a>>,
ctx: &Ctx<'a, '_>,
) -> bool {
let BindingPattern::ArrayPattern(id_pattern) = &mut decl.id else {
return false;
};
let Some(Expression::ArrayExpression(init_expr)) = &mut decl.init else {
return false;
};

// limit iteration of id (left) to last spread of init (right)
// [a, b, c] = [b, ...spread]
let index = if let Some(spread_index) =
init_expr.elements.iter().position(ArrayExpressionElement::is_spread)
{
min(id_pattern.elements.len(), spread_index)
} else {
id_pattern.elements.len()
};

let mut init_iter = init_expr.elements.drain(..);

for id_item in id_pattern.elements.drain(0..index) {
let init_item = match init_iter.next() {
None | Some(ArrayExpressionElement::Elision(_)) => ctx.ast.void_0(SPAN),
Some(ArrayExpressionElement::SpreadElement(_)) => {
unreachable!("spread element does not exist until `index`")
}
Some(init_item) => init_item.into_expression(),
};

match id_item {
// `[a = b] = [??]`
Some(BindingPattern::AssignmentPattern(mut pattern)) => {
if init_item.is_literal_value(false, ctx) {
// if value is determined, `[a = b] = [c]` => `a = c` or `a = b`
result.push(ctx.ast.variable_declarator(
pattern.span(),
decl.kind,
pattern.left.take_in(ctx.ast),
NONE,
Some(if init_item.is_void() || init_item.is_undefined() {
// `[a = b] = [undefined]` => `a = b`
pattern.right.take_in(ctx.ast)
} else {
// `[a = b] = [c]` => `a = c`
init_item
}),
decl.definite,
));
} else {
// `[a = b] = [c]` where c is undetermined => `[a = b] = [c]`
result.push(ctx.ast.variable_declarator(
pattern.span(),
decl.kind,
ctx.ast.binding_pattern_array_pattern(
decl.span,
ctx.ast.vec1(Some(BindingPattern::AssignmentPattern(pattern))),
NONE,
),
NONE,
Some(ctx.ast.expression_array(
init_item.span(),
ctx.ast.vec1(ArrayExpressionElement::from(init_item)),
)),
decl.definite,
));
}
}
// `[a, b] = [c, d]` => `a = c, b = d`
Some(id) => {
result.push(ctx.ast.variable_declarator(
id.span(),
decl.kind,
id,
NONE,
Some(init_item),
decl.definite,
));
}
// `[] = [??]` => `[] = [??]`
None => {
// unused literals can be removed `[] = [1]`, `[] = [void 0]`
if !init_item.is_literal_value(false, ctx) {
result.push(ctx.ast.variable_declarator(
init_item.span(),
decl.kind,
ctx.ast.binding_pattern_array_pattern(decl.span, ctx.ast.vec(), NONE),
NONE,
Some(ctx.ast.expression_array(
init_item.span(),
ctx.ast.vec1(ArrayExpressionElement::from(init_item)),
)),
decl.definite,
));
}
}
}
}

if init_iter.len() == 0 {
if !id_pattern.elements.is_empty() {
for id in id_pattern.elements.drain(..).flatten() {
result.push(ctx.ast.variable_declarator(
id.span(),
decl.kind,
id,
NONE,
Some(ctx.ast.void_0(SPAN)),
decl.definite,
));
}
}
if let Some(rest) = &mut id_pattern.rest {
result.push(ctx.ast.variable_declarator(
rest.span(),
decl.kind,
rest.argument.take_in(ctx.ast),
NONE,
Some(ctx.ast.expression_array(rest.span(), ctx.ast.vec())),
decl.definite,
));
}
true
} else if id_pattern.elements.is_empty()
&& let Some(rest) = &mut id_pattern.rest
{
// `[...rest] = [a, b, c]` => `rest = [a, b, c]`
result.push(ctx.ast.variable_declarator(
rest.span(),
decl.kind,
rest.argument.take_in(ctx.ast),
NONE,
Some(ctx.ast.expression_array(rest.span(), ctx.ast.vec_from_iter(init_iter))),
decl.definite,
));
true
} 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.
///
/// For some inputs, this transformation may increase the code size as this transformation
/// injects additional temporary variables. But we assume those cases are rare.
fn simplify_destructuring_assignment(
_kind: VariableDeclarationKind,
declarations: &mut Vec<'a, VariableDeclarator<'a>>,
ctx: &Ctx<'a, '_>,
) -> bool {
let mut changed = false;
let mut i = declarations.len();
while i > 0 {
i -= 1;

let Some(last) = declarations.get_mut(i) else {
continue;
};

if Self::can_simplify_array_to_array_destruction_assignment(last, ctx) {
let mut new_var_decl: Vec<'a, VariableDeclarator<'a>> = ctx.ast.vec();
let to_remove =
Self::simplify_array_destruction_assignment(last, &mut new_var_decl, ctx);

if !new_var_decl.is_empty() {
let len = new_var_decl.len();
if to_remove {
declarations.splice(i..=i, new_var_decl.into_iter());
} else {
declarations.splice(i..i, new_var_decl.into_iter());
}
changed = true;
// check for nested destructuring
i += len;
} 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>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();",
);
}

Expand Down
80 changes: 79 additions & 1 deletion crates/oxc_minifier/tests/peephole/minimize_statements.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::test;
use crate::{test, test_same};

#[test]
fn test_for_variable_declaration() {
Expand Down Expand Up @@ -32,3 +32,81 @@ fn test_for_continue_in_for() {
test("for( a in b ){ c(); continue; }", "for ( a in b ) c();");
test("for( ; ; ){ c(); continue; }", "for ( ; ; ) c();");
}

#[test]
fn test_array_variable_destruction() {
test_same("let [] = []");
test("let [a] = [1]", "let a=1");
test("let [a, b, c, d] = [1, 2, 3, 4]", "let a = 1, b = 2, c = 3, d = 4");
test("let [a, b, c, d] = [1, 2, 3]", "let a = 1, b = 2, c = 3, d");
test("let [a, b, c = 2, d] = [1]", "let a = 1, b, c = 2, d");
test("let [a, b, c] = [1, 2, 3, 4]", "let a = 1, b = 2, c = 3, [] = [4]");
test("let [a, b, c = 2] = [1, 2, 3, 4]", "let a = 1, b = 2, c = 3, [] = [4]");
test("let [a, b, c = 3] = [1, 2]", "let a = 1, b = 2, c = 3");
test("let [a, b] = [1, 2, 3]", "let a = 1, b = 2, [] = [3]");
test("let [a] = [123, 2222, 2222]", "let a = 123, [] = [2222, 2222]");
test_same("let [a = 1] = [void foo()]");
// spread
test("let [...a] = [...b]", "let a = [...b]");
test("let [a, a, ...d] = []", "let a, a, d = []");
test("let [a, ...d] = []", "let a, d = []");
test("let [a, ...d] = [1, ...f]", "let a = 1, d = [...f]");
test("let [a, ...d] = [1, foo]", "let a = 1, d = [foo] ");
test("let [a, b, c, ...d] = [1, 2, ...foo]", "let a = 1, b = 2, [c, ...d] = [...foo]");
test("let [a, b, ...c] = [1, 2, 3, ...foo]", "let a = 1, b = 2, c = [3, ...foo]");
test("let [a, b] = [...c, ...d]", "let [a, b] = [...c, ...d]");
test("let [a, b] = [...c, c, d]", "let [a,b] = [...c, c, d]");
// defaults
test("let [a = 1] = []", "let a = 1");
test("let [a = 1] = [void 0]", "let a = 1");
test("let [a = 1] = [null]", "let a = null");
test_same("let [a = 1] = [foo]");
test("let [a = foo] = [2]", "let a = 2");
test("let [a = foo] = [,]", "let a = foo");
// holes
test("let [, , , ] = [, , , ]", "");
test("let [, , ] = [1, 2]", "");
test("let [a, , c, d] = [, 3, , 4]", "let a, c, d = 4");
test("let [a, , c, d] = [void 0, e, null, f]", "let a, [] = [e], c = null, d = f");
test("let [a, , c, d] = [1, 2, 3, 4]", "let a = 1, c = 3, d = 4");
test("let [ , , a] = [1, 2, 3, 4]", "let a = 3, [] = [4]");
test("let [ , , ...t] = [1, 2, 3, 4]", "let t = [3, 4]");
test("let [ , , ...t] = [1, ...a, 2, , 4]", "let [, ...t] = [...a, 2, , 4]");
test("let [a, , b] = [, , , ]", "let a, b");
test("const [a, , b] = [, , , ]", "const a = void 0, b = void 0;");
// nested
test("let [a, [b, c]] = [1, [2, 3]]", "let a = 1, b = 2, c = 3");
test("let [a, [b, [c, d]]] = [1, ...[2, 3]]", "let a = 1, [[b, [c, d]]] = [...[2, 3]]");
test("let [a, [b, [c, ]]] = [1, [...2, 3]]", "let a = 1, [b, [c]] = [...2, 3]");
test("let [a, [b, [c, ]]] = [1, [2, [...3]]]", "let a = 1, b = 2, [c] = [...3];");
// self reference
test("let [a] = [a]", "let a = a");
test("let [a, b] = [b, a]", "let b = b");
// can't access lexical declaration 'b' before initialization
test("let [a, b] = [b, a]", "let b = b");
test("let [a, ...b] = [b, a]", "let b = [b]");
test_same("let [a, ...b] = [...b, a]");
// SyntaxError: redeclaration of let a
test("let [a, b] = [1, 2], [a, b] = [b, a]", "let a = 1, b = 2, a = 2, b = 2");
test("let [a, b] = [b, a], [a, b] = [b, a]", "let a = b, b = a, a = b, b = a");
// const
test("const [[x, y, z] = [4, 5, 6]] = []", "const x = 4, y = 5, z = 6;");
test("const [a, ...d] = []", "const a = void 0, d = [];");
test("const [a] = []", "const a = void 0");
// vars
test("var [a] = [a]", "var a = a");
test("var [...a] = [b, c]", "var a = [b, c]");
test_same("var [a, b] = [1, ...[2, 3]]");
test_same("var [a, b] = [c, ...[d, e]]");
test_same("var [ , , ...t] = [1, ...a, 2, , 4]");
test("var [a, ...b] = [3, 4, 5]", "var a = 3, b = [4, 5]");
test("var [c, ...d] = [6]", "var c = 6, d = []");
test("var [c, d] = [6]", "var c = 6, d = void 0");
test("var [a, b] = [1, 2]", "var a = 1, b = 2");
test("var [a, b] = [d, c]", "var a = d, b = c");
test_same("var [a, b] = [!d, !a]");
test("var [a, ...b] = [1, 2]", "var a = 1, b = [2]");
test("var [a, b] = [1, 2], [a, b] = [b, a]", "var a = 2, b = 1");
test_same("var [a, b] = [b, a]");
test_same("var [a, b] = [b, a], [a, b] = [b, a]");
}
4 changes: 2 additions & 2 deletions crates/oxc_minifier/tests/peephole/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ fn test_const_to_let() {
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] = [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++);");
Expand Down
Loading