Skip to content

Commit 4a2b4a7

Browse files
committed
feat(minifier): compress typeof foo === 'object' && foo !== null to typeof foo == 'object' && !!foo
1 parent 9268326 commit 4a2b4a7

File tree

2 files changed

+230
-8
lines changed

2 files changed

+230
-8
lines changed

crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

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

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

tasks/minsize/minsize.snap

+8-8
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)