refactor(minifier): clean up peephole_substitute_alternate_syntax.rs (#8111)

This commit is contained in:
Boshen 2024-12-25 13:54:51 +00:00
parent 5a1311e76c
commit e594c3988d
5 changed files with 141 additions and 145 deletions

2
Cargo.lock generated
View file

@ -1793,8 +1793,10 @@ dependencies = [
"oxc_codegen", "oxc_codegen",
"oxc_minifier", "oxc_minifier",
"oxc_parser", "oxc_parser",
"oxc_semantic",
"oxc_span", "oxc_span",
"oxc_tasks_common", "oxc_tasks_common",
"oxc_transformer",
"rustc-hash", "rustc-hash",
] ]

View file

@ -79,76 +79,33 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let ctx = Ctx(ctx); let ctx = Ctx(ctx);
// Change syntax
match expr { match expr {
Expression::AssignmentExpression(assignment_expr) => { Expression::ArrowFunctionExpression(arrow_expr) => {
if let Some(new_expr) = self.try_compress_arrow_expression(arrow_expr, ctx);
Self::try_compress_assignment_expression(assignment_expr, ctx)
{
*expr = new_expr;
self.changed = true;
}
}
Expression::LogicalExpression(logical_expr) => {
if let Some(new_expr) = Self::try_compress_is_null_or_undefined(logical_expr, ctx) {
*expr = new_expr;
self.changed = true;
}
} }
Expression::ChainExpression(e) => self.try_compress_chain_call_expression(e, ctx),
_ => {} _ => {}
} }
self.try_compress_boolean(expr, ctx);
self.try_compress_undefined(expr, ctx);
match expr {
Expression::NewExpression(new_expr) => {
if let Some(new_expr) = Self::try_fold_new_expression(new_expr, ctx) {
*expr = new_expr;
self.changed = true;
}
}
Expression::CallExpression(call_expr) => {
if let Some(new_expr) =
Self::try_fold_literal_constructor_call_expression(call_expr, ctx)
.or_else(|| Self::try_fold_simple_function_call(call_expr, ctx))
{
*expr = new_expr;
self.changed = true;
}
}
Expression::ChainExpression(chain_expr) => {
if let ChainElement::CallExpression(call_expr) = &mut chain_expr.expression {
self.try_fold_chain_call_expression(call_expr, ctx);
}
}
Expression::TemplateLiteral(_) => {
if let Some(val) = expr.to_js_string() {
*expr = ctx.ast.expression_string_literal(expr.span(), val, None);
self.changed = true;
}
}
// `() => { return foo })` -> `() => foo`
Expression::ArrowFunctionExpression(arrow_expr) => {
if !arrow_expr.expression
&& arrow_expr.body.directives.is_empty()
&& arrow_expr.body.statements.len() == 1
{
if let Some(body) = arrow_expr.body.statements.first_mut() {
if let Statement::ReturnStatement(ret_stmt) = body {
let return_stmt_arg =
ret_stmt.argument.as_mut().map(|arg| ctx.ast.move_expression(arg));
if let Some(return_stmt_arg) = return_stmt_arg { // Fold
*body = ctx.ast.statement_expression(SPAN, return_stmt_arg); if let Some(folded_expr) = match expr {
arrow_expr.expression = true; Expression::Identifier(ident) => self.try_compress_undefined(ident, ctx),
self.changed = true; Expression::BooleanLiteral(_) => self.try_compress_boolean(expr, ctx),
} Expression::AssignmentExpression(e) => Self::try_compress_assignment_expression(e, ctx),
} Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx),
} Expression::NewExpression(e) => Self::try_fold_new_expression(e, ctx),
} Expression::CallExpression(e) => {
Self::try_fold_literal_constructor_call_expression(e, ctx)
.or_else(|| Self::try_fold_simple_function_call(e, ctx))
} }
Expression::BinaryExpression(expr) => { Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx),
self.compress_typeof_undefined(expr, ctx); Expression::BinaryExpression(e) => Self::try_compress_typeof_undefined(e, ctx),
} _ => None,
_ => {} } {
*expr = folded_expr;
self.changed = true;
} }
} }
} }
@ -161,14 +118,18 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
/* Utilities */ /* Utilities */
/// Transforms `undefined` => `void 0` /// Transforms `undefined` => `void 0`
fn try_compress_undefined(&mut self, expr: &mut Expression<'a>, ctx: Ctx<'a, 'b>) { fn try_compress_undefined(
&self,
ident: &IdentifierReference<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
if self.in_fixed_loop { if self.in_fixed_loop {
return; return None;
} }
if ctx.is_expression_undefined(expr) { if !ctx.is_identifier_undefined(ident) {
*expr = ctx.ast.void_0(expr.span()); return None;
self.changed = true;
} }
Some(ctx.ast.void_0(ident.span))
} }
/// Test `Object.defineProperty(exports, ...)` /// Test `Object.defineProperty(exports, ...)`
@ -207,48 +168,80 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
/// Transforms boolean expression `true` => `!0` `false` => `!1`. /// Transforms boolean expression `true` => `!0` `false` => `!1`.
/// Do not compress `true` in `Object.defineProperty(exports, 'Foo', {enumerable: true, ...})`. /// Do not compress `true` in `Object.defineProperty(exports, 'Foo', {enumerable: true, ...})`.
fn try_compress_boolean(&mut self, expr: &mut Expression<'a>, ctx: Ctx<'a, 'b>) { fn try_compress_boolean(
&self,
expr: &mut Expression<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
if self.in_fixed_loop { if self.in_fixed_loop {
return; return None;
} }
let Expression::BooleanLiteral(lit) = expr else { return }; let Expression::BooleanLiteral(lit) = expr else { return None };
if !self.in_define_export { if self.in_define_export {
let parent = ctx.ancestry.parent(); return None;
let no_unary = { }
if let Ancestor::BinaryExpressionRight(u) = parent { let parent = ctx.ancestry.parent();
!matches!( let no_unary = {
u.operator(), if let Ancestor::BinaryExpressionRight(u) = parent {
BinaryOperator::Addition // Other effect, like string concatenation. !matches!(
u.operator(),
BinaryOperator::Addition // Other effect, like string concatenation.
| BinaryOperator::Instanceof // Relational operator. | BinaryOperator::Instanceof // Relational operator.
| BinaryOperator::In | BinaryOperator::In
| BinaryOperator::StrictEquality // It checks type, so we should not fold. | BinaryOperator::StrictEquality // It checks type, so we should not fold.
| BinaryOperator::StrictInequality | BinaryOperator::StrictInequality
) )
} else {
false
}
};
// XOR: We should use `!neg` when it is not in binary expression.
let num = ctx.ast.expression_numeric_literal(
SPAN,
if lit.value ^ no_unary { 0.0 } else { 1.0 },
None,
NumberBase::Decimal,
);
*expr = if no_unary {
num
} else { } else {
ctx.ast.expression_unary(SPAN, UnaryOperator::LogicalNot, num) false
}; }
self.changed = true; };
// XOR: We should use `!neg` when it is not in binary expression.
let num = ctx.ast.expression_numeric_literal(
SPAN,
if lit.value ^ no_unary { 0.0 } else { 1.0 },
None,
NumberBase::Decimal,
);
Some(if no_unary {
num
} else {
ctx.ast.expression_unary(SPAN, UnaryOperator::LogicalNot, num)
})
}
/// `() => { return foo })` -> `() => foo`
fn try_compress_arrow_expression(
&mut self,
arrow_expr: &mut ArrowFunctionExpression<'a>,
ctx: Ctx<'a, 'b>,
) {
if !arrow_expr.expression
&& arrow_expr.body.directives.is_empty()
&& arrow_expr.body.statements.len() == 1
{
if let Some(body) = arrow_expr.body.statements.first_mut() {
if let Statement::ReturnStatement(ret_stmt) = body {
let return_stmt_arg =
ret_stmt.argument.as_mut().map(|arg| ctx.ast.move_expression(arg));
if let Some(return_stmt_arg) = return_stmt_arg {
*body = ctx.ast.statement_expression(SPAN, return_stmt_arg);
arrow_expr.expression = true;
self.changed = true;
}
}
}
} }
} }
/// Compress `typeof foo == "undefined"` into `typeof foo > "u"` /// Compress `typeof foo == "undefined"` into `typeof foo > "u"`
/// Enabled by `compress.typeofs` /// Enabled by `compress.typeofs`
fn compress_typeof_undefined(&mut self, expr: &mut BinaryExpression<'a>, ctx: Ctx<'a, 'b>) { fn try_compress_typeof_undefined(
expr: &mut BinaryExpression<'a>,
ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> {
if !matches!(expr.operator, BinaryOperator::Equality | BinaryOperator::StrictEquality) { if !matches!(expr.operator, BinaryOperator::Equality | BinaryOperator::StrictEquality) {
return; return None;
} }
let pair = Self::commutative_pair( let pair = Self::commutative_pair(
(&expr.left, &expr.right), (&expr.left, &expr.right),
@ -264,16 +257,11 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
None None
}, },
); );
let Some((_void_exp, id_ref)) = pair else { let (_void_exp, id_ref) = pair?;
return;
};
let argument = Expression::Identifier(ctx.alloc(id_ref)); let argument = Expression::Identifier(ctx.alloc(id_ref));
let left = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, argument); let left = ctx.ast.expression_unary(SPAN, UnaryOperator::Typeof, argument);
let right = ctx.ast.expression_string_literal(SPAN, "u", None); let right = ctx.ast.expression_string_literal(SPAN, "u", None);
let binary_expr = Some(ctx.ast.expression_binary(expr.span, left, BinaryOperator::GreaterThan, right))
ctx.ast.binary_expression(expr.span, left, BinaryOperator::GreaterThan, right);
*expr = binary_expr;
self.changed = true;
} }
/// Compress `foo === null || foo === undefined` into `foo == null`. /// Compress `foo === null || foo === undefined` into `foo == null`.
@ -290,16 +278,11 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
ctx: Ctx<'a, 'b>, ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> { ) -> Option<Expression<'a>> {
let op = expr.operator; let op = expr.operator;
if !matches!(op, LogicalOperator::Or | LogicalOperator::And) {
return None;
}
#[allow(clippy::match_wildcard_for_single_variants)]
let target_ops = match op { let target_ops = match op {
LogicalOperator::Or => (BinaryOperator::StrictEquality, BinaryOperator::Equality), LogicalOperator::Or => (BinaryOperator::StrictEquality, BinaryOperator::Equality),
LogicalOperator::And => (BinaryOperator::StrictInequality, BinaryOperator::Inequality), LogicalOperator::And => (BinaryOperator::StrictInequality, BinaryOperator::Inequality),
_ => unreachable!(), LogicalOperator::Coalesce => return None,
}; };
if let Some(new_expr) = Self::try_compress_is_null_or_undefined_for_left_and_right( if let Some(new_expr) = Self::try_compress_is_null_or_undefined_for_left_and_right(
&expr.left, &expr.left,
&expr.right, &expr.right,
@ -309,14 +292,12 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
) { ) {
return Some(new_expr); return Some(new_expr);
} }
let Expression::LogicalExpression(left) = &mut expr.left else { let Expression::LogicalExpression(left) = &mut expr.left else {
return None; return None;
}; };
if left.operator != op { if left.operator != op {
return None; return None;
} }
Self::try_compress_is_null_or_undefined_for_left_and_right( Self::try_compress_is_null_or_undefined_for_left_and_right(
&left.right, &left.right,
&expr.right, &expr.right,
@ -445,36 +426,32 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
ctx: Ctx<'a, 'b>, ctx: Ctx<'a, 'b>,
) -> Option<Expression<'a>> { ) -> Option<Expression<'a>> {
let target = expr.left.as_simple_assignment_target_mut()?; let target = expr.left.as_simple_assignment_target_mut()?;
if matches!(expr.operator, AssignmentOperator::Subtraction) { if !matches!(expr.operator, AssignmentOperator::Subtraction) {
match &expr.right { return None;
Expression::NumericLiteral(num) if num.value.to_int_32() == 1 => { }
match &expr.right {
Expression::NumericLiteral(num) if num.value.to_int_32() == 1 => {
// The `_` will not be placed to the target code.
let target = std::mem::replace(
target,
ctx.ast.simple_assignment_target_identifier_reference(SPAN, "_"),
);
Some(ctx.ast.expression_update(SPAN, UpdateOperator::Decrement, true, target))
}
Expression::UnaryExpression(un)
if matches!(un.operator, UnaryOperator::UnaryNegation) =>
{
let Expression::NumericLiteral(num) = &un.argument else { return None };
(num.value.to_int_32() == 1).then(|| {
// The `_` will not be placed to the target code. // The `_` will not be placed to the target code.
let target = std::mem::replace( let target = std::mem::replace(
target, target,
ctx.ast.simple_assignment_target_identifier_reference(SPAN, "_"), ctx.ast.simple_assignment_target_identifier_reference(SPAN, "_"),
); );
Some(ctx.ast.expression_update(SPAN, UpdateOperator::Decrement, true, target)) ctx.ast.expression_update(SPAN, UpdateOperator::Increment, true, target)
} })
Expression::UnaryExpression(un)
if matches!(un.operator, UnaryOperator::UnaryNegation) =>
{
if let Expression::NumericLiteral(num) = &un.argument {
(num.value.to_int_32() == 1).then(|| {
// The `_` will not be placed to the target code.
let target = std::mem::replace(
target,
ctx.ast.simple_assignment_target_identifier_reference(SPAN, "_"),
);
ctx.ast.expression_update(SPAN, UpdateOperator::Increment, true, target)
})
} else {
None
}
}
_ => None,
} }
} else { _ => None,
None
} }
} }
@ -645,19 +622,25 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax {
} }
} }
fn try_fold_chain_call_expression( fn try_compress_chain_call_expression(
&mut self, &mut self,
call_expr: &mut CallExpression<'a>, chain_expr: &mut ChainExpression<'a>,
ctx: Ctx<'a, 'b>, ctx: Ctx<'a, 'b>,
) { ) {
// `window.Object?.()` -> `Object?.()` if let ChainElement::CallExpression(call_expr) = &mut chain_expr.expression {
if call_expr.arguments.is_empty() && Self::is_window_object(&call_expr.callee) { // `window.Object?.()` -> `Object?.()`
call_expr.callee = if call_expr.arguments.is_empty() && Self::is_window_object(&call_expr.callee) {
ctx.ast.expression_identifier_reference(call_expr.callee.span(), "Object"); call_expr.callee =
self.changed = true; ctx.ast.expression_identifier_reference(call_expr.callee.span(), "Object");
self.changed = true;
}
} }
} }
fn try_fold_template_literal(t: &TemplateLiteral, ctx: Ctx<'a, 'b>) -> Option<Expression<'a>> {
t.to_js_string().map(|val| ctx.ast.expression_string_literal(t.span(), val, None))
}
/// returns an `Array()` constructor call with zero, one, or more arguments, copying from the input /// returns an `Array()` constructor call with zero, one, or more arguments, copying from the input
fn array_constructor_call( fn array_constructor_call(
arguments: Vec<'a, Argument<'a>>, arguments: Vec<'a, Argument<'a>>,

View file

@ -22,7 +22,9 @@ oxc_allocator = { workspace = true }
oxc_codegen = { workspace = true } oxc_codegen = { workspace = true }
oxc_minifier = { workspace = true } oxc_minifier = { workspace = true }
oxc_parser = { workspace = true } oxc_parser = { workspace = true }
oxc_semantic = { workspace = true }
oxc_span = { workspace = true } oxc_span = { workspace = true }
oxc_transformer = { workspace = true }
flate2 = { workspace = true } flate2 = { workspace = true }
oxc_tasks_common = { workspace = true } oxc_tasks_common = { workspace = true }

View file

@ -1,7 +1,7 @@
| Oxc | ESBuild | Oxc | ESBuild | | Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Fixture Original | minified | minified | gzip | gzip | Fixture
------------------------------------------------------------------------------------- -------------------------------------------------------------------------------------
72.14 kB | 24.04 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js 72.14 kB | 24.00 kB | 23.70 kB | 8.58 kB | 8.54 kB | react.development.js
173.90 kB | 61.55 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js 173.90 kB | 61.55 kB | 59.82 kB | 19.54 kB | 19.33 kB | moment.js

View file

@ -11,8 +11,10 @@ use oxc_allocator::Allocator;
use oxc_codegen::{CodeGenerator, CodegenOptions}; use oxc_codegen::{CodeGenerator, CodegenOptions};
use oxc_minifier::{CompressOptions, MangleOptions, Minifier, MinifierOptions}; use oxc_minifier::{CompressOptions, MangleOptions, Minifier, MinifierOptions};
use oxc_parser::Parser; use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType; use oxc_span::SourceType;
use oxc_tasks_common::{project_root, TestFile, TestFiles}; use oxc_tasks_common::{project_root, TestFile, TestFiles};
use oxc_transformer::{ReplaceGlobalDefines, ReplaceGlobalDefinesConfig};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
// #[test] // #[test]
@ -137,6 +139,13 @@ fn minify(source_text: &str, source_type: SourceType, options: MinifierOptions)
let allocator = Allocator::default(); let allocator = Allocator::default();
let ret = Parser::new(&allocator, source_text, source_type).parse(); let ret = Parser::new(&allocator, source_text, source_type).parse();
let mut program = ret.program; let mut program = ret.program;
let (symbols, scopes) =
SemanticBuilder::new().build(&program).semantic.into_symbol_table_and_scope_tree();
let _ = ReplaceGlobalDefines::new(
&allocator,
ReplaceGlobalDefinesConfig::new(&[("process.env.NODE_ENV", "'development'")]).unwrap(),
)
.build(symbols, scopes, &mut program);
let ret = Minifier::new(options).build(&allocator, &mut program); let ret = Minifier::new(options).build(&allocator, &mut program);
CodeGenerator::new() CodeGenerator::new()
.with_options(CodegenOptions { minify: true, ..CodegenOptions::default() }) .with_options(CodegenOptions { minify: true, ..CodegenOptions::default() })