swc_ecma_minifier/compress/util/
mod.rs

1use std::{cmp::Ordering, f64};
2
3use swc_common::{util::take::Take, DUMMY_SP};
4use swc_ecma_ast::*;
5use swc_ecma_utils::{number::JsNumber, ExprCtx, ExprExt, IdentUsageFinder, Type, Value};
6use swc_ecma_visit::{
7    noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
8};
9
10#[cfg(feature = "debug")]
11use crate::debug::dump;
12use crate::util::ModuleItemExt;
13
14#[cfg(test)]
15mod tests;
16
17/// Creates `!e` where e is the expression passed as an argument.
18///
19/// Returns true if this modified ast.
20///
21/// # Note
22///
23/// This method returns `!e` if `!!e` is given as a argument.
24///
25/// TODO: Handle special cases like !1 or !0
26pub(super) fn negate(expr_ctx: ExprCtx, e: &mut Expr, in_bool_ctx: bool, is_ret_val_ignored: bool) {
27    negate_inner(expr_ctx, e, in_bool_ctx, is_ret_val_ignored);
28}
29
30fn negate_inner(
31    expr_ctx: ExprCtx,
32    e: &mut Expr,
33    in_bool_ctx: bool,
34    is_ret_val_ignored: bool,
35) -> bool {
36    #[cfg(feature = "debug")]
37    let start_str = dump(&*e, false);
38
39    match e {
40        Expr::Bin(bin @ BinExpr { op: op!("=="), .. })
41        | Expr::Bin(bin @ BinExpr { op: op!("!="), .. })
42        | Expr::Bin(bin @ BinExpr { op: op!("==="), .. })
43        | Expr::Bin(bin @ BinExpr { op: op!("!=="), .. }) => {
44            bin.op = match bin.op {
45                op!("==") => {
46                    op!("!=")
47                }
48                op!("!=") => {
49                    op!("==")
50                }
51                op!("===") => {
52                    op!("!==")
53                }
54                op!("!==") => {
55                    op!("===")
56                }
57                _ => {
58                    unreachable!()
59                }
60            };
61            report_change!("negate: binary");
62            return true;
63        }
64
65        Expr::Bin(BinExpr {
66            left,
67            right,
68            op: op @ op!("&&"),
69            ..
70        }) if is_ok_to_negate_rhs(expr_ctx, right) => {
71            trace_op!("negate: a && b => !a || !b");
72
73            let a = negate_inner(expr_ctx, left, in_bool_ctx || is_ret_val_ignored, false);
74            let b = negate_inner(expr_ctx, right, in_bool_ctx, is_ret_val_ignored);
75            *op = op!("||");
76            return a || b;
77        }
78
79        Expr::Bin(BinExpr {
80            left,
81            right,
82            op: op @ op!("||"),
83            ..
84        }) if is_ok_to_negate_rhs(expr_ctx, right) => {
85            trace_op!("negate: a || b => !a && !b");
86
87            let a = negate_inner(expr_ctx, left, in_bool_ctx || is_ret_val_ignored, false);
88            let b = negate_inner(expr_ctx, right, in_bool_ctx, is_ret_val_ignored);
89            *op = op!("&&");
90            return a || b;
91        }
92
93        Expr::Cond(CondExpr { cons, alt, .. })
94            if is_ok_to_negate_for_cond(cons) && is_ok_to_negate_for_cond(alt) =>
95        {
96            trace_op!("negate: cond");
97
98            let a = negate_inner(expr_ctx, cons, in_bool_ctx, false);
99            let b = negate_inner(expr_ctx, alt, in_bool_ctx, is_ret_val_ignored);
100            return a || b;
101        }
102
103        Expr::Seq(SeqExpr { exprs, .. }) => {
104            if let Some(last) = exprs.last_mut() {
105                trace_op!("negate: seq");
106
107                return negate_inner(expr_ctx, last, in_bool_ctx, is_ret_val_ignored);
108            }
109        }
110
111        _ => {}
112    }
113
114    let mut arg = Box::new(e.take());
115
116    if let Expr::Unary(UnaryExpr {
117        op: op!("!"), arg, ..
118    }) = &mut *arg
119    {
120        match &mut **arg {
121            Expr::Unary(UnaryExpr { op: op!("!"), .. }) => {
122                report_change!("negate: !!bool => !bool");
123                *e = *arg.take();
124                return true;
125            }
126            Expr::Bin(BinExpr { op: op!("in"), .. })
127            | Expr::Bin(BinExpr {
128                op: op!("instanceof"),
129                ..
130            }) => {
131                report_change!("negate: !bool => bool");
132                *e = *arg.take();
133                return true;
134            }
135            _ => {
136                if in_bool_ctx {
137                    report_change!("negate: !expr => expr (in bool context)");
138                    *e = *arg.take();
139                    return true;
140                }
141
142                if is_ret_val_ignored {
143                    report_change!("negate: !expr => expr (return value ignored)");
144                    *e = *arg.take();
145                    return true;
146                }
147            }
148        }
149    }
150
151    if is_ret_val_ignored {
152        log_abort!("negate: noop because it's ignored");
153        *e = *arg;
154
155        false
156    } else {
157        report_change!("negate: e => !e");
158
159        *e = UnaryExpr {
160            span: DUMMY_SP,
161            op: op!("!"),
162            arg,
163        }
164        .into();
165
166        dump_change_detail!("Negated `{}` as `{}`", start_str, dump(&*e, false));
167
168        true
169    }
170}
171
172pub(crate) fn is_ok_to_negate_for_cond(e: &Expr) -> bool {
173    !matches!(e, Expr::Update(..))
174}
175
176pub(crate) fn is_ok_to_negate_rhs(expr_ctx: ExprCtx, rhs: &Expr) -> bool {
177    match rhs {
178        Expr::Member(..) => true,
179        Expr::Bin(BinExpr {
180            op: op!("===") | op!("!==") | op!("==") | op!("!="),
181            ..
182        }) => true,
183
184        Expr::Call(..) | Expr::New(..) => false,
185
186        Expr::Update(..) => false,
187
188        Expr::Bin(BinExpr {
189            op: op!("&&") | op!("||"),
190            left,
191            right,
192            ..
193        }) => is_ok_to_negate_rhs(expr_ctx, left) && is_ok_to_negate_rhs(expr_ctx, right),
194
195        Expr::Bin(BinExpr { left, right, .. }) => {
196            is_ok_to_negate_rhs(expr_ctx, left) && is_ok_to_negate_rhs(expr_ctx, right)
197        }
198
199        Expr::Assign(e) => is_ok_to_negate_rhs(expr_ctx, &e.right),
200
201        Expr::Unary(UnaryExpr {
202            op: op!("!") | op!("delete"),
203            ..
204        }) => true,
205
206        Expr::Seq(e) => {
207            if let Some(last) = e.exprs.last() {
208                is_ok_to_negate_rhs(expr_ctx, last)
209            } else {
210                true
211            }
212        }
213
214        Expr::Cond(e) => {
215            is_ok_to_negate_rhs(expr_ctx, &e.cons) && is_ok_to_negate_rhs(expr_ctx, &e.alt)
216        }
217
218        _ => {
219            if !rhs.may_have_side_effects(expr_ctx) {
220                return true;
221            }
222
223            #[cfg(feature = "debug")]
224            {
225                tracing::warn!("unimplemented: is_ok_to_negate_rhs: `{}`", dump(rhs, false));
226            }
227
228            false
229        }
230    }
231}
232
233/// A negative value means that it's efficient to negate the expression.
234#[cfg_attr(
235    feature = "debug",
236    tracing::instrument(level = "debug", skip(e, expr_ctx))
237)]
238#[allow(clippy::let_and_return)]
239pub(crate) fn negate_cost(
240    expr_ctx: ExprCtx,
241    e: &Expr,
242    in_bool_ctx: bool,
243    is_ret_val_ignored: bool,
244) -> isize {
245    #[allow(clippy::only_used_in_recursion)]
246    #[cfg_attr(test, tracing::instrument(level = "debug", skip(e)))]
247    fn cost(
248        expr_ctx: ExprCtx,
249        e: &Expr,
250        in_bool_ctx: bool,
251        bin_op: Option<BinaryOp>,
252        is_ret_val_ignored: bool,
253    ) -> isize {
254        let cost = (|| {
255            match e {
256                Expr::Unary(UnaryExpr {
257                    op: op!("!"), arg, ..
258                }) => {
259                    // TODO: Check if this argument is actually start of line.
260                    if let Expr::Call(CallExpr {
261                        callee: Callee::Expr(callee),
262                        ..
263                    }) = &**arg
264                    {
265                        if let Expr::Fn(..) = &**callee {
266                            return 0;
267                        }
268                    }
269
270                    match &**arg {
271                        Expr::Bin(BinExpr {
272                            op: op!("&&") | op!("||"),
273                            ..
274                        }) => {}
275                        _ => {
276                            if in_bool_ctx {
277                                let c = -cost(expr_ctx, arg, true, None, is_ret_val_ignored);
278                                return c.min(-1);
279                            }
280                        }
281                    }
282
283                    match &**arg {
284                        Expr::Unary(UnaryExpr { op: op!("!"), .. }) => -1,
285
286                        _ => {
287                            if in_bool_ctx {
288                                -1
289                            } else {
290                                1
291                            }
292                        }
293                    }
294                }
295                Expr::Bin(BinExpr {
296                    op: op!("===") | op!("!==") | op!("==") | op!("!="),
297                    ..
298                }) => 0,
299
300                Expr::Bin(BinExpr {
301                    op: op @ op!("||") | op @ op!("&&"),
302                    left,
303                    right,
304                    ..
305                }) => {
306                    let l_cost = cost(
307                        expr_ctx,
308                        left,
309                        in_bool_ctx || is_ret_val_ignored,
310                        Some(*op),
311                        false,
312                    );
313
314                    if !is_ret_val_ignored && !is_ok_to_negate_rhs(expr_ctx, right) {
315                        return l_cost + 3;
316                    }
317                    l_cost + cost(expr_ctx, right, in_bool_ctx, Some(*op), is_ret_val_ignored)
318                }
319
320                Expr::Cond(CondExpr { cons, alt, .. })
321                    if is_ok_to_negate_for_cond(cons) && is_ok_to_negate_for_cond(alt) =>
322                {
323                    cost(expr_ctx, cons, in_bool_ctx, bin_op, is_ret_val_ignored)
324                        + cost(expr_ctx, alt, in_bool_ctx, bin_op, is_ret_val_ignored)
325                }
326
327                Expr::Cond(..)
328                | Expr::Update(..)
329                | Expr::Bin(BinExpr {
330                    op: op!("in") | op!("instanceof"),
331                    ..
332                }) => 3,
333
334                Expr::Assign(..) => {
335                    if is_ret_val_ignored {
336                        0
337                    } else {
338                        3
339                    }
340                }
341
342                Expr::Seq(e) => {
343                    if let Some(last) = e.exprs.last() {
344                        return cost(expr_ctx, last, in_bool_ctx, bin_op, is_ret_val_ignored);
345                    }
346
347                    isize::from(!is_ret_val_ignored)
348                }
349
350                _ => isize::from(!is_ret_val_ignored),
351            }
352        })();
353
354        cost
355    }
356
357    let cost = cost(expr_ctx, e, in_bool_ctx, None, is_ret_val_ignored);
358
359    #[cfg(feature = "debug")]
360    trace_op!("negate_cost of `{}`: {}", dump(e, false), cost);
361
362    cost
363}
364
365pub(crate) fn is_pure_undefined(expr_ctx: ExprCtx, e: &Expr) -> bool {
366    match e {
367        Expr::Unary(UnaryExpr {
368            op: UnaryOp::Void,
369            arg,
370            ..
371        }) if !arg.may_have_side_effects(expr_ctx) => true,
372
373        _ => e.is_undefined(expr_ctx),
374    }
375}
376
377pub(crate) fn is_primitive(expr_ctx: ExprCtx, e: &Expr) -> Option<&Expr> {
378    if is_pure_undefined(expr_ctx, e) {
379        Some(e)
380    } else {
381        match e {
382            Expr::Lit(Lit::Regex(_)) => None,
383            Expr::Lit(_) => Some(e),
384            _ => None,
385        }
386    }
387}
388
389pub(crate) fn is_valid_identifier(s: &str, ascii_only: bool) -> bool {
390    if ascii_only && !s.is_ascii() {
391        return false;
392    }
393    s.starts_with(Ident::is_valid_start)
394        && s.chars().skip(1).all(Ident::is_valid_continue)
395        && !s.is_reserved()
396}
397
398pub(crate) fn is_directive(e: &Stmt) -> bool {
399    match e {
400        Stmt::Expr(s) => match &*s.expr {
401            Expr::Lit(Lit::Str(Str { value, .. })) => value.starts_with("use "),
402            _ => false,
403        },
404        _ => false,
405    }
406}
407
408pub(crate) fn is_pure_undefined_or_null(expr_ctx: ExprCtx, e: &Expr) -> bool {
409    is_pure_undefined(expr_ctx, e) || matches!(e, Expr::Lit(Lit::Null(..)))
410}
411
412pub(crate) fn eval_to_undefined(expr_ctx: ExprCtx, e: &Expr) -> bool {
413    match e {
414        Expr::Unary(UnaryExpr {
415            op: UnaryOp::Void, ..
416        }) => true,
417        Expr::Seq(s) => eval_to_undefined(expr_ctx, s.exprs.last().as_ref().unwrap()),
418        Expr::Cond(c) => {
419            eval_to_undefined(expr_ctx, &c.cons) && eval_to_undefined(expr_ctx, &c.alt)
420        }
421
422        _ => e.is_undefined(expr_ctx),
423    }
424}
425
426/// This method does **not** modifies `e`.
427///
428/// This method is used to test if a whole call can be replaced, while
429/// preserving standalone constants.
430pub(crate) fn eval_as_number(expr_ctx: ExprCtx, e: &Expr) -> Option<f64> {
431    match e {
432        Expr::Bin(BinExpr {
433            op: op!(bin, "-"),
434            left,
435            right,
436            ..
437        }) => {
438            let l = eval_as_number(expr_ctx, left)?;
439            let r = eval_as_number(expr_ctx, right)?;
440
441            return Some(l - r);
442        }
443
444        Expr::Call(CallExpr {
445            callee: Callee::Expr(callee),
446            args,
447            ..
448        }) => {
449            for arg in args {
450                if arg.spread.is_some() || arg.expr.may_have_side_effects(expr_ctx) {
451                    return None;
452                }
453            }
454
455            if let Expr::Member(MemberExpr {
456                obj,
457                prop: MemberProp::Ident(prop),
458                ..
459            }) = &**callee
460            {
461                match &**obj {
462                    Expr::Ident(obj) if &*obj.sym == "Math" => match &*prop.sym {
463                        "cos" => {
464                            let v = eval_as_number(expr_ctx, &args.first()?.expr)?;
465
466                            return Some(v.cos());
467                        }
468                        "sin" => {
469                            let v = eval_as_number(expr_ctx, &args.first()?.expr)?;
470
471                            return Some(v.sin());
472                        }
473
474                        "max" => {
475                            let mut numbers = Vec::new();
476                            for arg in args {
477                                let v = eval_as_number(expr_ctx, &arg.expr)?;
478                                if v.is_infinite() || v.is_nan() {
479                                    return None;
480                                }
481                                numbers.push(v);
482                            }
483
484                            return Some(
485                                numbers
486                                    .into_iter()
487                                    .max_by(|&a, &b| cmp_num(a, b))
488                                    .unwrap_or(f64::NEG_INFINITY),
489                            );
490                        }
491
492                        "min" => {
493                            let mut numbers = Vec::new();
494                            for arg in args {
495                                let v = eval_as_number(expr_ctx, &arg.expr)?;
496                                if v.is_infinite() || v.is_nan() {
497                                    return None;
498                                }
499                                numbers.push(v);
500                            }
501
502                            return Some(
503                                numbers
504                                    .into_iter()
505                                    .min_by(|&a, &b| cmp_num(a, b))
506                                    .unwrap_or(f64::INFINITY),
507                            );
508                        }
509
510                        "pow" => {
511                            if args.len() != 2 {
512                                return None;
513                            }
514                            let base: JsNumber = eval_as_number(expr_ctx, &args[0].expr)?.into();
515                            let exponent: JsNumber =
516                                eval_as_number(expr_ctx, &args[1].expr)?.into();
517
518                            return Some(base.pow(exponent).into());
519                        }
520
521                        _ => {}
522                    },
523                    _ => {}
524                }
525            }
526        }
527
528        Expr::Member(MemberExpr {
529            obj,
530            prop: MemberProp::Ident(prop),
531            ..
532        }) => match &**obj {
533            Expr::Ident(obj) if &*obj.sym == "Math" => match &*prop.sym {
534                "PI" => return Some(f64::consts::PI),
535                "E" => return Some(f64::consts::E),
536                "LN10" => return Some(f64::consts::LN_10),
537                _ => {}
538            },
539            _ => {}
540        },
541
542        _ => {
543            if let Value::Known(v) = e.as_pure_number(expr_ctx) {
544                return Some(v);
545            }
546        }
547    }
548
549    None
550}
551
552pub(crate) fn is_ident_used_by<N>(id: &Ident, node: &N) -> bool
553where
554    N: for<'aa> VisitWith<IdentUsageFinder<'aa>>,
555{
556    IdentUsageFinder::find(id, node)
557}
558
559pub struct ExprReplacer<F>
560where
561    F: FnMut(&mut Expr),
562{
563    op: F,
564}
565
566impl<F> VisitMut for ExprReplacer<F>
567where
568    F: FnMut(&mut Expr),
569{
570    noop_visit_mut_type!(fail);
571
572    fn visit_mut_expr(&mut self, e: &mut Expr) {
573        e.visit_mut_children_with(self);
574
575        (self.op)(e);
576    }
577}
578
579pub fn replace_expr<N, F>(node: &mut N, op: F)
580where
581    N: VisitMutWith<ExprReplacer<F>>,
582    F: FnMut(&mut Expr),
583{
584    node.visit_mut_with(&mut ExprReplacer { op })
585}
586
587pub(super) fn is_fine_for_if_cons(s: &Stmt) -> bool {
588    match s {
589        Stmt::Decl(Decl::Fn(FnDecl {
590            ident: Ident { sym, .. },
591            ..
592        })) if &**sym == "undefined" => false,
593
594        Stmt::Decl(Decl::Var(v))
595            if matches!(
596                &**v,
597                VarDecl {
598                    kind: VarDeclKind::Var,
599                    ..
600                }
601            ) =>
602        {
603            true
604        }
605        Stmt::Decl(Decl::Fn(..)) => true,
606        Stmt::Decl(..) => false,
607        _ => true,
608    }
609}
610
611pub(super) fn drop_invalid_stmts<T>(stmts: &mut Vec<T>)
612where
613    T: ModuleItemExt,
614{
615    stmts.retain(|s| match s.as_module_decl() {
616        Ok(s) => match s {
617            ModuleDecl::ExportDecl(ExportDecl {
618                decl: Decl::Var(v), ..
619            }) => !v.decls.is_empty(),
620            _ => true,
621        },
622        Err(s) => match s {
623            Stmt::Empty(..) => false,
624            Stmt::Decl(Decl::Var(v)) => !v.decls.is_empty(),
625            _ => true,
626        },
627    });
628}
629
630#[derive(Debug, Default)]
631pub(super) struct UnreachableHandler {
632    vars: Vec<Ident>,
633    in_var_name: bool,
634    in_hoisted_var: bool,
635}
636
637impl UnreachableHandler {
638    /// Assumes `s` is not reachable, and preserves variable declarations and
639    /// function declarations in `s`.
640    ///
641    /// Returns true if statement is changed.
642    pub fn preserve_vars(s: &mut Stmt) -> bool {
643        if s.is_empty() {
644            return false;
645        }
646        if let Stmt::Decl(Decl::Var(v)) = s {
647            let mut changed = false;
648            for decl in &mut v.decls {
649                if decl.init.is_some() {
650                    decl.init = None;
651                    changed = true;
652                }
653            }
654
655            return changed;
656        }
657
658        let mut v = Self::default();
659        s.visit_mut_with(&mut v);
660        if v.vars.is_empty() {
661            *s = EmptyStmt { span: DUMMY_SP }.into();
662        } else {
663            *s = VarDecl {
664                span: DUMMY_SP,
665                kind: VarDeclKind::Var,
666                declare: false,
667                decls: v
668                    .vars
669                    .into_iter()
670                    .map(BindingIdent::from)
671                    .map(Pat::Ident)
672                    .map(|name| VarDeclarator {
673                        span: DUMMY_SP,
674                        name,
675                        init: None,
676                        definite: false,
677                    })
678                    .collect(),
679                ..Default::default()
680            }
681            .into()
682        }
683
684        true
685    }
686}
687
688impl VisitMut for UnreachableHandler {
689    noop_visit_mut_type!(fail);
690
691    fn visit_mut_arrow_expr(&mut self, _: &mut ArrowExpr) {}
692
693    fn visit_mut_fn_decl(&mut self, n: &mut FnDecl) {
694        self.vars.push(n.ident.clone());
695
696        n.function.visit_mut_with(self);
697    }
698
699    fn visit_mut_function(&mut self, _: &mut Function) {}
700
701    fn visit_mut_pat(&mut self, n: &mut Pat) {
702        n.visit_mut_children_with(self);
703
704        if self.in_var_name && self.in_hoisted_var {
705            if let Pat::Ident(i) = n {
706                self.vars.push(i.id.clone());
707            }
708        }
709    }
710
711    fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
712        self.in_hoisted_var = n.kind == VarDeclKind::Var;
713        n.visit_mut_children_with(self);
714    }
715
716    fn visit_mut_var_declarator(&mut self, n: &mut VarDeclarator) {
717        self.in_var_name = true;
718        n.name.visit_mut_with(self);
719        self.in_var_name = false;
720        n.init.visit_mut_with(self);
721    }
722}
723
724// TODO: remove
725pub(crate) fn contains_super<N>(body: &N) -> bool
726where
727    N: VisitWith<SuperFinder>,
728{
729    let mut visitor = SuperFinder { found: false };
730    body.visit_with(&mut visitor);
731    visitor.found
732}
733
734pub struct SuperFinder {
735    found: bool,
736}
737
738impl Visit for SuperFinder {
739    noop_visit_type!(fail);
740
741    /// Don't recurse into constructor
742    fn visit_constructor(&mut self, _: &Constructor) {}
743
744    /// Don't recurse into fn
745    fn visit_function(&mut self, _: &Function) {}
746
747    fn visit_prop(&mut self, n: &Prop) {
748        n.visit_children_with(self);
749
750        if let Prop::Shorthand(Ident { sym, .. }) = n {
751            if &**sym == "arguments" {
752                self.found = true;
753            }
754        }
755    }
756
757    fn visit_super(&mut self, _: &Super) {
758        self.found = true;
759    }
760}
761
762fn cmp_num(a: f64, b: f64) -> Ordering {
763    if a == 0.0 && a.is_sign_negative() && b == 0.0 && b.is_sign_positive() {
764        return Ordering::Less;
765    }
766
767    if a == 0.0 && a.is_sign_positive() && b == 0.0 && b.is_sign_negative() {
768        return Ordering::Greater;
769    }
770
771    a.partial_cmp(&b).unwrap()
772}
773
774pub(crate) fn is_eq(op: BinaryOp) -> bool {
775    matches!(op, op!("==") | op!("===") | op!("!=") | op!("!=="))
776}
777
778pub(crate) fn can_absorb_negate(e: &Expr, expr_ctx: ExprCtx) -> bool {
779    match e {
780        Expr::Lit(_) => true,
781        Expr::Bin(BinExpr {
782            op: op!("&&") | op!("||"),
783            left,
784            right,
785            ..
786        }) => can_absorb_negate(left, expr_ctx) && can_absorb_negate(right, expr_ctx),
787        Expr::Bin(BinExpr { op, .. }) if is_eq(*op) => true,
788        Expr::Unary(UnaryExpr {
789            op: op!("!"), arg, ..
790        }) => arg.get_type(expr_ctx) == Value::Known(Type::Bool),
791        _ => false,
792    }
793}