swc_ecma_minifier/compress/pure/
dead_code.rs

1use par_iter::prelude::*;
2use swc_common::{util::take::Take, EqIgnoreSpan, Spanned, DUMMY_SP};
3use swc_ecma_ast::*;
4use swc_ecma_utils::{extract_var_ids, ExprCtx, ExprExt, StmtExt, StmtLike, Value};
5use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
6
7use super::Pure;
8use crate::{
9    compress::util::is_fine_for_if_cons,
10    maybe_par,
11    util::{make_bool, ModuleItemExt},
12};
13
14/// Methods related to option `dead_code`.
15impl Pure<'_> {
16    pub(super) fn simplify_assign_expr(&mut self, e: &mut Expr) {
17        match e {
18            Expr::Assign(AssignExpr {
19                op: op!("="),
20                left: AssignTarget::Simple(l),
21                right: r,
22                ..
23            }) if match &*l {
24                SimpleAssignTarget::Ident(l) => match &**r {
25                    Expr::Ident(r) => l.sym == r.sym && l.ctxt == r.ctxt,
26                    _ => false,
27                },
28                _ => false,
29            } =>
30            {
31                report_change!("Dropping assignment to the same variable");
32                self.changed = true;
33                *e = r.take().ident().unwrap().into();
34            }
35
36            Expr::Assign(AssignExpr {
37                op: op!("="),
38                left: AssignTarget::Pat(left),
39                right,
40                ..
41            }) if match &*left {
42                AssignTargetPat::Array(arr) => {
43                    arr.elems.is_empty() || arr.elems.iter().all(|v| v.is_none())
44                }
45                _ => false,
46            } =>
47            {
48                report_change!("Dropping assignment to an empty array pattern");
49                self.changed = true;
50                *e = *right.take();
51            }
52
53            Expr::Assign(AssignExpr {
54                op: op!("="),
55                left: AssignTarget::Pat(left),
56                right,
57                ..
58            }) if match &*left {
59                AssignTargetPat::Object(obj) => obj.props.is_empty(),
60                _ => false,
61            } =>
62            {
63                report_change!("Dropping assignment to an empty object pattern");
64                self.changed = true;
65                *e = *right.take();
66            }
67
68            _ => {}
69        }
70    }
71
72    ///
73    ///  - Removes `L1: break L1`
74    pub(super) fn handle_instant_break(&mut self, s: &mut Stmt) {
75        if let Stmt::Labeled(ls) = s {
76            match &*ls.body {
77                Stmt::Break(BreakStmt {
78                    label: Some(label), ..
79                }) => {
80                    if label.sym == ls.label.sym {
81                        self.changed = true;
82                        report_change!("Dropping instant break `{}`", label);
83                        *s = Stmt::dummy();
84                    }
85                }
86
87                Stmt::Break(BreakStmt { label: None, .. }) => {
88                    self.changed = true;
89                    report_change!("Dropping instant break without label");
90                    *s = *ls.body.take();
91                }
92
93                _ => (),
94            }
95        }
96    }
97
98    /// # Operations
99    ///
100    ///
101    ///  - Convert if break to conditionals
102    ///
103    /// ```js
104    /// out: {
105    ///     if (foo) break out;
106    ///     console.log("bar");
107    /// }
108    /// ```
109    ///
110    /// =>
111    ///
112    /// ```js
113    /// foo || console.log("bar");
114    /// ```
115    pub(super) fn optimize_labeled_stmt(&mut self, s: &mut Stmt) -> Option<()> {
116        if !self.options.dead_code {
117            return None;
118        }
119
120        if let Stmt::Labeled(ls) = s {
121            if ls.body.is_empty() {
122                self.changed = true;
123                report_change!("Dropping an empty label statement: `{}`", ls.label);
124                *s = Stmt::dummy();
125                return None;
126            }
127
128            if let Stmt::Block(bs) = &mut *ls.body {
129                let first = bs.stmts.first_mut()?;
130
131                if let Stmt::If(IfStmt {
132                    test,
133                    cons,
134                    alt: None,
135                    ..
136                }) = first
137                {
138                    if let Stmt::Break(BreakStmt {
139                        label: Some(label), ..
140                    }) = &**cons
141                    {
142                        if ls.label.sym == label.sym {
143                            self.changed = true;
144                            report_change!(
145                                "Optimizing labeled stmt with a break to if statement: `{}`",
146                                label
147                            );
148
149                            self.negate(test, true, false);
150                            let test = test.take();
151
152                            let mut cons = bs.take();
153                            cons.stmts.remove(0);
154
155                            ls.body = Box::new(
156                                IfStmt {
157                                    span: ls.span,
158                                    test,
159                                    cons: Box::new(Stmt::Block(cons)),
160                                    alt: None,
161                                }
162                                .into(),
163                            );
164                            return None;
165                        }
166                    }
167                }
168
169                if let Stmt::If(IfStmt {
170                    test,
171                    cons,
172                    alt: Some(alt),
173                    ..
174                }) = first
175                {
176                    if let Stmt::Break(BreakStmt {
177                        label: Some(label), ..
178                    }) = &**alt
179                    {
180                        if ls.label.sym == label.sym {
181                            self.changed = true;
182                            report_change!(
183                                "Optimizing labeled stmt with a break in alt to if statement: {}",
184                                ls.label
185                            );
186
187                            let test = test.take();
188                            let cons = *cons.take();
189
190                            let mut new_cons = bs.take();
191                            new_cons.stmts[0] = cons;
192
193                            ls.body = Box::new(
194                                IfStmt {
195                                    span: ls.span,
196                                    test,
197                                    cons: Box::new(Stmt::Block(new_cons)),
198                                    alt: None,
199                                }
200                                .into(),
201                            );
202                            return None;
203                        }
204                    }
205                }
206            }
207        }
208
209        None
210    }
211
212    /// Remove the last statement of a loop if it's continue
213    pub(super) fn drop_useless_continue(&mut self, s: &mut Stmt) {
214        match s {
215            Stmt::Labeled(ls) => {
216                let new = self.drop_useless_continue_inner(Some(ls.label.clone()), &mut ls.body);
217                if let Some(new) = new {
218                    *s = new;
219                }
220            }
221
222            _ => {
223                let new = self.drop_useless_continue_inner(None, s);
224                if let Some(new) = new {
225                    *s = new;
226                }
227            }
228        }
229    }
230
231    /// Returns [Some] if the whole statement should be replaced
232    fn drop_useless_continue_inner(
233        &mut self,
234        label: Option<Ident>,
235        loop_stmt: &mut Stmt,
236    ) -> Option<Stmt> {
237        let body = match loop_stmt {
238            Stmt::While(ws) => &mut *ws.body,
239            Stmt::For(fs) => &mut *fs.body,
240            Stmt::ForIn(fs) => &mut *fs.body,
241            Stmt::ForOf(fs) => &mut *fs.body,
242            _ => return None,
243        };
244
245        if let Stmt::Block(b) = body {
246            let last = b.stmts.last_mut()?;
247
248            if let Stmt::Continue(last_cs) = last {
249                if last_cs.label.is_some() {
250                    if label.eq_ignore_span(&last_cs.label) {
251                    } else {
252                        return None;
253                    }
254                }
255            } else {
256                return None;
257            }
258            self.changed = true;
259            report_change!("Remove useless continue (last stmt of a loop)");
260            b.stmts.remove(b.stmts.len() - 1);
261
262            if let Some(label) = &label {
263                if !contains_label(b, label) {
264                    return Some(loop_stmt.take());
265                }
266            }
267        }
268
269        None
270    }
271
272    pub(super) fn drop_unreachable_stmts<T>(&mut self, stmts: &mut Vec<T>)
273    where
274        T: StmtLike + ModuleItemExt + Take,
275    {
276        if !self.options.dead_code && !self.options.if_return {
277            return;
278        }
279
280        let idx = stmts.iter().position(|stmt| match stmt.as_stmt() {
281            Some(s) => s.terminates(),
282            _ => false,
283        });
284
285        // TODO: let chain
286        if let Some(idx) = idx {
287            self.drop_duplicate_terminate(&mut stmts[..=idx]);
288
289            // Return at the last is fine
290            if idx == stmts.len() - 1 {
291                return;
292            }
293
294            // If only function declarations are left, we should not proceed
295            if stmts
296                .iter()
297                .skip(idx + 1)
298                .all(|s| matches!(s.as_stmt(), Some(Stmt::Decl(Decl::Fn(_)))))
299            {
300                if let Some(Stmt::Return(ReturnStmt { arg: None, .. })) = stmts[idx].as_stmt() {
301                    // Remove last return
302                    stmts.remove(idx);
303                }
304
305                return;
306            }
307
308            self.changed = true;
309
310            report_change!("Dropping statements after a control keyword");
311
312            let stmts_len = stmts.len();
313
314            // Hoist function and `var` declarations above return.
315            let (decls, hoisted_fns, mut new_stmts) = stmts.iter_mut().skip(idx + 1).fold(
316                (
317                    Vec::with_capacity(stmts_len),
318                    Vec::<T>::with_capacity(stmts_len),
319                    Vec::with_capacity(stmts_len),
320                ),
321                |(mut decls, mut hoisted_fns, mut new_stmts), stmt| {
322                    match stmt.take().try_into_stmt() {
323                        Ok(Stmt::Decl(Decl::Fn(f))) => {
324                            hoisted_fns.push(T::from(Stmt::from(f)));
325                        }
326                        Ok(t) => {
327                            let ids = extract_var_ids(&t).into_iter().map(|i| VarDeclarator {
328                                span: i.span,
329                                name: i.into(),
330                                init: None,
331                                definite: false,
332                            });
333                            decls.extend(ids);
334                        }
335                        Err(item) => new_stmts.push(item),
336                    };
337                    (decls, hoisted_fns, new_stmts)
338                },
339            );
340
341            if !decls.is_empty() {
342                new_stmts.push(T::from(Stmt::from(VarDecl {
343                    span: DUMMY_SP,
344                    kind: VarDeclKind::Var,
345                    decls,
346                    declare: false,
347                    ..Default::default()
348                })));
349            }
350
351            new_stmts.extend(stmts.drain(..=idx));
352
353            new_stmts.extend(hoisted_fns);
354
355            *stmts = new_stmts;
356        }
357    }
358
359    fn drop_duplicate_terminate<T: StmtLike>(&mut self, stmts: &mut [T]) {
360        let (last, stmts) = stmts.split_last_mut().unwrap();
361
362        let last = match last.as_stmt() {
363            Some(s @ (Stmt::Break(_) | Stmt::Continue(_) | Stmt::Return(_) | Stmt::Throw(_))) => s,
364            _ => return,
365        };
366
367        fn drop<T: StmtLike>(stmt: &mut T, last: &Stmt, need_break: bool, ctx: ExprCtx) -> bool {
368            match stmt.as_stmt_mut() {
369                Some(s) if s.eq_ignore_span(last) => {
370                    if need_break {
371                        *s = BreakStmt {
372                            label: None,
373                            span: s.span(),
374                        }
375                        .into();
376                    } else {
377                        s.take();
378                    }
379                    true
380                }
381                Some(Stmt::If(i)) => {
382                    let mut changed = false;
383                    changed |= drop(&mut *i.cons, last, need_break, ctx);
384                    if let Some(alt) = i.alt.as_mut() {
385                        changed |= drop(&mut **alt, last, need_break, ctx);
386                    }
387                    changed
388                }
389                Some(Stmt::Try(t)) => {
390                    let mut changed = false;
391                    // TODO: let chain
392                    if let Some(stmt) = t.block.stmts.last_mut() {
393                        let side_effect = match last {
394                            Stmt::Break(_) | Stmt::Continue(_) => false,
395                            Stmt::Return(ReturnStmt { arg: None, .. }) => false,
396                            Stmt::Return(ReturnStmt { arg: Some(arg), .. }) => {
397                                arg.may_have_side_effects(ctx)
398                            }
399                            Stmt::Throw(_) => true,
400                            _ => unreachable!(),
401                        };
402                        if t.finalizer.is_none() && !side_effect {
403                            changed |= drop(stmt, last, need_break, ctx)
404                        }
405                    }
406                    if let Some(h) = t.handler.as_mut() {
407                        if let Some(stmt) = h.body.stmts.last_mut() {
408                            changed |= drop(stmt, last, need_break, ctx);
409                        }
410                    }
411                    if let Some(f) = t.finalizer.as_mut() {
412                        if let Some(stmt) = f.stmts.last_mut() {
413                            changed |= drop(stmt, last, need_break, ctx);
414                        }
415                    }
416                    changed
417                }
418                Some(Stmt::Switch(s)) if !last.is_break_stmt() && !need_break => {
419                    let mut changed = false;
420                    for case in s.cases.iter_mut() {
421                        for stmt in case.cons.iter_mut() {
422                            changed |= drop(stmt, last, true, ctx);
423                        }
424                    }
425
426                    changed
427                }
428                Some(
429                    Stmt::For(ForStmt { body, .. })
430                    | Stmt::ForIn(ForInStmt { body, .. })
431                    | Stmt::ForOf(ForOfStmt { body, .. })
432                    | Stmt::While(WhileStmt { body, .. })
433                    | Stmt::DoWhile(DoWhileStmt { body, .. }),
434                ) if !last.is_break_stmt() && !last.is_continue_stmt() && !need_break => {
435                    if let Stmt::Block(b) = &mut **body {
436                        let mut changed = false;
437                        for stmt in b.stmts.iter_mut() {
438                            changed |= drop(stmt, last, true, ctx);
439                        }
440                        changed
441                    } else {
442                        drop(&mut **body, last, true, ctx)
443                    }
444                }
445                Some(Stmt::Block(b)) => {
446                    if let Some(stmt) = b.stmts.last_mut() {
447                        drop(stmt, last, need_break, ctx)
448                    } else {
449                        false
450                    }
451                }
452                _ => false,
453            }
454        }
455
456        if let Some(before_last) = stmts.last_mut() {
457            if drop(before_last, last, false, self.expr_ctx) {
458                self.changed = true;
459
460                report_change!("Dropping control keyword in nested block");
461            }
462        }
463    }
464
465    pub(super) fn drop_useless_blocks<T>(&mut self, stmts: &mut Vec<T>)
466    where
467        T: StmtLike,
468    {
469        fn is_ok(b: &BlockStmt) -> bool {
470            maybe_par!(
471                b.stmts.iter().all(is_fine_for_if_cons),
472                *crate::LIGHT_TASK_PARALLELS
473            )
474        }
475
476        if maybe_par!(
477            stmts
478                .iter()
479                .all(|stmt| !matches!(stmt.as_stmt(), Some(Stmt::Block(b)) if is_ok(b))),
480            *crate::LIGHT_TASK_PARALLELS
481        ) {
482            return;
483        }
484
485        self.changed = true;
486        report_change!("Dropping useless block");
487
488        let old_stmts = stmts.take();
489
490        let new: Vec<T> = if old_stmts.len() >= *crate::LIGHT_TASK_PARALLELS {
491            old_stmts
492                .into_par_iter()
493                .flat_map(|stmt| match stmt.try_into_stmt() {
494                    Ok(v) => match v {
495                        Stmt::Block(v) if is_ok(&v) => {
496                            let stmts = v.stmts;
497                            maybe_par!(
498                                stmts.into_iter().map(T::from).collect(),
499                                *crate::LIGHT_TASK_PARALLELS
500                            )
501                        }
502                        _ => vec![T::from(v)],
503                    },
504                    Err(v) => vec![v],
505                })
506                .collect()
507        } else {
508            let mut new = Vec::with_capacity(old_stmts.len() * 2);
509            old_stmts
510                .into_iter()
511                .for_each(|stmt| match stmt.try_into_stmt() {
512                    Ok(v) => match v {
513                        Stmt::Block(v) if is_ok(&v) => {
514                            new.extend(v.stmts.into_iter().map(T::from));
515                        }
516                        _ => new.push(T::from(v)),
517                    },
518                    Err(v) => new.push(v),
519                });
520            new
521        };
522        *stmts = new;
523    }
524
525    pub(super) fn drop_unused_stmt_at_end_of_fn(&mut self, s: &mut Stmt) {
526        if let Stmt::Return(r) = s {
527            if let Some(Expr::Unary(UnaryExpr {
528                span,
529                op: op!("void"),
530                arg,
531            })) = r.arg.as_deref_mut()
532            {
533                report_change!("unused: Removing `return void` in end of a function");
534                self.changed = true;
535                *s = ExprStmt {
536                    span: *span,
537                    expr: arg.take(),
538                }
539                .into();
540            }
541        }
542    }
543
544    pub(super) fn optimize_const_if<T>(&mut self, stmts: &mut Vec<T>)
545    where
546        T: StmtLike,
547    {
548        if !self.options.unused && !self.options.dead_code {
549            return;
550        }
551
552        if !maybe_par!(
553            stmts.iter().any(|stmt| match stmt.as_stmt() {
554                Some(Stmt::If(s)) => s.test.cast_to_bool(self.expr_ctx).1.is_known(),
555                _ => false,
556            }),
557            *crate::LIGHT_TASK_PARALLELS
558        ) {
559            return;
560        }
561
562        self.changed = true;
563        report_change!("dead_code: Removing dead codes");
564
565        let mut new = Vec::with_capacity(stmts.len());
566        stmts
567            .take()
568            .into_iter()
569            .for_each(|stmt| match stmt.try_into_stmt() {
570                Ok(stmt) => match stmt {
571                    Stmt::If(mut s) => {
572                        if let Value::Known(v) = s.test.cast_to_bool(self.expr_ctx).1 {
573                            let mut var_ids = Vec::new();
574                            new.push(T::from(
575                                ExprStmt {
576                                    span: DUMMY_SP,
577                                    expr: s.test.take(),
578                                }
579                                .into(),
580                            ));
581
582                            if v {
583                                if let Some(alt) = s.alt.take() {
584                                    var_ids = alt
585                                        .extract_var_ids()
586                                        .into_iter()
587                                        .map(|name| VarDeclarator {
588                                            span: DUMMY_SP,
589                                            name: name.into(),
590                                            init: None,
591                                            definite: Default::default(),
592                                        })
593                                        .collect();
594                                }
595                                if !var_ids.is_empty() {
596                                    new.push(T::from(
597                                        VarDecl {
598                                            span: DUMMY_SP,
599                                            kind: VarDeclKind::Var,
600                                            declare: Default::default(),
601                                            decls: var_ids,
602                                            ..Default::default()
603                                        }
604                                        .into(),
605                                    ))
606                                }
607                                new.push(T::from(*s.cons.take()));
608                            } else {
609                                var_ids = s
610                                    .cons
611                                    .extract_var_ids()
612                                    .into_iter()
613                                    .map(|name| VarDeclarator {
614                                        span: DUMMY_SP,
615                                        name: name.into(),
616                                        init: None,
617                                        definite: Default::default(),
618                                    })
619                                    .collect();
620                                if !var_ids.is_empty() {
621                                    new.push(T::from(
622                                        VarDecl {
623                                            span: DUMMY_SP,
624                                            kind: VarDeclKind::Var,
625                                            declare: Default::default(),
626                                            decls: var_ids,
627                                            ..Default::default()
628                                        }
629                                        .into(),
630                                    ))
631                                }
632                                if let Some(alt) = s.alt.take() {
633                                    new.push(T::from(*alt));
634                                }
635                            }
636                        } else {
637                            new.push(T::from(s.into()));
638                        }
639                    }
640                    _ => new.push(T::from(stmt)),
641                },
642                Err(stmt) => new.push(stmt),
643            });
644
645        *stmts = new;
646    }
647
648    pub(super) fn handle_known_delete(&mut self, e: &mut Expr) {
649        if !self.options.conditionals && !self.options.evaluate && !self.options.sequences() {
650            return;
651        }
652
653        let Expr::Unary(UnaryExpr {
654            op: op!("delete"),
655            arg,
656            ..
657        }) = e
658        else {
659            return;
660        };
661
662        match &**arg {
663            Expr::Ident(i) => {
664                if matches!(&*i.sym, "undefined" | "NaN" | "Infinity") {
665                    *e = make_bool(i.span, false);
666                }
667            }
668
669            Expr::Unary(..) | Expr::Bin(..) | Expr::Cond(..) => {
670                *e = make_bool(e.span(), true);
671            }
672
673            _ => (),
674        }
675    }
676}
677
678fn contains_label<N>(node: &N, label: &Ident) -> bool
679where
680    for<'aa> N: VisitWith<LabelFinder<'aa>>,
681{
682    let mut v = LabelFinder {
683        label,
684        found: false,
685    };
686    node.visit_with(&mut v);
687    v.found
688}
689
690struct LabelFinder<'a> {
691    label: &'a Ident,
692    found: bool,
693}
694impl Visit for LabelFinder<'_> {
695    noop_visit_type!(fail);
696
697    fn visit_break_stmt(&mut self, s: &BreakStmt) {
698        if let Some(label) = &s.label {
699            if label.sym == self.label.sym {
700                self.found = true;
701            }
702        }
703    }
704
705    fn visit_continue_stmt(&mut self, s: &ContinueStmt) {
706        if let Some(label) = &s.label {
707            if label.sym == self.label.sym {
708                self.found = true;
709            }
710        }
711    }
712}