feat(minifier): compress typeof foo === 'object' && foo !== null to typeof foo == 'object' && !!foo (#8638)

If `typeof foo == 'object'`, then `foo` is guaranteed to be an object or null. In that case, `foo !== null` can be replaced with `!!foo` because objects return `true` for `!!foo` and null returns `false` for it.

**References**
- [Spec of `typeof`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-typeof-operator)
- [Spec of `!`](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-logical-not-operator)
- [Spec of `ToBoolean`](https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toboolean)
This commit is contained in:
sapphi-red 2025-01-21 14:59:25 +00:00
parent 2bcbed2d50
commit 835b25889b
No known key found for this signature in database
GPG key ID: 67631A259A77AC6C
2 changed files with 224 additions and 8 deletions

View file

@ -7,6 +7,7 @@ use oxc_ecmascript::{
};
use oxc_span::cmp::ContentEq;
use oxc_span::GetSpan;
use oxc_span::SPAN;
use oxc_syntax::{
es_target::ESTarget,
identifier::is_identifier_name,
@ -134,6 +135,7 @@ impl<'a, 'b> PeepholeOptimizations {
Self::try_compress_assignment_to_update_expression(e, ctx)
}
Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx)
.or_else(|| Self::try_compress_is_object_and_not_null(e, ctx))
.or_else(|| self.try_compress_logical_expression_to_assignment_expression(e, ctx))
.or_else(|| Self::try_rotate_logical_expression(e, ctx)),
Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx),
@ -576,6 +578,171 @@ impl<'a, 'b> PeepholeOptimizations {
}
}
/// Compress `typeof foo === 'object' && foo !== null` into `typeof foo == 'object' && !!foo`.
///
/// - `typeof foo === 'object' && foo !== null` => `typeof foo == 'object' && !!foo`
/// - `typeof foo == 'object' && foo != null` => `typeof foo == 'object' && !!foo`
/// - `typeof foo !== 'object' || foo === null` => `typeof foo != 'object' || !foo`
/// - `typeof foo != 'object' || foo == null` => `typeof foo != 'object' || !foo`
///
/// If `typeof foo == 'object'`, then `foo` is guaranteed to be an object or null.
/// - If `foo` is an object, then `foo !== null` is `true`. If `foo` is null, then `foo !== null` is `false`.
/// - If `foo` is an object, then `foo != null` is `true`. If `foo` is null, then `foo != null` is `false`.
/// - If `foo` is an object, then `!!foo` is `true`. If `foo` is null, then `!!foo` is `false`.
///
/// This compression is safe for `document.all` because `typeof document.all` is not `'object'`.
fn try_compress_is_object_and_not_null(
expr: &mut LogicalExpression<'a>,
ctx: Ctx<'a, '_>,
) -> Option<Expression<'a>> {
let inversed = match expr.operator {
LogicalOperator::And => false,
LogicalOperator::Or => true,
LogicalOperator::Coalesce => return None,
};
if let Some(new_expr) = Self::try_compress_is_object_and_not_null_for_left_and_right(
&expr.left,
&expr.right,
expr.span,
ctx,
inversed,
) {
return Some(new_expr);
}
let Expression::LogicalExpression(left) = &mut expr.left else {
return None;
};
let inversed = match expr.operator {
LogicalOperator::And => false,
LogicalOperator::Or => true,
LogicalOperator::Coalesce => return None,
};
Self::try_compress_is_object_and_not_null_for_left_and_right(
&left.right,
&expr.right,
Span::new(left.right.span().start, expr.span.end),
ctx,
inversed,
)
.map(|new_expr| {
ctx.ast.expression_logical(
expr.span,
ctx.ast.move_expression(&mut left.left),
expr.operator,
new_expr,
)
})
}
fn try_compress_is_object_and_not_null_for_left_and_right(
left: &Expression<'a>,
right: &Expression<'a>,
span: Span,
ctx: Ctx<'a, 'b>,
inversed: bool,
) -> Option<Expression<'a>> {
let pair = Self::commutative_pair(
(&left, &right),
|a_expr| {
let Expression::BinaryExpression(a) = a_expr else { return None };
let is_target_ops = if inversed {
matches!(
a.operator,
BinaryOperator::StrictInequality | BinaryOperator::Inequality
)
} else {
matches!(a.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality)
};
if !is_target_ops {
return None;
}
let (id, ()) = Self::commutative_pair(
(&a.left, &a.right),
|a_a| {
let Expression::UnaryExpression(a_a) = a_a else { return None };
if a_a.operator != UnaryOperator::Typeof {
return None;
}
let Expression::Identifier(id) = &a_a.argument else { return None };
Some(id)
},
|b| b.is_specific_string_literal("object").then_some(()),
)?;
Some((id, a_expr))
},
|b| {
let Expression::BinaryExpression(b) = b else {
return None;
};
let is_target_ops = if inversed {
matches!(b.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality)
} else {
matches!(
b.operator,
BinaryOperator::StrictInequality | BinaryOperator::Inequality
)
};
if !is_target_ops {
return None;
}
let (id, ()) = Self::commutative_pair(
(&b.left, &b.right),
|a_a| {
let Expression::Identifier(id) = a_a else { return None };
Some(id)
},
|b| b.is_null().then_some(()),
)?;
Some(id)
},
);
let ((typeof_id_ref, typeof_binary_expr), is_null_id_ref) = pair?;
if typeof_id_ref.name != is_null_id_ref.name {
return None;
}
// It should also return None when the reference might refer to a reference value created by a with statement
// when the minifier supports with statements
if ctx.is_global_reference(typeof_id_ref) {
return None;
}
let mut new_left_expr = typeof_binary_expr.clone_in(ctx.ast.allocator);
if let Expression::BinaryExpression(new_left_expr_binary) = &mut new_left_expr {
new_left_expr_binary.operator =
if inversed { BinaryOperator::Inequality } else { BinaryOperator::Equality };
} else {
unreachable!();
}
let new_right_expr = if inversed {
ctx.ast.expression_unary(
SPAN,
UnaryOperator::LogicalNot,
ctx.ast.expression_identifier_reference(is_null_id_ref.span, is_null_id_ref.name),
)
} else {
ctx.ast.expression_unary(
SPAN,
UnaryOperator::LogicalNot,
ctx.ast.expression_unary(
SPAN,
UnaryOperator::LogicalNot,
ctx.ast
.expression_identifier_reference(is_null_id_ref.span, is_null_id_ref.name),
),
)
};
Some(ctx.ast.expression_logical(
span,
new_left_expr,
if inversed { LogicalOperator::Or } else { LogicalOperator::And },
new_right_expr,
))
}
fn commutative_pair<'x, A, F, G, RetF: 'x, RetG: 'x>(
pair: (&'x A, &'x A),
check_a: F,
@ -1838,6 +2005,55 @@ mod test {
test_same("(_foo = foo) === void 0 || bar === null");
}
#[test]
fn test_fold_is_object_and_not_null() {
test(
"var foo; v = typeof foo === 'object' && foo !== null",
"var foo; v = typeof foo == 'object' && !!foo",
);
test(
"var foo; v = typeof foo == 'object' && foo !== null",
"var foo; v = typeof foo == 'object' && !!foo",
);
test(
"var foo; v = typeof foo === 'object' && foo != null",
"var foo; v = typeof foo == 'object' && !!foo",
);
test(
"var foo; v = typeof foo == 'object' && foo != null",
"var foo; v = typeof foo == 'object' && !!foo",
);
test(
"var foo; v = typeof foo !== 'object' || foo === null",
"var foo; v = typeof foo != 'object' || !foo",
);
test(
"var foo; v = typeof foo != 'object' || foo === null",
"var foo; v = typeof foo != 'object' || !foo",
);
test(
"var foo; v = typeof foo !== 'object' || foo == null",
"var foo; v = typeof foo != 'object' || !foo",
);
test(
"var foo; v = typeof foo != 'object' || foo == null",
"var foo; v = typeof foo != 'object' || !foo",
);
test(
"var foo, bar; v = typeof foo === 'object' && foo !== null && bar !== 1",
"var foo, bar; v = typeof foo == 'object' && !!foo && bar !== 1",
);
test(
"var foo, bar; v = bar !== 1 && typeof foo === 'object' && foo !== null",
"var foo, bar; v = bar !== 1 && typeof foo == 'object' && !!foo",
);
test_same("var foo; v = typeof foo.a == 'object' && foo.a !== null"); // cannot be folded because accessing foo.a might have a side effect
test_same("v = foo !== null && typeof foo == 'object'"); // cannot be folded because accessing foo might have a side effect
test_same("v = typeof foo == 'object' && foo !== null"); // cannot be folded because accessing foo might have a side effect
test_same("var foo, bar; v = typeof foo == 'object' && bar !== null");
test_same("var foo; v = typeof foo == 'string' && foo !== null");
}
#[test]
fn test_fold_logical_expression_to_assignment_expression() {
test("x || (x = 3)", "x ||= 3");

View file

@ -1,27 +1,27 @@
| Oxc | ESBuild | Oxc | ESBuild |
Original | minified | minified | gzip | gzip | Fixture
-------------------------------------------------------------------------------------
72.14 kB | 23.70 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js
72.14 kB | 23.67 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js
173.90 kB | 59.79 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js
287.63 kB | 90.08 kB | 90.07 kB | 32.03 kB | 31.95 kB | jquery.js
287.63 kB | 90.08 kB | 90.07 kB | 32.02 kB | 31.95 kB | jquery.js
342.15 kB | 118.19 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js
544.10 kB | 71.76 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
544.10 kB | 71.75 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
555.77 kB | 272.90 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
555.77 kB | 272.89 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
1.01 MB | 460.18 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js
1.01 MB | 460.18 kB | 458.89 kB | 126.77 kB | 126.71 kB | bundle.min.js
1.25 MB | 652.90 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js
2.14 MB | 724.06 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js
2.14 MB | 724.01 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js
3.20 MB | 1.01 MB | 1.01 MB | 332.00 kB | 331.56 kB | echarts.js
3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js
6.69 MB | 2.31 MB | 2.31 MB | 491.99 kB | 488.28 kB | antd.js
10.95 MB | 3.48 MB | 3.49 MB | 905.39 kB | 915.50 kB | typescript.js
10.95 MB | 3.48 MB | 3.49 MB | 905.37 kB | 915.50 kB | typescript.js