feat(minifier): collapse var into for loop initializer (#8119)

`var a = 0; for(;a<0;a++) {}` => `for(var a = 0;a<0;a++) {}`
This commit is contained in:
Boshen 2024-12-26 05:22:02 +00:00
parent c22062bab3
commit fef0b25fd3
3 changed files with 373 additions and 134 deletions

View file

@ -886,6 +886,11 @@ impl fmt::Display for VariableDeclarationKind {
}
impl ForStatementInit<'_> {
/// Is `var` declaration
pub fn is_var_declaration(&self) -> bool {
matches!(self, Self::VariableDeclaration(decl) if decl.kind.is_var())
}
/// LexicalDeclaration[In, Yield, Await] :
/// LetOrConst BindingList[?In, ?Yield, ?Await] ;
pub fn is_lexical_declaration(&self) -> bool {

View file

@ -6,8 +6,13 @@ use crate::CompressorPass;
/// Collapse variable declarations.
///
/// Join Vars:
/// `var a; var b = 1; var c = 2` => `var a, b = 1; c = 2`
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/CollapseVariableDeclarations.java>
///
/// Collapse into for statements:
/// `var a = 0; for(;a<0;a++) {}` => `for(var a = 0;a<0;a++) {}`
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/Denormalize.java>
pub struct CollapseVariableDeclarations {
pub(crate) changed: bool,
}
@ -22,9 +27,11 @@ impl<'a> CompressorPass<'a> for CollapseVariableDeclarations {
impl<'a> Traverse<'a> for CollapseVariableDeclarations {
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
self.join_vars(stmts, ctx);
self.maybe_collapse_into_for_statements(stmts, ctx);
}
}
// Join Vars
impl<'a> CollapseVariableDeclarations {
pub fn new() -> Self {
Self { changed: false }
@ -104,6 +111,127 @@ impl<'a> CollapseVariableDeclarations {
}
}
// Collapse into for statements
impl<'a> CollapseVariableDeclarations {
fn maybe_collapse_into_for_statements(
&mut self,
stmts: &mut Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
if stmts.len() <= 1 {
return;
}
for i in 0..stmts.len() - 1 {
match &stmts[i + 1] {
Statement::ForStatement(for_stmt) => match &stmts[i] {
Statement::ExpressionStatement(_) if for_stmt.init.is_none() => {
self.collapse_expr_into_for(i, stmts, ctx);
}
Statement::VariableDeclaration(decl)
if decl.kind.is_var()
&& (for_stmt.init.is_none()
|| for_stmt
.init
.as_ref()
.is_some_and(ForStatementInit::is_var_declaration)) =>
{
self.collapse_var_into_for(i, stmts, ctx);
}
_ => {}
},
Statement::ForInStatement(_) | Statement::ForOfStatement(_) => {
self.collapse_var_into_for_in_or_for_of(i, stmts, ctx);
}
_ => {}
}
}
if self.changed {
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
}
}
fn collapse_expr_into_for(
&mut self,
i: usize,
stmts: &mut Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
if let Statement::ExpressionStatement(expr_stmt) = ctx.ast.move_statement(&mut stmts[i]) {
if let Statement::ForStatement(for_stmt) = &mut stmts[i + 1] {
for_stmt.init = Some(ForStatementInit::from(expr_stmt.unbox().expression));
self.changed = true;
};
}
}
fn collapse_var_into_for(
&mut self,
i: usize,
stmts: &mut Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
if let Statement::VariableDeclaration(var) = ctx.ast.move_statement(&mut stmts[i]) {
if let Statement::ForStatement(for_stmt) = &mut stmts[i + 1] {
match for_stmt.init.as_mut() {
Some(ForStatementInit::VariableDeclaration(for_var)) => {
for_var.declarations.splice(0..0, var.unbox().declarations);
self.changed = true;
}
None => {
for_stmt.init = Some(ForStatementInit::VariableDeclaration(var));
self.changed = true;
}
_ => {
unreachable!()
}
}
};
}
}
fn collapse_var_into_for_in_or_for_of(
&mut self,
i: usize,
stmts: &mut Vec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
) {
if let Statement::VariableDeclaration(decl) = &stmts[i] {
if decl.kind.is_var()
&& decl.declarations.len() == 1
&& decl.declarations[0].init.is_none()
{
if let BindingPatternKind::BindingIdentifier(binding) =
&decl.declarations[0].id.kind
{
if let ForStatementLeft::AssignmentTargetIdentifier(target) =
match &stmts[i + 1] {
Statement::ForInStatement(stmt) => &stmt.left,
Statement::ForOfStatement(stmt) => &stmt.left,
_ => unreachable!(),
}
{
if binding.name == target.name {
let var_stmt = ctx.ast.move_statement(&mut stmts[i]);
let Statement::VariableDeclaration(var) = var_stmt else {
unreachable!()
};
let left = match &mut stmts[i + 1] {
Statement::ForInStatement(stmt) => &mut stmt.left,
Statement::ForOfStatement(stmt) => &mut stmt.left,
_ => unreachable!(),
};
*left = ForStatementLeft::VariableDeclaration(var);
self.changed = true;
}
}
}
}
}
}
}
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/CollapseVariableDeclarationsTest.java>
#[cfg(test)]
mod test {
@ -121,151 +249,257 @@ mod test {
test(source_text, source_text);
}
#[test]
fn cjs() {
// Do not join `require` calls for cjs-module-lexer.
test_same(
"
Object.defineProperty(exports, '__esModule', { value: true });
var compilerDom = require('@vue/compiler-dom');
var runtimeDom = require('@vue/runtime-dom');
var shared = require('@vue/shared');
",
);
mod join_vars {
use super::{test, test_same};
#[test]
fn cjs() {
// Do not join `require` calls for cjs-module-lexer.
test_same(
" Object.defineProperty(exports, '__esModule', { value: true });
var compilerDom = require('@vue/compiler-dom');
var runtimeDom = require('@vue/runtime-dom');
var shared = require('@vue/shared');
",
);
}
#[test]
fn test_collapsing() {
// Basic collapsing
test("var a;var b;", "var a,b;");
// With initial values
test("var a = 1;var b = 1;", "var a=1,b=1;");
// Already collapsed
test_same("var a, b;");
// Already collapsed with values
test_same("var a = 1, b = 1;");
// Some already collapsed
test("var a;var b, c;var d;", "var a,b,c,d;");
// Some already collapsed with values
test("var a = 1;var b = 2, c = 3;var d = 4;", "var a=1,b=2,c=3,d=4;");
test(
"var x = 2; foo(x); x = 3; x = 1; var y = 2; var z = 4; x = 5",
"var x = 2; foo(x); x = 3; x = 1; var y = 2, z = 4; x = 5",
);
}
#[test]
fn test_issue820() {
// Don't redeclare function parameters, this is incompatible with
// strict mode.
test_same("function f(a){ var b=1; a=2; var c; }");
}
#[test]
fn test_if_else_var_declarations() {
test_same("if (x) var a = 1; else var b = 2;");
}
#[test]
fn test_aggressive_redeclaration_in_for() {
test_same("for(var x = 1; x = 2; x = 3) {x = 4}");
test_same("for(var x = 1; y = 2; z = 3) {var a = 4}");
test_same("var x; for(x = 1; x = 2; z = 3) {x = 4}");
}
#[test]
fn test_issue397() {
test_same("var x; x = 5; var z = 7;");
test("var x; var y = 3; x = 5;", "var x, y = 3; x = 5;");
test("var a = 1; var x; var y = 3; x = 5;", "var a = 1, x, y = 3; x = 5;");
test("var x; var y = 3; x = 5; var z = 7;", "var x, y = 3; x = 5; var z = 7;");
}
#[test]
fn test_arguments_assignment() {
test_same("function f() {arguments = 1;}");
}
// ES6 Tests
#[test]
fn test_collapsing_let_const() {
// Basic collapsing
test("let a;let b;", "let a,b;");
// With initial values
test("const a = 1;const b = 1;", "const a=1,b=1;");
// Already collapsed
test_same("let a, b;");
// Already collapsed with values
test_same("let a = 1, b = 1;");
// Some already collapsed
test("let a;let b, c;let d;", "let a,b,c,d;");
// Some already collapsed with values
test("let a = 1;let b = 2, c = 3;let d = 4;", "let a=1,b=2,c=3,d=4;");
// Different variable types
test_same("let a = 1; const b = 2;");
}
#[test]
fn test_if_else_var_declarations_let() {
test_same("if (x) { let a = 1; } else { let b = 2; }");
}
#[test]
fn test_aggressive_redeclaration_of_let_in_for() {
test_same("for(let x = 1; x = 2; x = 3) {x = 4}");
test_same("for(let x = 1; y = 2; z = 3) {let a = 4}");
test_same("let x; for(x = 1; x = 2; z = 3) {x = 4}");
}
#[test]
fn test_redeclaration_let_in_function() {
test(
"function f() { let x = 1; let y = 2; let z = 3; x + y + z; }",
"function f() { let x = 1, y = 2, z = 3; x + y + z; } ",
);
// recognize local scope version of x
test(
"var x = 1; function f() { let x = 1; let y = 2; x + y; }",
"var x = 1; function f() { let x = 1, y = 2; x + y } ",
);
// do not redeclare function parameters
// incompatible with strict mode
test_same("function f(x) { let y = 3; x = 4; x + y; }");
}
#[test]
fn test_arrow_function() {
test("() => {let x = 1; let y = 2; x + y; }", "() => {let x = 1, y = 2; x + y; }");
// do not redeclare function parameters
// incompatible with strict mode
test_same("(x) => {x = 4; let y = 2; x + y; }");
}
#[test]
fn test_uncollapsable_declarations() {
test_same("let x = 1; var y = 2; const z = 3");
test_same("let x = 1; var y = 2; let z = 3;");
}
#[test]
fn test_mixed_declaration_types() {
// lets, vars, const declarations consecutive
test("let x = 1; let z = 3; var y = 2;", "let x = 1, z = 3; var y = 2;");
test(
"let x = 1; let y = 2; var z = 3; var a = 4;",
"let x = 1, y = 2; var z = 3, a = 4",
);
}
}
#[test]
fn test_collapsing() {
// Basic collapsing
test("var a;var b;", "var a,b;");
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/DenormalizeTest.java>
#[cfg(test)]
mod collapse_for {
use super::{test, test_same};
// With initial values
test("var a = 1;var b = 1;", "var a=1,b=1;");
#[test]
fn test_for() {
// Verify assignments are moved into the FOR init node.
test("a = 0; for(; a < 2 ; a++) foo()", "for(a = 0; a < 2 ; a++) foo();");
// Verify vars are are moved into the FOR init node.
test("var a = 0; for(; c < b ; c++) foo()", "for(var a = 0; c < b ; c++) foo()");
test(
"var a = 0; var b = 0; for(; c < b ; c++) foo()",
"for(var a = 0, b = 0; c < b ; c++) foo()",
);
// Already collapsed
test_same("var a, b;");
// We don't handle labels yet.
test_same("var a = 0; a:for(; c < b ; c++) foo()");
test_same("var a = 0; a:b:for(; c < b ; c++) foo()");
// Already collapsed with values
test_same("var a = 1, b = 1;");
// Do not inline let or const
test_same("let a = 0; for(; c < b ; c++) foo()");
test_same("const a = 0; for(; c < b ; c++) foo()");
// Some already collapsed
test("var a;var b, c;var d;", "var a,b,c,d;");
// Verify FOR inside IFs.
test(
"if(x){var a = 0; for(; c < b; c++) foo()}",
"if(x){for(var a = 0; c < b; c++) foo()}",
);
// Some already collapsed with values
test("var a = 1;var b = 2, c = 3;var d = 4;", "var a=1,b=2,c=3,d=4;");
// Any other expression.
test("init(); for(; a < 2 ; a++) foo()", "for(init(); a < 2 ; a++) foo();");
test(
"var x = 2; foo(x); x = 3; x = 1; var y = 2; var z = 4; x = 5",
"var x = 2; foo(x); x = 3; x = 1; var y = 2, z = 4; x = 5",
);
}
// Other statements are left as is.
test(
"function f(){ var a; for(; a < 2 ; a++) foo() }",
"function f(){ for(var a; a < 2 ; a++) foo() }",
);
test_same("function f(){ return; for(; a < 2 ; a++) foo() }");
#[test]
fn test_issue820() {
// Don't redeclare function parameters, this is incompatible with
// strict mode.
test_same("function f(a){ var b=1; a=2; var c; }");
}
// TODO
// Verify destructuring assignments are moved.
// test(
// "[a, b] = [1, 2]; for (; a < 2; a = b++) foo();",
// "for ([a, b] = [1, 2]; a < 2; a = b++) foo();",
// );
#[test]
fn test_if_else_var_declarations() {
test_same("if (x) var a = 1; else var b = 2;");
}
// test(
// "var [a, b] = [1, 2]; for (; a < 2; a = b++) foo();",
// "var a; var b; for ([a, b] = [1, 2]; a < 2; a = b++) foo();",
// );
}
#[test]
fn test_aggressive_redeclaration_in_for() {
test_same("for(var x = 1; x = 2; x = 3) {x = 4}");
test_same("for(var x = 1; y = 2; z = 3) {var a = 4}");
test_same("var x; for(x = 1; x = 2; z = 3) {x = 4}");
}
#[test]
fn test_for_in() {
test("var a; for(a in b) foo()", "for (var a in b) foo()");
test_same("a = 0; for(a in b) foo()");
test_same("var a = 0; for(a in b) foo()");
#[test]
fn test_issue397() {
test_same("var x; x = 5; var z = 7;");
test("var x; var y = 3; x = 5;", "var x, y = 3; x = 5;");
test("var a = 1; var x; var y = 3; x = 5;", "var a = 1, x, y = 3; x = 5;");
test("var x; var y = 3; x = 5; var z = 7;", "var x, y = 3; x = 5; var z = 7;");
}
// We don't handle labels yet.
test_same("var a; a:for(a in b) foo()");
test_same("var a; a:b:for(a in b) foo()");
#[test]
fn test_arguments_assignment() {
test_same("function f() {arguments = 1;}");
}
// Verify FOR inside IFs.
test("if(x){var a; for(a in b) foo()}", "if(x){for(var a in b) foo()}");
// ES6 Tests
#[test]
fn test_collapsing_let_const() {
// Basic collapsing
test("let a;let b;", "let a,b;");
// Any other expression.
test_same("init(); for(a in b) foo()");
// With initial values
test("const a = 1;const b = 1;", "const a=1,b=1;");
// Other statements are left as is.
test_same("function f(){ return; for(a in b) foo() }");
// Already collapsed
test_same("let a, b;");
// We don't handle destructuring patterns yet.
test("var a; var b; for ([a, b] in c) foo();", "var a, b; for ([a, b] in c) foo();");
}
// Already collapsed with values
test_same("let a = 1, b = 1;");
#[test]
fn test_for_of() {
test("var a; for (a of b) foo()", "for (var a of b) foo()");
test_same("a = 0; for (a of b) foo()");
test_same("var a = 0; for (a of b) foo()");
// Some already collapsed
test("let a;let b, c;let d;", "let a,b,c,d;");
// We don't handle labels yet.
test_same("var a; a: for (a of b) foo()");
test_same("var a; a: b: for (a of b) foo()");
// Some already collapsed with values
test("let a = 1;let b = 2, c = 3;let d = 4;", "let a=1,b=2,c=3,d=4;");
// Verify FOR inside IFs.
test("if (x) { var a; for (a of b) foo() }", "if (x) { for (var a of b) foo() }");
// Different variable types
test_same("let a = 1; const b = 2;");
}
// Any other expression.
test_same("init(); for (a of b) foo()");
#[test]
fn test_if_else_var_declarations_let() {
test_same("if (x) { let a = 1; } else { let b = 2; }");
}
// Other statements are left as is.
test_same("function f() { return; for (a of b) foo() }");
#[test]
fn test_aggressive_redeclaration_of_let_in_for() {
test_same("for(let x = 1; x = 2; x = 3) {x = 4}");
test_same("for(let x = 1; y = 2; z = 3) {let a = 4}");
test_same("let x; for(x = 1; x = 2; z = 3) {x = 4}");
}
#[test]
fn test_redeclaration_let_in_function() {
test(
"function f() { let x = 1; let y = 2; let z = 3; x + y + z; }",
"function f() { let x = 1, y = 2, z = 3; x + y + z; } ",
);
// recognize local scope version of x
test(
"var x = 1; function f() { let x = 1; let y = 2; x + y; }",
"var x = 1; function f() { let x = 1, y = 2; x + y } ",
);
// do not redeclare function parameters
// incompatible with strict mode
test_same("function f(x) { let y = 3; x = 4; x + y; }");
}
#[test]
fn test_arrow_function() {
test("() => {let x = 1; let y = 2; x + y; }", "() => {let x = 1, y = 2; x + y; }");
// do not redeclare function parameters
// incompatible with strict mode
test_same("(x) => {x = 4; let y = 2; x + y; }");
}
#[test]
fn test_uncollapsable_declarations() {
test_same("let x = 1; var y = 2; const z = 3");
test_same("let x = 1; var y = 2; let z = 3;");
}
#[test]
fn test_mixed_declaration_types() {
// lets, vars, const declarations consecutive
test("let x = 1; let z = 3; var y = 2;", "let x = 1, z = 3; var y = 2;");
test("let x = 1; let y = 2; var z = 3; var a = 4;", "let x = 1, y = 2; var z = 3, a = 4");
// We don't handle destructuring patterns yet.
test("var a; var b; for ([a, b] of c) foo();", "var a, b; for ([a, b] of c) foo();");
}
}
}

View file

@ -1,27 +1,27 @@
| Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.96 kB | 23.70 kB | 8.58 kB | 8.54 kB | react.development.js
72.14 kB | 23.94 kB | 23.70 kB | 8.59 kB | 8.54 kB | react.development.js
173.90 kB | 61.52 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js
287.63 kB | 92.51 kB | 90.07 kB | 32.29 kB | 31.95 kB | jquery.js
287.63 kB | 92.47 kB | 90.07 kB | 32.30 kB | 31.95 kB | jquery.js
342.15 kB | 121.48 kB | 118.14 kB | 44.65 kB | 44.37 kB | vue.js
342.15 kB | 121.36 kB | 118.14 kB | 44.67 kB | 44.37 kB | vue.js
544.10 kB | 73.32 kB | 72.48 kB | 26.13 kB | 26.20 kB | lodash.js
555.77 kB | 275.83 kB | 270.13 kB | 91.13 kB | 90.80 kB | d3.js
555.77 kB | 275.77 kB | 270.13 kB | 91.12 kB | 90.80 kB | d3.js
1.01 MB | 466.57 kB | 458.89 kB | 126.69 kB | 126.71 kB | bundle.min.js
1.01 MB | 466.38 kB | 458.89 kB | 126.72 kB | 126.71 kB | bundle.min.js
1.25 MB | 661.44 kB | 646.76 kB | 163.95 kB | 163.73 kB | three.js
1.25 MB | 660.41 kB | 646.76 kB | 163.99 kB | 163.73 kB | three.js
2.14 MB | 740.15 kB | 724.14 kB | 181.34 kB | 181.07 kB | victory.js
2.14 MB | 740.08 kB | 724.14 kB | 181.34 kB | 181.07 kB | victory.js
3.20 MB | 1.02 MB | 1.01 MB | 332.02 kB | 331.56 kB | echarts.js
3.20 MB | 1.02 MB | 1.01 MB | 332.26 kB | 331.56 kB | echarts.js
6.69 MB | 2.39 MB | 2.31 MB | 495.64 kB | 488.28 kB | antd.js
6.69 MB | 2.39 MB | 2.31 MB | 495.65 kB | 488.28 kB | antd.js
10.95 MB | 3.54 MB | 3.49 MB | 909.75 kB | 915.50 kB | typescript.js
10.95 MB | 3.54 MB | 3.49 MB | 909.94 kB | 915.50 kB | typescript.js