mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(minifier): port esbuild conditional expr minification (#8351)
still some TODOs, but the main framework is there
This commit is contained in:
parent
09f0f483f6
commit
f367a16364
2 changed files with 279 additions and 48 deletions
|
|
@ -1,5 +1,5 @@
|
|||
use oxc_allocator::Vec;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_ast::{ast::*, NONE};
|
||||
use oxc_ecmascript::constant_evaluation::ValueType;
|
||||
use oxc_span::{cmp::ContentEq, GetSpan, SPAN};
|
||||
use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
|
||||
|
|
@ -342,41 +342,31 @@ impl<'a> PeepholeMinimizeConditions {
|
|||
None
|
||||
}
|
||||
|
||||
// based on https://github.com/evanw/esbuild/blob/df815ac27b84f8b34374c9182a93c94718f8a630/internal/js_ast/js_ast_helpers.go#L2745
|
||||
fn try_minimize_conditional(
|
||||
expr: &mut ConditionalExpression<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> Option<Expression<'a>> {
|
||||
// `a ? a : b` -> `a || b`
|
||||
if let (Expression::Identifier(test_ident), Expression::Identifier(consequent_ident)) =
|
||||
(&expr.test, &expr.consequent)
|
||||
{
|
||||
if test_ident.name == consequent_ident.name {
|
||||
let ident = ctx.ast.move_expression(&mut expr.test);
|
||||
|
||||
return Some(ctx.ast.expression_logical(
|
||||
expr.span,
|
||||
ident,
|
||||
LogicalOperator::Or,
|
||||
// `(a, b) ? c : d` -> `a, b ? c : d`
|
||||
if let Expression::SequenceExpression(sequence_expr) = &expr.test {
|
||||
if sequence_expr.expressions.len() > 1 {
|
||||
let mut sequence = ctx.ast.move_expression(&mut expr.test);
|
||||
let Expression::SequenceExpression(ref mut sequence_expr) = &mut sequence else {
|
||||
unreachable!()
|
||||
};
|
||||
let test = sequence_expr.expressions.pop().expect("sequence_expr.expressions");
|
||||
expr.test = test;
|
||||
sequence_expr.expressions.push(ctx.ast.expression_conditional(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
ctx.ast.move_expression(&mut expr.alternate),
|
||||
));
|
||||
return Some(sequence);
|
||||
}
|
||||
}
|
||||
|
||||
// `foo ? bar : foo` -> `foo && bar`
|
||||
if let (Expression::Identifier(test_ident), Expression::Identifier(alternate_ident)) =
|
||||
(&expr.test, &expr.alternate)
|
||||
{
|
||||
if test_ident.name == alternate_ident.name {
|
||||
return Some(ctx.ast.expression_logical(
|
||||
expr.span,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::And,
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `!a ? b() : c()` -> `a ? c() : b()`
|
||||
// `!a ? b : c` -> `a ? c : b`
|
||||
if let Expression::UnaryExpression(test_expr) = &mut expr.test {
|
||||
if test_expr.operator.is_not()
|
||||
// Skip `!!!a`
|
||||
|
|
@ -391,8 +381,19 @@ impl<'a> PeepholeMinimizeConditions {
|
|||
}
|
||||
}
|
||||
|
||||
// `a ? false : true` -> `!a`
|
||||
// TODO: `/* @__PURE__ */ a() ? b : b` -> `b`
|
||||
|
||||
// `a ? b : b` -> `a, b`
|
||||
if expr.alternate.content_eq(&expr.consequent) {
|
||||
let expressions = ctx.ast.vec_from_array([
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
]);
|
||||
return Some(ctx.ast.expression_sequence(expr.span, expressions));
|
||||
}
|
||||
|
||||
// `a ? true : false` -> `!!a`
|
||||
// `a ? false : true` -> `!a`
|
||||
if let (
|
||||
Expression::Identifier(_),
|
||||
Expression::BooleanLiteral(consequent_lit),
|
||||
|
|
@ -420,6 +421,222 @@ impl<'a> PeepholeMinimizeConditions {
|
|||
}
|
||||
}
|
||||
|
||||
// `a ? a : b` -> `a || b`
|
||||
if let (Expression::Identifier(test_ident), Expression::Identifier(consequent_ident)) =
|
||||
(&expr.test, &expr.consequent)
|
||||
{
|
||||
if test_ident.name == consequent_ident.name {
|
||||
let ident = ctx.ast.move_expression(&mut expr.test);
|
||||
|
||||
return Some(ctx.ast.expression_logical(
|
||||
expr.span,
|
||||
ident,
|
||||
LogicalOperator::Or,
|
||||
ctx.ast.move_expression(&mut expr.alternate),
|
||||
));
|
||||
}
|
||||
}
|
||||
// `a ? b : a` -> `a && b`
|
||||
if let (Expression::Identifier(test_ident), Expression::Identifier(alternate_ident)) =
|
||||
(&expr.test, &expr.alternate)
|
||||
{
|
||||
if test_ident.name == alternate_ident.name {
|
||||
return Some(ctx.ast.expression_logical(
|
||||
expr.span,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::And,
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? b ? c : d : d` -> `a && b ? c : d`
|
||||
if let Expression::ConditionalExpression(consequent) = &mut expr.consequent {
|
||||
if consequent.alternate.content_eq(&expr.alternate) {
|
||||
return Some(ctx.ast.expression_conditional(
|
||||
SPAN,
|
||||
ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::And,
|
||||
ctx.ast.move_expression(&mut consequent.test),
|
||||
),
|
||||
ctx.ast.move_expression(&mut consequent.consequent),
|
||||
ctx.ast.move_expression(&mut consequent.alternate),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? b : c ? b : d` -> `a || c ? b : d`
|
||||
if let Expression::ConditionalExpression(alternate) = &mut expr.alternate {
|
||||
if alternate.consequent.content_eq(&expr.consequent) {
|
||||
return Some(ctx.ast.expression_conditional(
|
||||
SPAN,
|
||||
ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::Or,
|
||||
ctx.ast.move_expression(&mut alternate.test),
|
||||
),
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
ctx.ast.move_expression(&mut alternate.alternate),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? c : (b, c)` -> `(a || b), c`
|
||||
if let Expression::SequenceExpression(alternate) = &mut expr.alternate {
|
||||
if alternate.expressions.len() == 2
|
||||
&& alternate.expressions[1].content_eq(&expr.consequent)
|
||||
{
|
||||
return Some(ctx.ast.expression_sequence(
|
||||
SPAN,
|
||||
ctx.ast.vec_from_array([
|
||||
ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::Or,
|
||||
ctx.ast.move_expression(&mut alternate.expressions[0]),
|
||||
),
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? (b, c) : c` -> `(a && b), c`
|
||||
if let Expression::SequenceExpression(consequent) = &mut expr.consequent {
|
||||
if consequent.expressions.len() == 2
|
||||
&& consequent.expressions[1].content_eq(&expr.alternate)
|
||||
{
|
||||
return Some(ctx.ast.expression_sequence(
|
||||
SPAN,
|
||||
ctx.ast.vec_from_array([
|
||||
ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::And,
|
||||
ctx.ast.move_expression(&mut consequent.expressions[0]),
|
||||
),
|
||||
ctx.ast.move_expression(&mut expr.alternate),
|
||||
]),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? b || c : c` => "(a && b) || c"
|
||||
if let Expression::LogicalExpression(logical_expr) = &mut expr.consequent {
|
||||
if logical_expr.operator == LogicalOperator::Or
|
||||
&& logical_expr.right.content_eq(&expr.alternate)
|
||||
{
|
||||
return Some(ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::And,
|
||||
ctx.ast.move_expression(&mut logical_expr.left),
|
||||
),
|
||||
LogicalOperator::Or,
|
||||
ctx.ast.move_expression(&mut expr.alternate),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? c : b && c` -> `(a || b) && c``
|
||||
if let Expression::LogicalExpression(logical_expr) = &mut expr.alternate {
|
||||
if logical_expr.operator == LogicalOperator::And
|
||||
&& logical_expr.right.content_eq(&expr.consequent)
|
||||
{
|
||||
return Some(ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.expression_logical(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
LogicalOperator::Or,
|
||||
ctx.ast.move_expression(&mut logical_expr.left),
|
||||
),
|
||||
LogicalOperator::And,
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// `a ? b(c, d) : b(e, d)` -> `b(a ? c : e, d)``
|
||||
if let (
|
||||
Expression::Identifier(test),
|
||||
Expression::CallExpression(consequent),
|
||||
Expression::CallExpression(alternate),
|
||||
) = (&expr.test, &mut expr.consequent, &mut expr.alternate)
|
||||
{
|
||||
if consequent.callee.content_eq(&alternate.callee)
|
||||
&& consequent.arguments.len() == alternate.arguments.len()
|
||||
&& ctx.scopes().find_binding(ctx.current_scope_id(), &test.name).is_some()
|
||||
&& consequent
|
||||
.arguments
|
||||
.iter()
|
||||
.zip(&alternate.arguments)
|
||||
.skip(1)
|
||||
.all(|(a, b)| a.content_eq(b))
|
||||
{
|
||||
// `a ? b(...c) : b(...e)` -> `b(...a ? c : e)``
|
||||
if matches!(consequent.arguments[0], Argument::SpreadElement(_))
|
||||
&& matches!(alternate.arguments[0], Argument::SpreadElement(_))
|
||||
{
|
||||
let callee = ctx.ast.move_expression(&mut consequent.callee);
|
||||
let consequent_first_arg = {
|
||||
let Argument::SpreadElement(ref mut el) = &mut consequent.arguments[0]
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
ctx.ast.move_expression(&mut el.argument)
|
||||
};
|
||||
let alternate_first_arg = {
|
||||
let Argument::SpreadElement(ref mut el) = &mut alternate.arguments[0]
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
ctx.ast.move_expression(&mut el.argument)
|
||||
};
|
||||
let mut args = std::mem::replace(&mut consequent.arguments, ctx.ast.vec());
|
||||
args[0] = ctx.ast.argument_spread_element(
|
||||
SPAN,
|
||||
ctx.ast.expression_conditional(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
consequent_first_arg,
|
||||
alternate_first_arg,
|
||||
),
|
||||
);
|
||||
|
||||
return Some(ctx.ast.expression_call(expr.span, callee, NONE, args, false));
|
||||
}
|
||||
// `a ? b(c) : b(e)` -> `b(a ? c : e)``
|
||||
if !matches!(consequent.arguments[0], Argument::SpreadElement(_))
|
||||
&& !matches!(alternate.arguments[0], Argument::SpreadElement(_))
|
||||
{
|
||||
let callee = ctx.ast.move_expression(&mut consequent.callee);
|
||||
|
||||
let consequent_first_arg =
|
||||
ctx.ast.move_expression(consequent.arguments[0].to_expression_mut());
|
||||
let alternate_first_arg =
|
||||
ctx.ast.move_expression(alternate.arguments[0].to_expression_mut());
|
||||
let mut args = std::mem::replace(&mut consequent.arguments, ctx.ast.vec());
|
||||
args[0] = Argument::from(ctx.ast.expression_conditional(
|
||||
SPAN,
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
consequent_first_arg,
|
||||
alternate_first_arg,
|
||||
));
|
||||
return Some(ctx.ast.expression_call(expr.span, callee, NONE, args, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Try using the "??" or "?." operators
|
||||
|
||||
// Non esbuild optimizations
|
||||
|
||||
// `x ? true : y` -> `x || y`
|
||||
// `x ? false : y` -> `!x && y`
|
||||
if let (Expression::Identifier(_), Expression::BooleanLiteral(consequent_lit), _) =
|
||||
|
|
@ -474,15 +691,6 @@ impl<'a> PeepholeMinimizeConditions {
|
|||
));
|
||||
}
|
||||
|
||||
// `foo() ? bar : bar` -> `foo(), bar`
|
||||
if expr.alternate.content_eq(&expr.consequent) {
|
||||
let expressions = ctx.ast.vec_from_array([
|
||||
ctx.ast.move_expression(&mut expr.test),
|
||||
ctx.ast.move_expression(&mut expr.consequent),
|
||||
]);
|
||||
return Some(ctx.ast.expression_sequence(expr.span, expressions));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
|
|
@ -1775,4 +1983,27 @@ mod test {
|
|||
test("!!!!delete x.y", "delete x.y");
|
||||
test("var k = !!(foo instanceof bar)", "var k = foo instanceof bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimize_conditional_exprs_esbuild() {
|
||||
test("(a, b) ? c : d", "a, b ? c : d");
|
||||
test("!a ? b : c", "a ? c : b");
|
||||
// test("/* @__PURE__ */ a() ? b : b", "b");
|
||||
test("a ? b : b", "a, b");
|
||||
test("a ? true : false", "!!a");
|
||||
test("a ? false : true", "!a");
|
||||
test("a ? a : b", "a || b");
|
||||
test("a ? b : a", "a && b");
|
||||
test("a ? b ? c : d : d", "a && b ? c : d");
|
||||
test("a ? b : c ? b : d", "a || c ? b : d");
|
||||
test("a ? c : (b, c)", "(a || b), c");
|
||||
test("a ? (b, c) : c", "(a && b), c");
|
||||
test("a ? b || c : c", "(a && b) || c");
|
||||
test("a ? c : b && c", "(a || b) && c");
|
||||
test("var a; a ? b(c, d) : b(e, d)", "var a; b(a ? c : e, d)");
|
||||
test("var a; a ? b(...c) : b(...e)", "var a; b(...a ? c : e)");
|
||||
test("var a; a ? b(c) : b(e)", "var a; b(a ? c : e)");
|
||||
// test("a != null ? a : b", "a ?? b");
|
||||
// test("a != null ? a.b.c[d](e) : undefined", "a?.b.c[d](e)");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
| Oxc | ESBuild | Oxc | ESBuild |
|
||||
Original | minified | minified | gzip | gzip | Fixture
|
||||
-------------------------------------------------------------------------------------
|
||||
72.14 kB | 23.71 kB | 23.70 kB | 8.62 kB | 8.54 kB | react.development.js
|
||||
72.14 kB | 23.70 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js
|
||||
|
||||
173.90 kB | 59.80 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js
|
||||
|
||||
287.63 kB | 90.14 kB | 90.07 kB | 32.07 kB | 31.95 kB | jquery.js
|
||||
287.63 kB | 90.13 kB | 90.07 kB | 32.05 kB | 31.95 kB | jquery.js
|
||||
|
||||
342.15 kB | 118.38 kB | 118.14 kB | 44.53 kB | 44.37 kB | vue.js
|
||||
342.15 kB | 118.36 kB | 118.14 kB | 44.52 kB | 44.37 kB | vue.js
|
||||
|
||||
544.10 kB | 71.75 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js
|
||||
544.10 kB | 71.74 kB | 72.48 kB | 26.14 kB | 26.20 kB | lodash.js
|
||||
|
||||
555.77 kB | 273.20 kB | 270.13 kB | 90.93 kB | 90.80 kB | d3.js
|
||||
555.77 kB | 273.19 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js
|
||||
|
||||
1.01 MB | 460.62 kB | 458.89 kB | 126.89 kB | 126.71 kB | bundle.min.js
|
||||
1.01 MB | 460.47 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js
|
||||
|
||||
1.25 MB | 653.02 kB | 646.76 kB | 163.55 kB | 163.73 kB | three.js
|
||||
1.25 MB | 652.88 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js
|
||||
|
||||
2.14 MB | 726.50 kB | 724.14 kB | 180.19 kB | 181.07 kB | victory.js
|
||||
2.14 MB | 726.28 kB | 724.14 kB | 180.14 kB | 181.07 kB | victory.js
|
||||
|
||||
3.20 MB | 1.01 MB | 1.01 MB | 331.98 kB | 331.56 kB | echarts.js
|
||||
3.20 MB | 1.01 MB | 1.01 MB | 331.93 kB | 331.56 kB | echarts.js
|
||||
|
||||
6.69 MB | 2.32 MB | 2.31 MB | 492.75 kB | 488.28 kB | antd.js
|
||||
6.69 MB | 2.32 MB | 2.31 MB | 492.68 kB | 488.28 kB | antd.js
|
||||
|
||||
10.95 MB | 3.50 MB | 3.49 MB | 909.08 kB | 915.50 kB | typescript.js
|
||||
10.95 MB | 3.50 MB | 3.49 MB | 908.82 kB | 915.50 kB | typescript.js
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue