From 946669bd830f7f173b054b706a4d3eea20e5e58e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sun, 31 Aug 2025 15:09:55 +0000 Subject: [PATCH] fix(minifier): inline multiple variable declarations at once (#13477) Codes like: ```js var a1 = 'a1' var a2 = 'a2' var a3 = 'a3' var arr = [ a1, a2, a3, ] console.log(arr) ``` was not processes at once for DCE because DCE does not join variable declarations. This PR fixes that. --- .../src/peephole/minimize_statements.rs | 115 +++++++++--------- .../tests/peephole/dead_code_elimination.rs | 75 ++++++++++++ 2 files changed, 135 insertions(+), 55 deletions(-) diff --git a/crates/oxc_minifier/src/peephole/minimize_statements.rs b/crates/oxc_minifier/src/peephole/minimize_statements.rs index c490bddf08f5b..66b0e16ed9712 100644 --- a/crates/oxc_minifier/src/peephole/minimize_statements.rs +++ b/crates/oxc_minifier/src/peephole/minimize_statements.rs @@ -1038,65 +1038,70 @@ impl<'a> PeepholeOptimizations { if Self::keep_top_level_var_in_script_mode(ctx) { return false; } - let Some(Statement::VariableDeclaration(prev_var_decl)) = stmts.last_mut() else { - return false; - }; - if prev_var_decl.kind.is_using() { - return false; - } - let last_non_inlined_index = prev_var_decl.declarations.iter_mut().rposition(|prev_decl| { - let Some(prev_decl_init) = &mut prev_decl.init else { - return true; - }; - let BindingPatternKind::BindingIdentifier(prev_decl_id) = &prev_decl.id.kind else { - return true; - }; - if ctx.is_expression_whose_name_needs_to_be_kept(prev_decl_init) { - return true; - } - let Some(symbol_value) = - ctx.state.symbol_values.get_symbol_value(prev_decl_id.symbol_id()) - else { - return true; - }; - // we should check whether it's exported by `symbol_value.exported` - // because the variable might be exported with `export { foo }` rather than `export var foo` - if symbol_value.exported - || symbol_value.read_references_count > 1 - || symbol_value.write_references_count > 0 - { - return true; - } - let replaced = Self::substitute_single_use_symbol_in_expression( - expr_in_stmt, - &prev_decl_id.name, - prev_decl_init, - prev_decl_init.may_have_side_effects(ctx), - ctx, - ); - if replaced != Some(true) { - return true; - } - false - }); - match last_non_inlined_index { - None => { - // all inlined - stmts.pop(); - true - } - Some(last_non_inlined_index) - if last_non_inlined_index + 1 == prev_var_decl.declarations.len() => - { - // no change - false + let mut inlined = false; + while let Some(Statement::VariableDeclaration(prev_var_decl)) = stmts.last_mut() { + if prev_var_decl.kind.is_using() { + break; } - Some(last_non_inlined_index) => { - prev_var_decl.declarations.truncate(last_non_inlined_index + 1); - true + + let last_non_inlined_index = + prev_var_decl.declarations.iter_mut().rposition(|prev_decl| { + let Some(prev_decl_init) = &mut prev_decl.init else { + return true; + }; + let BindingPatternKind::BindingIdentifier(prev_decl_id) = &prev_decl.id.kind + else { + return true; + }; + if ctx.is_expression_whose_name_needs_to_be_kept(prev_decl_init) { + return true; + } + let Some(symbol_value) = + ctx.state.symbol_values.get_symbol_value(prev_decl_id.symbol_id()) + else { + return true; + }; + // we should check whether it's exported by `symbol_value.exported` + // because the variable might be exported with `export { foo }` rather than `export var foo` + if symbol_value.exported + || symbol_value.read_references_count > 1 + || symbol_value.write_references_count > 0 + { + return true; + } + let replaced = Self::substitute_single_use_symbol_in_expression( + expr_in_stmt, + &prev_decl_id.name, + prev_decl_init, + prev_decl_init.may_have_side_effects(ctx), + ctx, + ); + if replaced != Some(true) { + return true; + } + false + }); + match last_non_inlined_index { + None => { + // all inlined + stmts.pop(); + inlined = true; + } + Some(last_non_inlined_index) + if last_non_inlined_index + 1 == prev_var_decl.declarations.len() => + { + // no change + break; + } + Some(last_non_inlined_index) => { + prev_var_decl.declarations.truncate(last_non_inlined_index + 1); + inlined = true; + break; + } } } + inlined } /// Returns Some(true) when the expression is successfully replaced. diff --git a/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs b/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs index cd45aacb4562d..f6655edb3d009 100644 --- a/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs +++ b/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs @@ -271,3 +271,78 @@ fn dce_from_terser() { "console.log(foo, bar, Baz);", ); } + +#[test] +fn dce_iterations() { + test( + " +var a1 = 'a1' +var a2 = 'a2' +var a3 = 'a3' +var a4 = 'a4' +var a5 = 'a5' +var a6 = 'a6' +var a7 = 'a7' +var a8 = 'a8' +var a9 = 'a9' +var a10 = 'a10' +var a11 = 'a11' +var a12 = 'a12' +var a13 = 'a13' +var a14 = 'a14' +var a15 = 'a15' +var a16 = 'a16' +var a17 = 'a17' +var a18 = 'a18' +var a19 = 'a19' +var a20 = 'a20' +var arr = [ + a1, + a2, + a3, + a4, + a5, + a6, + a7, + a8, + a9, + a10, + a11, + a12, + a13, + a14, + a15, + a16, + a17, + a18, + a19, + a20 +] +console.log(arr) + ", + " +console.log([ + 'a1', + 'a2', + 'a3', + 'a4', + 'a5', + 'a6', + 'a7', + 'a8', + 'a9', + 'a10', + 'a11', + 'a12', + 'a13', + 'a14', + 'a15', + 'a16', + 'a17', + 'a18', + 'a19', + 'a20' +]) + ", + ); +}