From 8ea9e38ee5df9af08bbb3e072f22650c35c09a2d Mon Sep 17 00:00:00 2001 From: Boshen Date: Sat, 27 May 2023 10:52:15 +0800 Subject: [PATCH] feat(minifier): remove redundant curly braces from block statements (#390) --- crates/oxc_ast/src/ast/js.rs | 16 ++++++ crates/oxc_ast_lower/src/lib.rs | 10 ++++ crates/oxc_minifier/examples/minifier.rs | 7 +++ crates/oxc_minifier/src/compressor/mod.rs | 15 ++++++ crates/oxc_minifier/src/printer/gen.rs | 50 ++++++++++++++++++- crates/oxc_minifier/src/printer/mod.rs | 3 ++ .../closure/substitute_alternate_syntax.rs | 2 +- crates/oxc_minifier/tests/terser/mod.rs | 5 +- crates/oxc_semantic2/src/builder.rs | 27 ++++++++++ crates/oxc_semantic2/src/mangler.rs | 5 -- tasks/minsize/minsize.snap | 24 ++++----- 11 files changed, 143 insertions(+), 21 deletions(-) diff --git a/crates/oxc_ast/src/ast/js.rs b/crates/oxc_ast/src/ast/js.rs index 5a4cdf7d9..0b1ef1f05 100644 --- a/crates/oxc_ast/src/ast/js.rs +++ b/crates/oxc_ast/src/ast/js.rs @@ -1018,6 +1018,14 @@ pub enum ForStatementInit<'a> { Expression(Expression<'a>), } +impl<'a> ForStatementInit<'a> { + /// LexicalDeclaration[In, Yield, Await] : + /// LetOrConst BindingList[?In, ?Yield, ?Await] ; + pub fn is_lexical_declaration(&self) -> bool { + matches!(self, Self::VariableDeclaration(decl) if decl.kind.is_lexical()) + } +} + /// For-In Statement #[derive(Debug, Hash)] #[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))] @@ -1048,6 +1056,14 @@ pub enum ForStatementLeft<'a> { AssignmentTarget(AssignmentTarget<'a>), } +impl<'a> ForStatementLeft<'a> { + /// LexicalDeclaration[In, Yield, Await] : + /// LetOrConst BindingList[?In, ?Yield, ?Await] ; + pub fn is_lexical_declaration(&self) -> bool { + matches!(self, Self::VariableDeclaration(decl) if decl.kind.is_lexical()) + } +} + /// Continue Statement #[derive(Debug, Hash)] #[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))] diff --git a/crates/oxc_ast_lower/src/lib.rs b/crates/oxc_ast_lower/src/lib.rs index 67e8432f5..082d8f44b 100644 --- a/crates/oxc_ast_lower/src/lib.rs +++ b/crates/oxc_ast_lower/src/lib.rs @@ -133,10 +133,14 @@ impl<'a> AstLower<'a> { } fn lower_for_statement(&mut self, stmt: &ast::ForStatement<'a>) -> hir::Statement<'a> { + let is_lexical_declaration = + stmt.init.as_ref().is_some_and(ast::ForStatementInit::is_lexical_declaration); + self.semantic.enter_for_statement(is_lexical_declaration); let init = stmt.init.as_ref().map(|init| self.lower_for_statement_init(init)); let test = stmt.test.as_ref().map(|expr| self.lower_expression(expr)); let update = stmt.update.as_ref().map(|expr| self.lower_expression(expr)); let body = self.lower_statement(&stmt.body); + self.semantic.leave_for_statement(is_lexical_declaration); self.hir.for_statement(stmt.span, init, test, update, body) } @@ -155,16 +159,22 @@ impl<'a> AstLower<'a> { } fn lower_for_in_statement(&mut self, stmt: &ast::ForInStatement<'a>) -> hir::Statement<'a> { + let is_lexical_declaration = stmt.left.is_lexical_declaration(); + self.semantic.enter_for_in_of_statement(is_lexical_declaration); let left = self.lower_for_statement_left(&stmt.left); let right = self.lower_expression(&stmt.right); let body = self.lower_statement(&stmt.body); + self.semantic.leave_for_in_of_statement(is_lexical_declaration); self.hir.for_in_statement(stmt.span, left, right, body) } fn lower_for_of_statement(&mut self, stmt: &ast::ForOfStatement<'a>) -> hir::Statement<'a> { + let is_lexical_declaration = stmt.left.is_lexical_declaration(); + self.semantic.enter_for_in_of_statement(is_lexical_declaration); let left = self.lower_for_statement_left(&stmt.left); let right = self.lower_expression(&stmt.right); let body = self.lower_statement(&stmt.body); + self.semantic.leave_for_in_of_statement(is_lexical_declaration); self.hir.for_of_statement(stmt.span, stmt.r#await, left, right, body) } diff --git a/crates/oxc_minifier/examples/minifier.rs b/crates/oxc_minifier/examples/minifier.rs index 53d2aced0..005372e02 100644 --- a/crates/oxc_minifier/examples/minifier.rs +++ b/crates/oxc_minifier/examples/minifier.rs @@ -22,6 +22,7 @@ fn main() { let name = args.subcommand().ok().flatten().unwrap_or_else(|| String::from("test.js")); let mangle = args.contains("--mangle"); + let twice = args.contains("--twice"); let path = Path::new(&name); let source_text = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("{name} not found")); @@ -30,4 +31,10 @@ fn main() { let options = MinifierOptions { mangle, ..MinifierOptions::default() }; let printed = Minifier::new(&source_text, source_type, options).build(); println!("{printed}"); + + if twice { + let options = MinifierOptions { mangle, ..MinifierOptions::default() }; + let printed = Minifier::new(&printed, source_type, options).build(); + println!("{printed}"); + } } diff --git a/crates/oxc_minifier/src/compressor/mod.rs b/crates/oxc_minifier/src/compressor/mod.rs index 1bf5f58af..16b5ce71d 100644 --- a/crates/oxc_minifier/src/compressor/mod.rs +++ b/crates/oxc_minifier/src/compressor/mod.rs @@ -79,6 +79,20 @@ impl<'a> Compressor<'a> { /* Statements */ + /// Remove block from single line blocks + /// `{ block } -> block` + #[allow(clippy::only_used_in_recursion)] // `&self` is only used in recursion + fn compress_block<'b>(&self, stmt: &'b mut Statement<'a>) { + if let Statement::BlockStatement(block) = stmt { + // Avoid compressing `if (x) { var x = 1 }` to `if (x) var x = 1` due to different + // semantics according to AnnexB, which lead to different semantics. + if block.body.len() == 1 && !matches!(&block.body[0], Statement::Declaration(_)) { + *stmt = block.body.remove(0); + self.compress_block(stmt); + } + } + } + /// Drop `drop_debugger` statement. /// Enabled by `compress.drop_debugger` fn drop_debugger<'b>(&mut self, stmt: &'b Statement<'a>) -> bool { @@ -248,6 +262,7 @@ impl<'a, 'b> VisitMut<'a, 'b> for Compressor<'a> { } fn visit_statement(&mut self, stmt: &'b mut Statement<'a>) { + self.compress_block(stmt); self.compress_while(stmt); self.visit_statement_match(stmt); } diff --git a/crates/oxc_minifier/src/printer/gen.rs b/crates/oxc_minifier/src/printer/gen.rs index 89f40826f..0c9d95a15 100644 --- a/crates/oxc_minifier/src/printer/gen.rs +++ b/crates/oxc_minifier/src/printer/gen.rs @@ -102,7 +102,25 @@ fn print_if(if_stmt: &IfStatement<'_>, p: &mut Printer) { p.print(b'('); if_stmt.test.gen(p); p.print(b')'); - if_stmt.consequent.gen(p); + + match &if_stmt.consequent { + Some(Statement::BlockStatement(block)) => { + p.print_block1(block); + } + Some(stmt) if wrap_to_avoid_ambiguous_else(stmt) => { + p.print(b'{'); + stmt.gen(p); + p.print(b'}'); + p.needs_semicolon = false; + } + Some(stmt) => { + stmt.gen(p); + } + None => { + p.print(b';'); + } + } + if let Some(alternate) = if_stmt.alternate.as_ref() { p.print_semicolon_if_needed(); p.print(b' '); @@ -122,6 +140,36 @@ fn print_if(if_stmt: &IfStatement<'_>, p: &mut Printer) { } } +// +fn wrap_to_avoid_ambiguous_else(stmt: &Statement) -> bool { + let mut current = stmt; + loop { + current = match current { + Statement::IfStatement(Box(IfStatement { alternate, .. })) => { + if let Some(stmt) = &alternate { + stmt + } else { + return true; + } + } + Statement::ForStatement(Box(ForStatement { body, .. })) + | Statement::ForOfStatement(Box(ForOfStatement { body, .. })) + | Statement::ForInStatement(Box(ForInStatement { body, .. })) + | Statement::WhileStatement(Box(WhileStatement { body, .. })) + | Statement::WithStatement(Box(WithStatement { body, .. })) + | Statement::LabeledStatement(Box(LabeledStatement { body, .. })) => { + if let Some(stmt) = &body { + stmt + } else { + return false; + } + } + _ => return false, + } + } + false +} + impl<'a> Gen for BlockStatement<'a> { fn gen(&self, p: &mut Printer) { p.print_block1(self); diff --git a/crates/oxc_minifier/src/printer/mod.rs b/crates/oxc_minifier/src/printer/mod.rs index 0397cfb3b..20741876e 100644 --- a/crates/oxc_minifier/src/printer/mod.rs +++ b/crates/oxc_minifier/src/printer/mod.rs @@ -33,7 +33,10 @@ pub struct Printer { // states prev_op_end: usize, + + /// For avoiding `;` if the previous statement ends with `}`. needs_semicolon: bool, + prev_op: Option, } diff --git a/crates/oxc_minifier/tests/closure/substitute_alternate_syntax.rs b/crates/oxc_minifier/tests/closure/substitute_alternate_syntax.rs index 49db148b9..89094f3d3 100644 --- a/crates/oxc_minifier/tests/closure/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/tests/closure/substitute_alternate_syntax.rs @@ -9,7 +9,7 @@ fn fold_return_result() { expect("function f(){return void 0;}", "function f(){return}"); expect("function f(){return void foo();}", "function f(){return void foo()}"); expect("function f(){return undefined;}", "function f(){return}"); - expect("function f(){if(a()){return undefined;}}", "function f(){if(a()){return}}"); + expect("function f(){if(a()){return undefined;}}", "function f(){if(a())return}"); } #[test] diff --git a/crates/oxc_minifier/tests/terser/mod.rs b/crates/oxc_minifier/tests/terser/mod.rs index 6be8e732c..229641e47 100644 --- a/crates/oxc_minifier/tests/terser/mod.rs +++ b/crates/oxc_minifier/tests/terser/mod.rs @@ -3,7 +3,7 @@ use oxc_allocator::Allocator; use oxc_ast::ast::*; use oxc_minifier::{CompressOptions, Minifier, MinifierOptions, PrinterOptions}; use oxc_parser::Parser; -use oxc_span::SourceType; +use oxc_span::{SourceType, Span}; use walkdir::WalkDir; #[test] @@ -103,7 +103,8 @@ impl TestCase { // Parse input / expect if let Statement::LabeledStatement(labeled_stmt) = stmt && let Statement::BlockStatement(block_stmt) = &labeled_stmt.body { - let code = block_stmt.span.source_text(source_text).to_string().into_boxed_str(); + let span = block_stmt.span; + let code = Span::new(span.start + 1, span.end - 1).source_text(source_text).to_string().into_boxed_str(); match labeled_stmt.label.name.as_str() { "input" => { input = code; diff --git a/crates/oxc_semantic2/src/builder.rs b/crates/oxc_semantic2/src/builder.rs index 065dff537..2f28a2fd1 100644 --- a/crates/oxc_semantic2/src/builder.rs +++ b/crates/oxc_semantic2/src/builder.rs @@ -205,4 +205,31 @@ impl SemanticBuilder { pub fn leave_catch_clause(&mut self) { self.leave_scope(); } + + // ForStatement : for ( LexicalDeclaration Expressionopt ; Expressionopt ) Statement + // 1. Let oldEnv be the running execution context's LexicalEnvironment. + // 2. Let loopEnv be NewDeclarativeEnvironment(oldEnv). + pub fn enter_for_statement(&mut self, is_lexical_declaration: bool) { + if is_lexical_declaration { + self.enter_scope(ScopeFlags::empty()); + } + } + + pub fn leave_for_statement(&mut self, is_lexical_declaration: bool) { + if is_lexical_declaration { + self.leave_scope(); + } + } + + pub fn enter_for_in_of_statement(&mut self, is_lexical_declaration: bool) { + if is_lexical_declaration { + self.enter_scope(ScopeFlags::empty()); + } + } + + pub fn leave_for_in_of_statement(&mut self, is_lexical_declaration: bool) { + if is_lexical_declaration { + self.leave_scope(); + } + } } diff --git a/crates/oxc_semantic2/src/mangler.rs b/crates/oxc_semantic2/src/mangler.rs index 2bf3d19af..da4e83e49 100644 --- a/crates/oxc_semantic2/src/mangler.rs +++ b/crates/oxc_semantic2/src/mangler.rs @@ -71,11 +71,6 @@ impl Mangler { // Walk the scope tree and compute the slot number for each scope for scope_id in scope_tree.descendants() { let bindings = scope_tree.get_bindings(scope_id); - // Skip if the scope is empty - if bindings.is_empty() { - continue; - } - // The current slot number is continued by the maximum slot from the parent scope let parent_max_slot = scope_tree .get_parent_id(scope_id) diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 3eb37d1fd..3ad8fde4f 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,25 +1,25 @@ Original -> Minified -> Gzip Brotli -72.14 kB -> 24.58 kB -> 8.70 kB 7.66 kB react.development.js +72.14 kB -> 24.36 kB -> 8.67 kB 7.63 kB react.development.js -173.90 kB -> 62.53 kB -> 19.65 kB 17.82 kB moment.js +173.90 kB -> 62.04 kB -> 19.54 kB 17.74 kB moment.js -287.63 kB -> 94.34 kB -> 32.65 kB 29.59 kB jquery.js +287.63 kB -> 93.32 kB -> 32.35 kB 29.39 kB jquery.js -342.15 kB -> 125.39 kB -> 45.62 kB 40.34 kB vue.js +342.15 kB -> 123.97 kB -> 45.28 kB 40.17 kB vue.js -544.10 kB -> 75.44 kB -> 26.34 kB 23.51 kB lodash.js +544.10 kB -> 74.91 kB -> 26.25 kB 23.54 kB lodash.js -555.77 kB -> 276.19 kB -> 91.67 kB 77.58 kB d3.js +555.77 kB -> 275.56 kB -> 91.51 kB 77.42 kB d3.js -977.19 kB -> 458.34 kB -> 124.55 kB 107.80 kB bundle.min.js +977.19 kB -> 456.98 kB -> 124.12 kB 107.58 kB bundle.min.js -1.25 MB -> 679.93 kB -> 167.23 kB 135.53 kB three.js +1.25 MB -> 678.14 kB -> 166.78 kB 135.11 kB three.js -2.14 MB -> 749.70 kB -> 182.45 kB 139.44 kB victory.js +2.14 MB -> 748.49 kB -> 182.38 kB 139.17 kB victory.js -3.20 MB -> 1.04 MB -> 335.34 kB 271.81 kB echarts.js +3.20 MB -> 1.04 MB -> 333.55 kB 270.51 kB echarts.js -6.69 MB -> 2.42 MB -> 500.04 kB 390.96 kB antd.js +6.69 MB -> 2.41 MB -> 499.10 kB 390.65 kB antd.js -10.82 MB -> 3.54 MB -> 915.71 kB 704.86 kB typescript.js +10.82 MB -> 3.53 MB -> 912.43 kB 703.18 kB typescript.js