diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index e4b6e85877d9e..37df05cc79dfe 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -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 { diff --git a/crates/oxc_minifier/src/ast_passes/collapse_variable_declarations.rs b/crates/oxc_minifier/src/ast_passes/collapse_variable_declarations.rs index da3431eb95ab6..ae2100e0d213f 100644 --- a/crates/oxc_minifier/src/ast_passes/collapse_variable_declarations.rs +++ b/crates/oxc_minifier/src/ast_passes/collapse_variable_declarations.rs @@ -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` /// +/// +/// Collapse into for statements: +/// `var a = 0; for(;a<0;a++) {}` => `for(var a = 0;a<0;a++) {}` +/// pub struct CollapseVariableDeclarations { pub(crate) changed: bool, } @@ -22,9 +27,14 @@ 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); + if self.changed { + stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_))); + } } } +// Join Vars impl<'a> CollapseVariableDeclarations { pub fn new() -> Self { Self { changed: false } @@ -104,6 +114,113 @@ 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], &stmts[i + 1]) { + (Statement::ExpressionStatement(_), Statement::ForStatement(for_stmt)) + if for_stmt.init.is_none() => + { + let Statement::ExpressionStatement(expr_stmt) = + ctx.ast.move_statement(&mut stmts[i]) + else { + unreachable!() + }; + let Statement::ForStatement(for_stmt) = &mut stmts[i + 1] else { + unreachable!() + }; + for_stmt.init = Some(ForStatementInit::from(expr_stmt.unbox().expression)); + self.changed = true; + } + (Statement::VariableDeclaration(decl), Statement::ForStatement(for_stmt)) + if decl.kind.is_var() + && (for_stmt.init.is_none() + || for_stmt + .init + .as_ref() + .is_some_and(ForStatementInit::is_var_declaration)) => + { + let var_stmt = ctx.ast.move_statement(&mut stmts[i]); + let Statement::VariableDeclaration(var) = var_stmt else { unreachable!() }; + let for_stmt = &mut stmts[i + 1]; + let Statement::ForStatement(for_stmt) = for_stmt else { unreachable!() }; + 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; + } + _ => {} + } + } + (Statement::VariableDeclaration(decl), Statement::ForInStatement(for_stmt)) + if decl.kind.is_var() + && decl.declarations.len() == 1 + && decl.declarations[0].init.is_none() => + { + let ForStatementLeft::AssignmentTargetIdentifier(target_ident) = &for_stmt.left + else { + continue; + }; + let BindingPatternKind::BindingIdentifier(binding) = + &decl.declarations[0].id.kind + else { + continue; + }; + if target_ident.name != binding.name { + continue; + } + let var_stmt = ctx.ast.move_statement(&mut stmts[i]); + let Statement::VariableDeclaration(var) = var_stmt else { unreachable!() }; + let Statement::ForInStatement(for_stmt) = &mut stmts[i + 1] else { + unreachable!() + }; + for_stmt.left = ForStatementLeft::VariableDeclaration(var); + self.changed = true; + } + (Statement::VariableDeclaration(decl), Statement::ForOfStatement(for_stmt)) + if decl.kind.is_var() + && decl.declarations.len() == 1 + && decl.declarations[0].init.is_none() => + { + let ForStatementLeft::AssignmentTargetIdentifier(target_ident) = &for_stmt.left + else { + continue; + }; + let BindingPatternKind::BindingIdentifier(binding) = + &decl.declarations[0].id.kind + else { + continue; + }; + if target_ident.name != binding.name { + continue; + } + let var_stmt = ctx.ast.move_statement(&mut stmts[i]); + let Statement::VariableDeclaration(var) = var_stmt else { unreachable!() }; + let Statement::ForOfStatement(for_stmt) = &mut stmts[i + 1] else { + unreachable!() + }; + for_stmt.left = ForStatementLeft::VariableDeclaration(var); + self.changed = true; + } + _ => {} + } + } + } +} + /// #[cfg(test)] mod test { @@ -121,151 +238,253 @@ 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;"); + #[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;"); + // With initial values + test("var a = 1;var b = 1;", "var a=1,b=1;"); - // Already collapsed - test_same("var a, b;"); + // Already collapsed + test_same("var a, b;"); - // Already collapsed with values - test_same("var a = 1, b = 1;"); + // 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 + 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;"); + // 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( + "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_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_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_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_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;}"); - } + #[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;"); + // 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;"); + // With initial values + test("const a = 1;const b = 1;", "const a=1,b=1;"); - // Already collapsed - test_same("let a, b;"); + // Already collapsed + test_same("let a, b;"); - // Already collapsed with values - test_same("let a = 1, b = 1;"); + // 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 + 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;"); + // 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;"); - } + // 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_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_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_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; }"); + #[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; }"); - } + // 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_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_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"); + /// + #[cfg(test)] + mod collapse_for { + use super::{test, test_same}; + + #[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()"); + + // 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()"); + + // 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()"); + + // 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()}", + ); + + // Any other expression. + test("init(); for(; a < 2 ; a++) foo()", "for(init(); a < 2 ; a++) foo();"); + + // 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() }"); + + // 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( + // "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_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()"); + + // 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()"); + + // Verify FOR inside IFs. + test("if(x){var a; for(a in b) foo()}", "if(x){for(var a in b) foo()}"); + + // Any other expression. + test_same("init(); for(a in b) foo()"); + + // Other statements are left as is. + test_same("function f(){ return; for(a in b) foo() }"); + + // 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();"); + } + + #[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()"); + + // 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()"); + + // Verify FOR inside IFs. + test("if (x) { var a; for (a of b) foo() }", "if (x) { for (var a of b) foo() }"); + + // Any other expression. + test_same("init(); for (a of b) foo()"); + + // Other statements are left as is. + test_same("function f() { return; for (a of b) foo() }"); + + // 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();"); + } } } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 517880f8272fb..93646406847ed 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -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