Skip to content

Commit 835b258

Browse files
committed
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)
1 parent 2bcbed2 commit 835b258

2 files changed

Lines changed: 224 additions & 8 deletions

File tree

crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use oxc_ecmascript::{
77
};
88
use oxc_span::cmp::ContentEq;
99
use oxc_span::GetSpan;
10+
use oxc_span::SPAN;
1011
use oxc_syntax::{
1112
es_target::ESTarget,
1213
identifier::is_identifier_name,
@@ -134,6 +135,7 @@ impl<'a, 'b> PeepholeOptimizations {
134135
Self::try_compress_assignment_to_update_expression(e, ctx)
135136
}
136137
Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx)
138+
.or_else(|| Self::try_compress_is_object_and_not_null(e, ctx))
137139
.or_else(|| self.try_compress_logical_expression_to_assignment_expression(e, ctx))
138140
.or_else(|| Self::try_rotate_logical_expression(e, ctx)),
139141
Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx),
@@ -576,6 +578,171 @@ impl<'a, 'b> PeepholeOptimizations {
576578
}
577579
}
578580

581+
/// Compress `typeof foo === 'object' && foo !== null` into `typeof foo == 'object' && !!foo`.
582+
///
583+
/// - `typeof foo === 'object' && foo !== null` => `typeof foo == 'object' && !!foo`
584+
/// - `typeof foo == 'object' && foo != null` => `typeof foo == 'object' && !!foo`
585+
/// - `typeof foo !== 'object' || foo === null` => `typeof foo != 'object' || !foo`
586+
/// - `typeof foo != 'object' || foo == null` => `typeof foo != 'object' || !foo`
587+
///
588+
/// If `typeof foo == 'object'`, then `foo` is guaranteed to be an object or null.
589+
/// - If `foo` is an object, then `foo !== null` is `true`. If `foo` is null, then `foo !== null` is `false`.
590+
/// - If `foo` is an object, then `foo != null` is `true`. If `foo` is null, then `foo != null` is `false`.
591+
/// - If `foo` is an object, then `!!foo` is `true`. If `foo` is null, then `!!foo` is `false`.
592+
///
593+
/// This compression is safe for `document.all` because `typeof document.all` is not `'object'`.
594+
fn try_compress_is_object_and_not_null(
595+
expr: &mut LogicalExpression<'a>,
596+
ctx: Ctx<'a, '_>,
597+
) -> Option<Expression<'a>> {
598+
let inversed = match expr.operator {
599+
LogicalOperator::And => false,
600+
LogicalOperator::Or => true,
601+
LogicalOperator::Coalesce => return None,
602+
};
603+
604+
if let Some(new_expr) = Self::try_compress_is_object_and_not_null_for_left_and_right(
605+
&expr.left,
606+
&expr.right,
607+
expr.span,
608+
ctx,
609+
inversed,
610+
) {
611+
return Some(new_expr);
612+
}
613+
614+
let Expression::LogicalExpression(left) = &mut expr.left else {
615+
return None;
616+
};
617+
let inversed = match expr.operator {
618+
LogicalOperator::And => false,
619+
LogicalOperator::Or => true,
620+
LogicalOperator::Coalesce => return None,
621+
};
622+
623+
Self::try_compress_is_object_and_not_null_for_left_and_right(
624+
&left.right,
625+
&expr.right,
626+
Span::new(left.right.span().start, expr.span.end),
627+
ctx,
628+
inversed,
629+
)
630+
.map(|new_expr| {
631+
ctx.ast.expression_logical(
632+
expr.span,
633+
ctx.ast.move_expression(&mut left.left),
634+
expr.operator,
635+
new_expr,
636+
)
637+
})
638+
}
639+
640+
fn try_compress_is_object_and_not_null_for_left_and_right(
641+
left: &Expression<'a>,
642+
right: &Expression<'a>,
643+
span: Span,
644+
ctx: Ctx<'a, 'b>,
645+
inversed: bool,
646+
) -> Option<Expression<'a>> {
647+
let pair = Self::commutative_pair(
648+
(&left, &right),
649+
|a_expr| {
650+
let Expression::BinaryExpression(a) = a_expr else { return None };
651+
let is_target_ops = if inversed {
652+
matches!(
653+
a.operator,
654+
BinaryOperator::StrictInequality | BinaryOperator::Inequality
655+
)
656+
} else {
657+
matches!(a.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality)
658+
};
659+
if !is_target_ops {
660+
return None;
661+
}
662+
let (id, ()) = Self::commutative_pair(
663+
(&a.left, &a.right),
664+
|a_a| {
665+
let Expression::UnaryExpression(a_a) = a_a else { return None };
666+
if a_a.operator != UnaryOperator::Typeof {
667+
return None;
668+
}
669+
let Expression::Identifier(id) = &a_a.argument else { return None };
670+
Some(id)
671+
},
672+
|b| b.is_specific_string_literal("object").then_some(()),
673+
)?;
674+
Some((id, a_expr))
675+
},
676+
|b| {
677+
let Expression::BinaryExpression(b) = b else {
678+
return None;
679+
};
680+
let is_target_ops = if inversed {
681+
matches!(b.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality)
682+
} else {
683+
matches!(
684+
b.operator,
685+
BinaryOperator::StrictInequality | BinaryOperator::Inequality
686+
)
687+
};
688+
if !is_target_ops {
689+
return None;
690+
}
691+
let (id, ()) = Self::commutative_pair(
692+
(&b.left, &b.right),
693+
|a_a| {
694+
let Expression::Identifier(id) = a_a else { return None };
695+
Some(id)
696+
},
697+
|b| b.is_null().then_some(()),
698+
)?;
699+
Some(id)
700+
},
701+
);
702+
let ((typeof_id_ref, typeof_binary_expr), is_null_id_ref) = pair?;
703+
if typeof_id_ref.name != is_null_id_ref.name {
704+
return None;
705+
}
706+
// It should also return None when the reference might refer to a reference value created by a with statement
707+
// when the minifier supports with statements
708+
if ctx.is_global_reference(typeof_id_ref) {
709+
return None;
710+
}
711+
712+
let mut new_left_expr = typeof_binary_expr.clone_in(ctx.ast.allocator);
713+
if let Expression::BinaryExpression(new_left_expr_binary) = &mut new_left_expr {
714+
new_left_expr_binary.operator =
715+
if inversed { BinaryOperator::Inequality } else { BinaryOperator::Equality };
716+
} else {
717+
unreachable!();
718+
}
719+
720+
let new_right_expr = if inversed {
721+
ctx.ast.expression_unary(
722+
SPAN,
723+
UnaryOperator::LogicalNot,
724+
ctx.ast.expression_identifier_reference(is_null_id_ref.span, is_null_id_ref.name),
725+
)
726+
} else {
727+
ctx.ast.expression_unary(
728+
SPAN,
729+
UnaryOperator::LogicalNot,
730+
ctx.ast.expression_unary(
731+
SPAN,
732+
UnaryOperator::LogicalNot,
733+
ctx.ast
734+
.expression_identifier_reference(is_null_id_ref.span, is_null_id_ref.name),
735+
),
736+
)
737+
};
738+
Some(ctx.ast.expression_logical(
739+
span,
740+
new_left_expr,
741+
if inversed { LogicalOperator::Or } else { LogicalOperator::And },
742+
new_right_expr,
743+
))
744+
}
745+
579746
fn commutative_pair<'x, A, F, G, RetF: 'x, RetG: 'x>(
580747
pair: (&'x A, &'x A),
581748
check_a: F,
@@ -1838,6 +2005,55 @@ mod test {
18382005
test_same("(_foo = foo) === void 0 || bar === null");
18392006
}
18402007

2008+
#[test]
2009+
fn test_fold_is_object_and_not_null() {
2010+
test(
2011+
"var foo; v = typeof foo === 'object' && foo !== null",
2012+
"var foo; v = typeof foo == 'object' && !!foo",
2013+
);
2014+
test(
2015+
"var foo; v = typeof foo == 'object' && foo !== null",
2016+
"var foo; v = typeof foo == 'object' && !!foo",
2017+
);
2018+
test(
2019+
"var foo; v = typeof foo === 'object' && foo != null",
2020+
"var foo; v = typeof foo == 'object' && !!foo",
2021+
);
2022+
test(
2023+
"var foo; v = typeof foo == 'object' && foo != null",
2024+
"var foo; v = typeof foo == 'object' && !!foo",
2025+
);
2026+
test(
2027+
"var foo; v = typeof foo !== 'object' || foo === null",
2028+
"var foo; v = typeof foo != 'object' || !foo",
2029+
);
2030+
test(
2031+
"var foo; v = typeof foo != 'object' || foo === null",
2032+
"var foo; v = typeof foo != 'object' || !foo",
2033+
);
2034+
test(
2035+
"var foo; v = typeof foo !== 'object' || foo == null",
2036+
"var foo; v = typeof foo != 'object' || !foo",
2037+
);
2038+
test(
2039+
"var foo; v = typeof foo != 'object' || foo == null",
2040+
"var foo; v = typeof foo != 'object' || !foo",
2041+
);
2042+
test(
2043+
"var foo, bar; v = typeof foo === 'object' && foo !== null && bar !== 1",
2044+
"var foo, bar; v = typeof foo == 'object' && !!foo && bar !== 1",
2045+
);
2046+
test(
2047+
"var foo, bar; v = bar !== 1 && typeof foo === 'object' && foo !== null",
2048+
"var foo, bar; v = bar !== 1 && typeof foo == 'object' && !!foo",
2049+
);
2050+
test_same("var foo; v = typeof foo.a == 'object' && foo.a !== null"); // cannot be folded because accessing foo.a might have a side effect
2051+
test_same("v = foo !== null && typeof foo == 'object'"); // cannot be folded because accessing foo might have a side effect
2052+
test_same("v = typeof foo == 'object' && foo !== null"); // cannot be folded because accessing foo might have a side effect
2053+
test_same("var foo, bar; v = typeof foo == 'object' && bar !== null");
2054+
test_same("var foo; v = typeof foo == 'string' && foo !== null");
2055+
}
2056+
18412057
#[test]
18422058
fn test_fold_logical_expression_to_assignment_expression() {
18432059
test("x || (x = 3)", "x ||= 3");

tasks/minsize/minsize.snap

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
| Oxc | ESBuild | Oxc | ESBuild |
22
Original | minified | minified | gzip | gzip | Fixture
33
-------------------------------------------------------------------------------------
4-
72.14 kB | 23.70 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js
4+
72.14 kB | 23.67 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js
55

66
173.90 kB | 59.79 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js
77

8-
287.63 kB | 90.08 kB | 90.07 kB | 32.03 kB | 31.95 kB | jquery.js
8+
287.63 kB | 90.08 kB | 90.07 kB | 32.02 kB | 31.95 kB | jquery.js
99

1010
342.15 kB | 118.19 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js
1111

12-
544.10 kB | 71.76 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
12+
544.10 kB | 71.75 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js
1313

14-
555.77 kB | 272.90 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
14+
555.77 kB | 272.89 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js
1515

16-
1.01 MB | 460.18 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js
16+
1.01 MB | 460.18 kB | 458.89 kB | 126.77 kB | 126.71 kB | bundle.min.js
1717

1818
1.25 MB | 652.90 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js
1919

20-
2.14 MB | 724.06 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js
20+
2.14 MB | 724.01 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js
2121

22-
3.20 MB | 1.01 MB | 1.01 MB | 332.00 kB | 331.56 kB | echarts.js
22+
3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js
2323

2424
6.69 MB | 2.31 MB | 2.31 MB | 491.99 kB | 488.28 kB | antd.js
2525

26-
10.95 MB | 3.48 MB | 3.49 MB | 905.39 kB | 915.50 kB | typescript.js
26+
10.95 MB | 3.48 MB | 3.49 MB | 905.37 kB | 915.50 kB | typescript.js
2727

0 commit comments

Comments
 (0)