mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
fix(minifier): preserve sibling declarations in arguments copy rewrite (#27140)
## Summary - Keep sibling `var` declarators when rewriting an `arguments` copy loop to `[...arguments]`. This preserves bindings that later code still uses. - Leave loops with initialized sibling declarators unchanged, because moving the spread across an initializer could change evaluation order. Fixes #27138
This commit is contained in:
1 parent
6531ce925c
commit
4cf9ae275b
3 files changed
+39
-19
No files matched your search
@@ -844,8 +844,12 @@ impl<'a> PeepholeOptimizations {
|
||||
|
||||
let Some(init) = &mut for_stmt.init else { return };
|
||||
let ForStatementInit::VariableDeclaration(var_init) = init else { return };
|
||||
// Need at least two declarators: r, a (optional `e` may precede them)
|
||||
if var_init.declarations.len() < init_decl_len {
|
||||
let Some(siblings) = var_init.declarations.get(init_decl_len..) else { return };
|
||||
// Sibling declarators may be in the source loop or folded into it by `sequences`.
|
||||
// Keep uninitialized `var` siblings, but don't move an initializer across the spread.
|
||||
if !siblings.is_empty()
|
||||
&& (!var_init.kind.is_var() || siblings.iter().any(|decl| decl.init.is_some()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -951,7 +955,7 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
}
|
||||
|
||||
// Build `var r = [...arguments]` (with optional `.slice(offset)`) as the only declarator and drop test/update/body.
|
||||
// Build `var r = [...arguments]` (with optional `.slice(offset)`) and drop test/update/body.
|
||||
|
||||
let r_id_pat = {
|
||||
let (r_id, de_id_symbol_id) = r_id_pat_with_info;
|
||||
@@ -995,23 +999,21 @@ impl<'a> PeepholeOptimizations {
|
||||
};
|
||||
|
||||
let new_decl = VariableDeclarator::new(SPAN, r_id_pat, None, Some(arr), false, ctx);
|
||||
// The old declarators (`e`, `a`, and `r`'s original init) are
|
||||
// replaced wholesale — walk them so refs inside (e.g. `e` in
|
||||
// `Array(e > 1 ? e - 1 : 0)`) reach `PassChanges`. The moved-out
|
||||
// `r` binding and `arguments` ident left id-less dummies behind.
|
||||
for decl in &var_init.declarations {
|
||||
ctx.drop_variable_declarator(decl);
|
||||
// Drop only the copy-loop declarators. Siblings remain in the `var` statement.
|
||||
// Walk removed declarators so refs inside them reach `PassChanges`.
|
||||
for decl in var_init.declarations.drain(..init_decl_len) {
|
||||
ctx.drop_variable_declarator(&decl);
|
||||
}
|
||||
var_init.declarations = ArenaVec::from_value_in(new_decl, ctx);
|
||||
var_init.declarations.insert(0, new_decl);
|
||||
} else {
|
||||
// `for (var; 0;)` with an empty `VariableDeclaration` is invalid JS when printed and
|
||||
// makes `try_fold_for` hoist a bogus `var;`. Use `for (; 0;)` instead so dead-code
|
||||
// folding becomes an empty statement. Walk the dropped
|
||||
// declarators so their refs reach `PassChanges`.
|
||||
for decl in &var_init.declarations {
|
||||
ctx.drop_variable_declarator(decl);
|
||||
// The copied array is unused, but sibling declarations still need to survive.
|
||||
for decl in var_init.declarations.drain(..init_decl_len) {
|
||||
ctx.drop_variable_declarator(&decl);
|
||||
}
|
||||
// Avoid invalid `for (var; 0;)` when no siblings remain.
|
||||
if var_init.declarations.is_empty() {
|
||||
for_stmt.init = None;
|
||||
}
|
||||
for_stmt.init = None;
|
||||
}
|
||||
if let Some(old) = for_stmt.test.take() {
|
||||
ctx.drop_expression(&old);
|
||||
|
||||
@@ -1054,6 +1054,24 @@ fn test_rewrite_arguments_copy_loop() {
|
||||
test_same(
|
||||
"function _() { for (var e = arguments.length, r = Array(e), a = 0; a < e; a++) r[a] = arguments[a]; console.log(r, a) }",
|
||||
);
|
||||
|
||||
// A previous pass may fold sibling `var` declarations into the loop initializer.
|
||||
// Those bindings must survive even when the copy loop looks otherwise rewritable.
|
||||
test(
|
||||
"function _() { for (var r = [], a = 0, keep; a < arguments.length; a++) r[a] = arguments[a]; use(r, keep) }",
|
||||
"function _() { var r = [...arguments], keep; use(r, keep) }",
|
||||
);
|
||||
test(
|
||||
"function _() { var items = [], t = 0, i, u, r; for (; t < arguments.length; t++) items[t] = arguments[t]; if (this.list = [], items != null) for (i = 0, u = items; i < u.length; i++) r = u[i], this.list.push(r) }",
|
||||
"function _() { var items = [...arguments], i, u, r; if (this.list = [], items != null) for (i = 0, u = items; i < u.length; i++) r = u[i], this.list.push(r) }",
|
||||
);
|
||||
test(
|
||||
"function _() { for (var r = [], a = 0, keep; a < arguments.length; a++) r[a] = arguments[a]; use(keep) }",
|
||||
"function _() { var keep; use(keep) }",
|
||||
);
|
||||
test_same(
|
||||
"function _() { for (var r = [], a = 0, keep = side(); a < arguments.length; a++) r[a] = arguments[a]; use(r, keep) }",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 860
|
||||
sys alloc bytes: 29973448 # 29.97 MB
|
||||
sys peak growth: 19934725 # 19.93 MB
|
||||
arena allocs: 181501
|
||||
arena allocs: 181469
|
||||
arena reallocs: 34406
|
||||
arena size: 43041368 # 43.04 MB
|
||||
arena size: 43039576 # 43.04 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
|
||||
Reference in new issue
Block a user