feat(mininifier): minimize variants of a instanceof b == true (#8241)

This commit is contained in:
Boshen 2025-01-04 06:04:52 +00:00
parent ccdc039f54
commit ad9a0a9c4a
3 changed files with 116 additions and 59 deletions

View file

@ -1,4 +1,4 @@
use oxc_ast::ast::Expression;
use oxc_ast::ast::{BinaryExpression, Expression};
use oxc_syntax::operator::{BinaryOperator, UnaryOperator};
/// JavaScript Language Type
@ -81,27 +81,11 @@ impl<'a> From<&Expression<'a>> for ValueType {
Self::Number
}
UnaryOperator::UnaryPlus => Self::Number,
UnaryOperator::LogicalNot => Self::Boolean,
UnaryOperator::LogicalNot | UnaryOperator::Delete => Self::Boolean,
UnaryOperator::Typeof => Self::String,
_ => Self::Undetermined,
},
Expression::BinaryExpression(binary_expr) => match binary_expr.operator {
BinaryOperator::Addition => {
let left_ty = Self::from(&binary_expr.left);
let right_ty = Self::from(&binary_expr.right);
if left_ty == Self::String || right_ty == Self::String {
return Self::String;
}
// There are some pretty weird cases for object types:
// {} + [] === "0"
// [] + {} === "[object Object]"
if left_ty == Self::Object || right_ty == Self::Object {
return Self::Undetermined;
}
Self::Undetermined
}
_ => Self::Undetermined,
UnaryOperator::BitwiseNot => Self::Undetermined,
},
Expression::BinaryExpression(e) => Self::from(&**e),
Expression::SequenceExpression(e) => {
e.expressions.last().map_or(ValueType::Undetermined, Self::from)
}
@ -109,3 +93,26 @@ impl<'a> From<&Expression<'a>> for ValueType {
}
}
}
impl<'a> From<&BinaryExpression<'a>> for ValueType {
fn from(e: &BinaryExpression<'a>) -> Self {
match e.operator {
BinaryOperator::Addition => {
let left_ty = Self::from(&e.left);
let right_ty = Self::from(&e.right);
if left_ty == Self::String || right_ty == Self::String {
return Self::String;
}
// There are some pretty weird cases for object types:
// {} + [] === "0"
// [] + {} === "[object Object]"
if left_ty == Self::Object || right_ty == Self::Object {
return Self::Undetermined;
}
Self::Undetermined
}
BinaryOperator::Instanceof => Self::Boolean,
_ => Self::Undetermined,
}
}
}

View file

@ -1,5 +1,6 @@
use oxc_allocator::Vec;
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::ValueType;
use oxc_span::{GetSpan, SPAN};
use oxc_traverse::{traverse_mut_with_ctx, Ancestor, ReusableTraverseCtx, Traverse, TraverseCtx};
@ -57,12 +58,9 @@ impl<'a> Traverse<'a> for PeepholeMinimizeConditions {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(folded_expr) = match expr {
Expression::UnaryExpression(e) => Self::try_minimize_not(e, ctx),
Expression::LogicalExpression(logical_expr) => {
Self::try_minimize_logical(logical_expr, ctx)
}
Expression::ConditionalExpression(conditional_expr) => {
Self::try_minimize_conditional(conditional_expr, ctx)
}
Expression::LogicalExpression(e) => Self::try_minimize_logical(e, ctx),
Expression::BinaryExpression(e) => Self::try_minimize_binary(e, ctx),
Expression::ConditionalExpression(e) => Self::try_minimize_conditional(e, ctx),
_ => None,
} {
*expr = folded_expr;
@ -275,7 +273,7 @@ impl<'a> PeepholeMinimizeConditions {
Expression::BooleanLiteral(consequent_lit),
) = (&expr.left, &expr.right)
{
if !is_in_boolean_context(ctx) {
if !Self::is_in_boolean_context(ctx) {
return None;
}
if consequent_lit.value {
@ -357,7 +355,7 @@ impl<'a> PeepholeMinimizeConditions {
}
}
let in_boolean_context = is_in_boolean_context(ctx);
let in_boolean_context = Self::is_in_boolean_context(ctx);
// `a ? false : true` -> `!a`
// `a ? true : false` -> `!!a`
@ -456,39 +454,68 @@ impl<'a> PeepholeMinimizeConditions {
None
}
}
// returns `true` if the current node is in a context in which the return
// value type is coerced to boolean.
// For example `if (condition)` and `return condition`
// inside the `if` stmt, `condition` is coerced to a boolean
// whereas inside the return, it is not
fn is_in_boolean_context(ctx: &mut TraverseCtx<'_>) -> bool {
for ancestor in ctx.ancestors() {
match ancestor {
Ancestor::IfStatementTest(_)
| Ancestor::WhileStatementTest(_)
| Ancestor::ForStatementTest(_)
| Ancestor::DoWhileStatementTest(_)
| Ancestor::ExpressionStatementExpression(_) => return true,
Ancestor::CallExpressionArguments(_)
| Ancestor::AssignmentPatternRight(_)
| Ancestor::BindingRestElementArgument(_)
| Ancestor::JSXSpreadAttributeArgument(_)
| Ancestor::NewExpressionArguments(_)
| Ancestor::ObjectPropertyKey(_)
| Ancestor::ObjectPropertyValue(_)
| Ancestor::ReturnStatementArgument(_)
| Ancestor::ThrowStatementArgument(_)
| Ancestor::YieldExpressionArgument(_)
| Ancestor::VariableDeclaratorInit(_) => return false,
_ => continue,
// returns `true` if the current node is in a context in which the return
// value type is coerced to boolean.
// For example `if (condition)` and `return condition`
// inside the `if` stmt, `condition` is coerced to a boolean
// whereas inside the return, it is not
fn is_in_boolean_context(ctx: &mut TraverseCtx<'_>) -> bool {
for ancestor in ctx.ancestors() {
match ancestor {
Ancestor::IfStatementTest(_)
| Ancestor::WhileStatementTest(_)
| Ancestor::ForStatementTest(_)
| Ancestor::DoWhileStatementTest(_)
| Ancestor::ExpressionStatementExpression(_) => return true,
Ancestor::CallExpressionArguments(_)
| Ancestor::AssignmentPatternRight(_)
| Ancestor::BindingRestElementArgument(_)
| Ancestor::JSXSpreadAttributeArgument(_)
| Ancestor::NewExpressionArguments(_)
| Ancestor::ObjectPropertyKey(_)
| Ancestor::ObjectPropertyValue(_)
| Ancestor::ReturnStatementArgument(_)
| Ancestor::ThrowStatementArgument(_)
| Ancestor::YieldExpressionArgument(_)
| Ancestor::VariableDeclaratorInit(_) => return false,
_ => continue,
}
}
#[cfg(debug_assertions)]
unreachable!();
#[cfg(not(debug_assertions))]
false
}
fn try_minimize_binary(
e: &mut BinaryExpression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
// let ctx = Ctx(ctx);
if !ValueType::from(&e.left).is_boolean() {
return None;
}
let Expression::BooleanLiteral(b) = &mut e.right else {
return None;
};
match e.operator {
BinaryOperator::Inequality | BinaryOperator::StrictInequality => {
e.operator = BinaryOperator::Equality;
b.value = !b.value;
}
BinaryOperator::StrictEquality => {
e.operator = BinaryOperator::Equality;
}
_ => {}
}
Some(if b.value {
ctx.ast.move_expression(&mut e.left)
} else {
let argument = ctx.ast.move_expression(&mut e.left);
ctx.ast.expression_unary(e.span, UnaryOperator::LogicalNot, argument)
})
}
#[cfg(debug_assertions)]
unreachable!();
#[cfg(not(debug_assertions))]
false
}
/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/PeepholeMinimizeConditionsTest.java>
@ -1598,4 +1625,27 @@ mod test {
test_same("x.y ? x.y : bar");
test_same("x.y ? bar : x.y");
}
#[test]
fn compress_binary() {
test("a instanceof b === true", "a instanceof b");
test("a instanceof b == true", "a instanceof b");
test("a instanceof b === false", "!(a instanceof b)");
test("a instanceof b == false", "!(a instanceof b)");
test("a instanceof b !== true", "!(a instanceof b)");
test("a instanceof b != true", "!(a instanceof b)");
test("a instanceof b !== false", "a instanceof b");
test("a instanceof b != false", "a instanceof b");
test("delete x === true", "delete x");
test("delete x == true", "delete x");
test("delete x === false", "!(delete x)");
test("delete x == false", "!(delete x)");
test("delete x !== true", "!(delete x)");
test("delete x != true", "!(delete x)");
test("delete x !== false", "delete x");
test("delete x != false", "delete x");
}
}

View file

@ -19,7 +19,7 @@ Original | minified | minified | gzip | gzip | Fixture
2.14 MB | 726.19 kB | 724.14 kB | 180.18 kB | 181.07 kB | victory.js
3.20 MB | 1.01 MB | 1.01 MB | 331.91 kB | 331.56 kB | echarts.js
3.20 MB | 1.01 MB | 1.01 MB | 331.90 kB | 331.56 kB | echarts.js
6.69 MB | 2.32 MB | 2.31 MB | 492.80 kB | 488.28 kB | antd.js