@@ -7,6 +7,7 @@ use oxc_ecmascript::{
7
7
} ;
8
8
use oxc_span:: cmp:: ContentEq ;
9
9
use oxc_span:: GetSpan ;
10
+ use oxc_span:: SPAN ;
10
11
use oxc_syntax:: {
11
12
es_target:: ESTarget ,
12
13
identifier:: is_identifier_name,
@@ -134,6 +135,7 @@ impl<'a, 'b> PeepholeOptimizations {
134
135
Self :: try_compress_assignment_to_update_expression ( e, ctx)
135
136
}
136
137
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) )
137
139
. or_else ( || self . try_compress_logical_expression_to_assignment_expression ( e, ctx) )
138
140
. or_else ( || Self :: try_rotate_logical_expression ( e, ctx) ) ,
139
141
Expression :: TemplateLiteral ( t) => Self :: try_fold_template_literal ( t, ctx) ,
@@ -576,6 +578,177 @@ impl<'a, 'b> PeepholeOptimizations {
576
578
}
577
579
}
578
580
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
+
579
752
fn commutative_pair < ' x , A , F , G , RetF : ' x , RetG : ' x > (
580
753
pair : ( & ' x A , & ' x A ) ,
581
754
check_a : F ,
@@ -1838,6 +2011,55 @@ mod test {
1838
2011
test_same ( "(_foo = foo) === void 0 || bar === null" ) ;
1839
2012
}
1840
2013
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
+
1841
2063
#[ test]
1842
2064
fn test_fold_logical_expression_to_assignment_expression ( ) {
1843
2065
test ( "x || (x = 3)" , "x ||= 3" ) ;
0 commit comments