swc_ecma_minifier/compress/pure/
bools.rs

1use std::mem::swap;
2
3use swc_common::{util::take::Take, Spanned};
4use swc_ecma_ast::*;
5use swc_ecma_utils::{ExprCtx, ExprExt, Type, Value};
6
7use super::Pure;
8use crate::{
9    compress::util::{can_absorb_negate, is_eq, is_pure_undefined, negate, negate_cost},
10    util::make_bool,
11};
12
13impl Pure<'_> {
14    pub(super) fn compress_if_stmt_as_expr(&mut self, s: &mut Stmt) {
15        if !self.options.conditionals && !self.options.bools {
16            return;
17        }
18
19        let stmt = match s {
20            Stmt::If(v) => v,
21            _ => return,
22        };
23
24        if stmt.alt.is_none() {
25            if let Stmt::Expr(cons) = &mut *stmt.cons {
26                self.changed = true;
27                report_change!("conditionals: `if (foo) bar;` => `foo && bar`");
28                *s = ExprStmt {
29                    span: stmt.span,
30                    expr: BinExpr {
31                        span: stmt.test.span(),
32                        op: op!("&&"),
33                        left: stmt.test.take(),
34                        right: cons.expr.take(),
35                    }
36                    .into(),
37                }
38                .into();
39            }
40        }
41    }
42
43    pub(super) fn make_bool_short(
44        &mut self,
45        e: &mut Expr,
46        in_bool_ctx: bool,
47        ignore_return_value: bool,
48    ) {
49        match e {
50            Expr::Cond(cond) => {
51                self.make_bool_short(&mut cond.test, true, false);
52                self.make_bool_short(&mut cond.cons, in_bool_ctx, ignore_return_value);
53                self.make_bool_short(&mut cond.alt, in_bool_ctx, ignore_return_value);
54
55                if negate_cost(self.expr_ctx, &cond.test, true, false) >= 0 {
56                    return;
57                }
58                self.negate(&mut cond.test, true, false);
59                swap(&mut cond.cons, &mut cond.alt);
60                return;
61            }
62
63            Expr::Bin(BinExpr {
64                op: op @ (op!("&&") | op!("||")),
65                left,
66                right,
67                ..
68            }) => {
69                self.make_bool_short(left, in_bool_ctx, false);
70                self.make_bool_short(right, in_bool_ctx, ignore_return_value);
71
72                if in_bool_ctx {
73                    match *op {
74                        op!("||") => {
75                            // `a || false` => `a` (as it will be casted to boolean anyway)
76
77                            if let Value::Known(false) = right.as_pure_bool(self.expr_ctx) {
78                                report_change!(
79                                    "bools: `expr || false` => `expr` (in bool context)"
80                                );
81                                self.changed = true;
82                                *e = *left.take();
83                                return;
84                            }
85                        }
86
87                        op!("&&") => {
88                            // false && foo => false (as it will be always false)
89
90                            if let (_, Value::Known(false)) = left.cast_to_bool(self.expr_ctx) {
91                                report_change!(
92                                    "bools: `false && foo` => `false` (in bool context)"
93                                );
94                                self.changed = true;
95                                *e = *left.take();
96                                return;
97                            }
98                        }
99
100                        _ => {}
101                    }
102                }
103            }
104
105            Expr::Bin(BinExpr { left, right, .. }) => {
106                self.make_bool_short(left, false, false);
107                self.make_bool_short(right, false, false);
108                return;
109            }
110
111            Expr::Unary(UnaryExpr {
112                op: op!("!"), arg, ..
113            }) => {
114                self.make_bool_short(arg, true, ignore_return_value);
115                return;
116            }
117
118            Expr::Array(ArrayLit { elems, .. }) => {
119                for elem in elems.iter_mut().flatten() {
120                    self.make_bool_short(&mut elem.expr, false, false);
121                }
122                return;
123            }
124
125            Expr::Call(CallExpr { callee, args, .. }) => {
126                if let Callee::Expr(callee) = callee {
127                    self.make_bool_short(callee, false, false);
128                }
129
130                for arg in args {
131                    self.make_bool_short(&mut arg.expr, false, false);
132                }
133                return;
134            }
135
136            Expr::Seq(SeqExpr { exprs, .. }) => {
137                let len = exprs.len();
138                for (idx, expr) in exprs.iter_mut().enumerate() {
139                    let is_last = idx == len - 1;
140
141                    self.make_bool_short(expr, false, !is_last || ignore_return_value);
142                }
143                return;
144            }
145
146            Expr::Assign(AssignExpr { right, .. }) => {
147                self.make_bool_short(right, false, false);
148                return;
149            }
150
151            _ => return,
152        }
153
154        let cost = negate_cost(self.expr_ctx, e, in_bool_ctx, ignore_return_value);
155
156        if cost >= 0 {
157            return;
158        }
159
160        if let Expr::Bin(BinExpr {
161            op: op @ (op!("&&") | op!("||")),
162            left,
163            ..
164        }) = e
165        {
166            if ignore_return_value {
167                // Negate only left, and change operator
168                *op = match op {
169                    op!("&&") => op!("||"),
170                    op!("||") => op!("&&"),
171                    _ => unreachable!(),
172                };
173
174                self.negate(left, true, false);
175            }
176        }
177    }
178
179    pub(super) fn negate_twice(&mut self, e: &mut Expr, is_ret_val_ignored: bool) {
180        negate(self.expr_ctx, e, true, is_ret_val_ignored);
181        negate(self.expr_ctx, e, false, is_ret_val_ignored);
182    }
183
184    pub(super) fn negate(&mut self, e: &mut Expr, in_bool_ctx: bool, is_ret_val_ignored: bool) {
185        negate(self.expr_ctx, e, in_bool_ctx, is_ret_val_ignored)
186    }
187
188    pub(super) fn optimize_negate_eq(&mut self, e: &mut Expr) {
189        fn negate_eq(op: BinaryOp) -> BinaryOp {
190            match op {
191                op!("==") => op!("!="),
192                op!("!=") => op!("=="),
193                op!("===") => op!("!=="),
194                op!("!==") => op!("==="),
195                _ => unreachable!(),
196            }
197        }
198
199        if !self.options.bools {
200            return;
201        }
202
203        let Expr::Unary(UnaryExpr {
204            op: op!("!"), arg, ..
205        }) = e
206        else {
207            return;
208        };
209
210        let arg_can_negate = can_absorb_negate(arg, self.expr_ctx);
211
212        match &mut **arg {
213            Expr::Bin(BinExpr { op, .. }) if is_eq(*op) => {
214                self.changed = true;
215                report_change!("bools: Optimizing `!(a == b)` as `a != b`");
216
217                *op = negate_eq(*op);
218
219                *e = *arg.take();
220            }
221            Expr::Bin(BinExpr {
222                op: op @ (op!("&&") | op!("||")),
223                left,
224                right,
225                ..
226            }) if arg_can_negate => {
227                self.changed = true;
228                report_change!("bools: Optimizing `!(a == b && c == d)` as `a != b`");
229
230                *op = match op {
231                    op!("&&") => op!("||"),
232                    op!("||") => op!("&&"),
233                    _ => unreachable!(),
234                };
235
236                self.negate(left, false, false);
237                self.negate(right, false, false);
238                *e = *arg.take();
239            }
240            _ => (),
241        }
242    }
243
244    pub(super) fn compress_cmp_with_long_op(&mut self, e: &mut BinExpr) {
245        if !matches!(e.op, op!("===") | op!("!==")) {
246            return;
247        }
248
249        let is_typeof_unaray = |l: &Expr, r: &Expr| {
250            matches!(
251                (l, r),
252                (
253                    Expr::Unary(UnaryExpr {
254                        op: op!("typeof"),
255                        ..
256                    }),
257                    Expr::Lit(..)
258                )
259            )
260        };
261
262        let should_optimize = is_typeof_unaray(&e.left, &e.right)
263            || is_typeof_unaray(&e.right, &e.left)
264            || (self.options.comparisons && {
265                if let Value::Known(l) = e.left.get_type(self.expr_ctx) {
266                    if let Value::Known(r) = e.right.get_type(self.expr_ctx) {
267                        l == r
268                    } else {
269                        false
270                    }
271                } else {
272                    false
273                }
274            });
275
276        if should_optimize {
277            report_change!("bools: Compressing comparison of `typeof` with literal");
278            self.changed = true;
279            e.op = match e.op {
280                op!("===") => {
281                    op!("==")
282                }
283                op!("!==") => {
284                    op!("!=")
285                }
286                _ => {
287                    unreachable!()
288                }
289            }
290        }
291    }
292
293    ///
294    /// - `!condition() || !-3.5` => `!condition()`
295    ///
296    /// In this case, if lhs is false, rhs is also false so it's removable.
297    pub(super) fn remove_useless_logical_rhs(&mut self, e: &mut Expr) {
298        if !self.options.bools {
299            return;
300        }
301
302        match e {
303            Expr::Bin(BinExpr {
304                left,
305                op: op @ op!("&&"),
306                right,
307                ..
308            })
309            | Expr::Bin(BinExpr {
310                left,
311                op: op @ op!("||"),
312                right,
313                ..
314            }) => {
315                let lt = left.get_type(self.expr_ctx);
316                let rt = right.get_type(self.expr_ctx);
317
318                if let (Value::Known(Type::Bool), Value::Known(Type::Bool)) = (lt, rt) {
319                    let rb = right.as_pure_bool(self.expr_ctx);
320                    let rb = match rb {
321                        Value::Known(v) => v,
322                        Value::Unknown => return,
323                    };
324
325                    //
326                    let can_remove = if *op == op!("&&") { rb } else { !rb };
327
328                    if can_remove {
329                        #[allow(clippy::if_same_then_else)]
330                        if *op == op!("&&") {
331                            report_change!("booleans: Compressing `!foo && true` as `!foo`");
332                        } else {
333                            report_change!("booleans: Compressing `!foo || false` as `!foo`");
334                        }
335                        self.changed = true;
336                        *e = *left.take();
337                    }
338                }
339            }
340            _ => {}
341        }
342    }
343
344    /// Note: This should be invoked before calling `handle_negated_seq`.
345    pub(super) fn compress_useless_deletes(&mut self, e: &mut Expr) {
346        if !self.options.bools {
347            return;
348        }
349
350        let delete = match e {
351            Expr::Unary(
352                u @ UnaryExpr {
353                    op: op!("delete"), ..
354                },
355            ) => u,
356            _ => return,
357        };
358
359        if delete.arg.may_have_side_effects(ExprCtx {
360            is_unresolved_ref_safe: true,
361            ..self.expr_ctx
362        }) {
363            return;
364        }
365
366        let convert_to_true = match &*delete.arg {
367            Expr::Seq(..)
368            | Expr::Cond(..)
369            | Expr::Bin(BinExpr { op: op!("&&"), .. })
370            | Expr::Bin(BinExpr { op: op!("||"), .. }) => true,
371            // V8 and terser test ref have different opinion.
372            Expr::Ident(Ident { sym, .. }) if &**sym == "Infinity" => false,
373            Expr::Ident(Ident { sym, .. }) if &**sym == "undefined" => false,
374            Expr::Ident(Ident { sym, .. }) if &**sym == "NaN" => false,
375
376            e if is_pure_undefined(self.expr_ctx, e) => true,
377
378            Expr::Ident(i) => i.ctxt != self.expr_ctx.unresolved_ctxt,
379
380            // NaN
381            Expr::Bin(BinExpr {
382                op: op!("/"),
383                right,
384                ..
385            }) => {
386                let rn = right.as_pure_number(self.expr_ctx);
387                let v = if let Value::Known(rn) = rn {
388                    rn != 0.0
389                } else {
390                    false
391                };
392
393                if v {
394                    true
395                } else {
396                    self.changed = true;
397                    let span = delete.arg.span();
398                    report_change!("booleans: Compressing `delete` as sequence expression");
399                    *e = SeqExpr {
400                        span,
401                        exprs: vec![delete.arg.take(), Box::new(make_bool(span, true))],
402                    }
403                    .into();
404                    return;
405                }
406            }
407
408            _ => false,
409        };
410
411        if convert_to_true {
412            self.changed = true;
413            let span = delete.arg.span();
414            report_change!("booleans: Compressing `delete` => true");
415            *e = make_bool(span, true);
416        }
417    }
418
419    pub(super) fn handle_negated_seq(&mut self, n: &mut Expr) {
420        match &mut *n {
421            Expr::Unary(e @ UnaryExpr { op: op!("!"), .. }) => {
422                if let Expr::Seq(SeqExpr { exprs, .. }) = &mut *e.arg {
423                    if exprs.is_empty() {
424                        return;
425                    }
426                    report_change!("bools: Optimizing negated sequences");
427
428                    {
429                        let last = exprs.last_mut().unwrap();
430                        self.optimize_expr_in_bool_ctx(last, false);
431                        // Negate last element.
432                        negate(self.expr_ctx, last, false, false);
433                    }
434
435                    *n = *e.arg.take();
436                }
437            }
438            Expr::Unary(UnaryExpr {
439                op: op!("delete"), ..
440            }) => {
441                // TODO
442            }
443            _ => {}
444        }
445    }
446
447    /// This method converts `!1` to `0`.
448    pub(super) fn optimize_expr_in_bool_ctx(
449        &mut self,
450        n: &mut Expr,
451        is_return_value_ignored: bool,
452    ) {
453        match n {
454            Expr::Bin(BinExpr {
455                op: op!("&&") | op!("||"),
456                left,
457                right,
458                ..
459            }) => {
460                // Regardless if it's truthy or falsy, we can optimize it because it will be
461                // casted as bool anyway.
462                self.optimize_expr_in_bool_ctx(left, false);
463                self.optimize_expr_in_bool_ctx(right, is_return_value_ignored);
464                return;
465            }
466
467            Expr::Seq(e) => {
468                if let Some(last) = e.exprs.last_mut() {
469                    self.optimize_expr_in_bool_ctx(last, is_return_value_ignored);
470                }
471            }
472
473            _ => {}
474        }
475
476        if !self.options.bools {
477            return;
478        }
479
480        match n {
481            Expr::Unary(UnaryExpr {
482                span,
483                op: op!("!"),
484                arg,
485            }) => match &mut **arg {
486                Expr::Lit(Lit::Num(Number { value, .. })) => {
487                    report_change!("Optimizing: number => number (in bool context)");
488
489                    self.changed = true;
490                    *n = Lit::Num(Number {
491                        span: *span,
492                        value: if *value == 0.0 { 1.0 } else { 0.0 },
493                        raw: None,
494                    })
495                    .into()
496                }
497
498                Expr::Unary(UnaryExpr {
499                    op: op!("!"), arg, ..
500                }) => {
501                    report_change!("bools: !!expr => expr (in bool ctx)");
502                    self.changed = true;
503                    *n = *arg.take();
504                }
505                _ => {}
506            },
507
508            Expr::Unary(UnaryExpr {
509                span,
510                op: op!("typeof"),
511                arg,
512            }) => {
513                report_change!("Optimizing: typeof => true (in bool context)");
514                self.changed = true;
515
516                match &**arg {
517                    Expr::Ident(..) => {
518                        *n = Lit::Num(Number {
519                            span: *span,
520                            value: 1.0,
521                            raw: None,
522                        })
523                        .into()
524                    }
525                    _ => {
526                        // Return value of typeof is always truthy
527                        let true_expr = Lit::Num(Number {
528                            span: *span,
529                            value: 1.0,
530                            raw: None,
531                        })
532                        .into();
533                        *n = SeqExpr {
534                            span: *span,
535                            exprs: vec![arg.take(), true_expr],
536                        }
537                        .into()
538                    }
539                }
540            }
541
542            Expr::Lit(Lit::Str(s)) => {
543                if !is_return_value_ignored {
544                    report_change!("Converting string as boolean expressions");
545                    self.changed = true;
546                    *n = Lit::Num(Number {
547                        span: s.span,
548                        value: if s.value.is_empty() { 0.0 } else { 1.0 },
549                        raw: None,
550                    })
551                    .into();
552                }
553            }
554
555            Expr::Lit(Lit::Num(num)) => {
556                if num.value == 1.0 || num.value == 0.0 {
557                    return;
558                }
559                if self.options.bools {
560                    report_change!("booleans: Converting number as boolean expressions");
561                    self.changed = true;
562                    *n = Lit::Num(Number {
563                        span: num.span,
564                        value: if num.value == 0.0 { 0.0 } else { 1.0 },
565                        raw: None,
566                    })
567                    .into();
568                }
569            }
570
571            Expr::Bin(BinExpr {
572                op: op!("??"),
573                left,
574                right,
575                ..
576            }) => {
577                // Optimize if (a ?? false); as if (a);
578                if let Value::Known(false) = right.as_pure_bool(self.expr_ctx) {
579                    report_change!(
580                        "Dropping right operand of `??` as it's always false (in bool context)"
581                    );
582                    self.changed = true;
583                    *n = *left.take();
584                }
585            }
586
587            Expr::Bin(BinExpr {
588                op: op!("||"),
589                left,
590                right,
591                ..
592            }) => {
593                // `a || false` => `a` (as it will be casted to boolean anyway)
594
595                if let Value::Known(false) = right.as_pure_bool(self.expr_ctx) {
596                    report_change!("bools: `expr || false` => `expr` (in bool context)");
597                    self.changed = true;
598                    *n = *left.take();
599                }
600            }
601
602            _ => {
603                let v = n.as_pure_bool(self.expr_ctx);
604                if let Value::Known(v) = v {
605                    let span = n.span();
606                    report_change!("Optimizing expr as {} (in bool context)", v);
607                    *n = make_bool(span, v);
608                }
609            }
610        }
611    }
612
613    /// Rules:
614    ///  - `l > i` => `i < l`
615    fn can_swap_bin_operands(&mut self, l: &Expr, r: &Expr, is_for_rel: bool) -> bool {
616        match (l, r) {
617            (Expr::Member(_), _) if is_for_rel => false,
618
619            (Expr::Update(..) | Expr::Assign(..), Expr::Lit(..)) if is_for_rel => false,
620
621            (Expr::Ident(..), Expr::Ident(Ident { sym: r_s, .. })) if &**r_s == "undefined" => true,
622
623            (
624                Expr::Member(..)
625                | Expr::Call(..)
626                | Expr::Assign(..)
627                | Expr::Update(..)
628                | Expr::Bin(BinExpr {
629                    op: op!("&&") | op!("||"),
630                    ..
631                }),
632                Expr::Lit(..),
633            ) => true,
634
635            (
636                Expr::Member(..) | Expr::Call(..) | Expr::Assign(..),
637                Expr::Unary(UnaryExpr {
638                    op: op!("!"), arg, ..
639                }),
640            ) if matches!(&**arg, Expr::Lit(..)) => true,
641
642            (Expr::Member(..) | Expr::Call(..) | Expr::Assign(..), r)
643                if is_pure_undefined(self.expr_ctx, r) =>
644            {
645                true
646            }
647
648            (Expr::Ident(l), Expr::Ident(r)) => {
649                is_for_rel && self.options.unsafe_comps && l.sym < r.sym
650            }
651
652            (Expr::Ident(..), Expr::Lit(..)) if is_for_rel => false,
653
654            (Expr::Ident(..), Expr::Lit(..))
655            | (
656                Expr::Ident(..) | Expr::Member(..),
657                Expr::Unary(UnaryExpr {
658                    op: op!("void") | op!("!"),
659                    ..
660                }),
661            )
662            | (
663                Expr::This(..),
664                Expr::Unary(UnaryExpr {
665                    op: op!("void"), ..
666                }),
667            )
668            | (Expr::Unary(..), Expr::Lit(..))
669            | (Expr::Tpl(..), Expr::Lit(..)) => true,
670            _ => false,
671        }
672    }
673
674    fn try_swap_bin(&mut self, op: BinaryOp, left: &mut Expr, right: &mut Expr) -> bool {
675        let can_swap = matches!(
676            op,
677            op!("===")
678                | op!("!==")
679                | op!("==")
680                | op!("!=")
681                | op!("&")
682                | op!("^")
683                | op!("|")
684                | op!("*")
685        ) && self.can_swap_bin_operands(left, right, false);
686
687        // a * (b / c) -> b / c * a
688        let can_swap = can_swap
689            || (matches!(op, op!("*") | op!("&") | op!("|") | op!("^"))
690                && right
691                    .as_bin()
692                    .filter(|b| b.op.precedence() == op.precedence())
693                    .is_some()
694                && left
695                    .as_bin()
696                    .filter(|b| b.op.precedence() == op.precedence())
697                    .is_none()
698                && !left.may_have_side_effects(self.expr_ctx)
699                && !right.may_have_side_effects(self.expr_ctx));
700
701        if can_swap {
702            report_change!("Swapping operands of binary expession");
703            swap(left, right);
704        }
705
706        can_swap
707    }
708
709    /// Swap lhs and rhs in certain conditions.
710    pub(super) fn swap_bin_operands(&mut self, expr: &mut Expr) {
711        match expr {
712            Expr::Bin(e @ BinExpr { op: op!("<="), .. })
713            | Expr::Bin(e @ BinExpr { op: op!("<"), .. }) => {
714                if self.options.comparisons && self.can_swap_bin_operands(&e.left, &e.right, true) {
715                    self.changed = true;
716                    report_change!("comparisons: Swapping operands of {}", e.op);
717
718                    e.op = if e.op == op!("<=") {
719                        op!(">=")
720                    } else {
721                        op!(">")
722                    };
723
724                    swap(&mut e.left, &mut e.right);
725                }
726            }
727
728            Expr::Bin(bin) => {
729                if self.try_swap_bin(bin.op, &mut bin.left, &mut bin.right) {
730                    self.changed = true;
731                }
732            }
733            _ => {}
734        }
735    }
736}