feat(minifier): remove redundant curly braces from block statements (#390)

This commit is contained in:
Boshen 2023-05-27 10:52:15 +08:00 committed by GitHub
parent db51772c8c
commit 8ea9e38ee5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 143 additions and 21 deletions

View file

@ -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"))]

View file

@ -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)
}

View file

@ -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}");
}
}

View file

@ -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);
}

View file

@ -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) {
}
}
// <https://github.com/evanw/esbuild/blob/e6a8169c3a574f4c67d4cdd5f31a938b53eb7421/internal/js_printer/js_printer.go#L3444>
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);

View file

@ -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<Operator>,
}

View file

@ -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]

View file

@ -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;

View file

@ -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();
}
}
}

View file

@ -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)

View file

@ -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