swc_ecma_minifier/compress/pure/
switches.rs

1use swc_common::{util::take::Take, EqIgnoreSpan, SyntaxContext, DUMMY_SP};
2use swc_ecma_ast::*;
3use swc_ecma_utils::{extract_var_ids, prepend_stmt, ExprExt, ExprFactory, StmtExt};
4use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
5
6use super::Pure;
7use crate::{
8    compress::{
9        pure::{Ctx, DropOpts},
10        util::is_primitive,
11    },
12    util::idents_used_by,
13};
14
15/// Methods related to option `switches`.
16impl Pure<'_> {
17    pub(super) fn optimize_switch_stmt(&mut self, s: &mut Stmt) {
18        if !self.options.switches {
19            return;
20        }
21
22        let Stmt::Switch(..) = s else {
23            return;
24        };
25
26        self.remove_empty_switch(s);
27
28        self.optimize_small_switch(s);
29
30        self.optimize_const_switches(s);
31
32        self.optimize_switch_with_default_on_last(s);
33    }
34
35    fn remove_empty_switch(&mut self, s: &mut Stmt) {
36        let Stmt::Switch(sw) = s else {
37            return;
38        };
39
40        // Remove empty switch
41        if sw.cases.is_empty() {
42            self.changed = true;
43            report_change!("switches: Removing empty switch");
44
45            self.ignore_return_value(
46                &mut sw.discriminant,
47                DropOpts::DROP_NUMBER.union(DropOpts::DROP_STR_LIT),
48            );
49
50            let discriminant = sw.discriminant.take();
51            if discriminant.is_invalid() {
52                *s = Stmt::dummy();
53                return;
54            }
55
56            *s = ExprStmt {
57                span: sw.span,
58                expr: discriminant,
59            }
60            .into();
61        }
62    }
63
64    /// Handle switches in the case where we can know which branch will be
65    /// taken.
66    fn optimize_const_switches(&mut self, s: &mut Stmt) {
67        if !self.options.switches || !self.options.dead_code {
68            return;
69        }
70
71        let stmt = match s {
72            Stmt::Switch(s) => s,
73            _ => return,
74        };
75
76        // TODO: evaluate
77        fn tail_expr(e: &Expr) -> &Expr {
78            match e {
79                Expr::Seq(s) => s.exprs.last().unwrap(),
80                _ => e,
81            }
82        }
83
84        let discriminant = &mut stmt.discriminant;
85
86        let tail = if let Some(e) = is_primitive(self.expr_ctx, tail_expr(discriminant)) {
87            e
88        } else {
89            return;
90        };
91
92        let mut var_ids = Vec::new();
93        let mut cases = Vec::new();
94        let mut exact = None;
95        let mut may_match_other_than_exact = false;
96
97        for (idx, case) in stmt.cases.iter_mut().enumerate() {
98            if let Some(test) = case.test.as_ref() {
99                if let Some(e) = is_primitive(self.expr_ctx, tail_expr(test)) {
100                    if match (e, tail) {
101                        (Expr::Lit(Lit::Num(e)), Expr::Lit(Lit::Num(tail))) => {
102                            e.value == tail.value
103                        }
104                        _ => e.eq_ignore_span(tail),
105                    } {
106                        cases.push(case.take());
107
108                        exact = Some(idx);
109                        break;
110                    } else {
111                        var_ids.extend(extract_var_ids(&case.cons))
112                    }
113                } else {
114                    if !may_match_other_than_exact
115                        && !test.is_ident()
116                        && !idents_used_by(test).is_empty()
117                    {
118                        may_match_other_than_exact = true;
119                    }
120
121                    cases.push(case.take())
122                }
123            } else {
124                cases.push(case.take())
125            }
126        }
127
128        if let Some(exact) = exact {
129            let exact_case = cases.last_mut().unwrap();
130            let mut terminate = exact_case.cons.iter().rev().any(|s| s.terminates());
131            for case in stmt.cases[(exact + 1)..].iter_mut() {
132                if terminate {
133                    var_ids.extend(extract_var_ids(&case.cons))
134                } else {
135                    terminate |= case.cons.iter().rev().any(|s| s.terminates());
136                    exact_case.cons.extend(case.cons.take())
137                }
138            }
139
140            if !may_match_other_than_exact {
141                // remove default if there's an exact match
142                cases.retain(|case| {
143                    if case.test.is_some() {
144                        true
145                    } else {
146                        var_ids.extend(extract_var_ids(&case.cons));
147                        false
148                    }
149                });
150            }
151
152            if cases.len() == 2 {
153                let last = cases.last_mut().unwrap();
154
155                self.changed = true;
156                report_change!("switches: Turn exact match into default");
157                // so that following pass could turn it into if else
158                if let Some(test) = last.test.take() {
159                    prepend_stmt(&mut last.cons, test.into_stmt())
160                }
161            }
162        }
163
164        if cases.len() == stmt.cases.len() {
165            stmt.cases = cases;
166            return;
167        }
168
169        self.optimize_switch_cases(&mut cases);
170
171        let var_ids: Vec<VarDeclarator> = var_ids
172            .into_iter()
173            .map(|name| VarDeclarator {
174                span: DUMMY_SP,
175                name: name.into(),
176                init: None,
177                definite: Default::default(),
178            })
179            .collect();
180
181        self.changed = true;
182
183        if cases.len() == 1
184            && (cases[0].test.is_none() || exact.is_some())
185            && !contains_nested_break(&cases[0])
186        {
187            report_change!("switches: Removing a constant switch");
188
189            let mut stmts = Vec::new();
190
191            if !var_ids.is_empty() {
192                stmts.push(
193                    VarDecl {
194                        span: DUMMY_SP,
195                        kind: VarDeclKind::Var,
196                        declare: Default::default(),
197                        decls: var_ids,
198                        ..Default::default()
199                    }
200                    .into(),
201                )
202            }
203
204            self.ignore_return_value(
205                discriminant,
206                DropOpts::DROP_NUMBER.union(DropOpts::DROP_STR_LIT),
207            );
208
209            if !discriminant.is_invalid() {
210                stmts.push(discriminant.take().into_stmt());
211            }
212
213            let mut last = cases.pop().unwrap();
214            remove_last_break(&mut last.cons);
215
216            if let Some(mut test) = last.test {
217                // We are creating ExprStmt, so we can ignore return value
218                self.ignore_return_value(
219                    &mut test,
220                    DropOpts::DROP_NUMBER.union(DropOpts::DROP_STR_LIT),
221                );
222
223                if !test.is_invalid() {
224                    stmts.push(test.into_stmt());
225                }
226            }
227
228            stmts.extend(last.cons);
229            *s = BlockStmt {
230                stmts,
231                ..Default::default()
232            }
233            .into();
234            return;
235        }
236
237        report_change!("switches: Removing unreachable cases from a constant switch");
238        stmt.cases = cases;
239
240        if !var_ids.is_empty() {
241            *s = BlockStmt {
242                stmts: vec![
243                    VarDecl {
244                        span: DUMMY_SP,
245                        kind: VarDeclKind::Var,
246                        declare: Default::default(),
247                        decls: var_ids,
248                        ..Default::default()
249                    }
250                    .into(),
251                    s.take(),
252                ],
253                ..Default::default()
254            }
255            .into()
256        }
257    }
258
259    /// Drops useless switch cases and statements in it.
260    ///
261    /// This method will
262    ///
263    /// - drop the empty cases at the end.
264    /// - drop break at last case
265    /// - merge branch with default at the end
266    pub(super) fn optimize_switch_cases(&mut self, cases: &mut Vec<SwitchCase>) {
267        if !self.options.switches || !self.options.dead_code || cases.is_empty() {
268            return;
269        }
270
271        self.merge_cases_with_same_cons(cases);
272
273        // last case with no empty body
274        let mut last = 0;
275
276        for (idx, case) in cases.iter_mut().enumerate().rev() {
277            self.changed |= remove_last_break(&mut case.cons);
278
279            if case
280                .cons
281                .iter()
282                .any(|stmt| stmt.may_have_side_effects(self.expr_ctx) || stmt.terminates())
283            {
284                last = idx + 1;
285                break;
286            }
287        }
288
289        let has_side_effect = cases.iter().skip(last).rposition(|case| {
290            case.test
291                .as_deref()
292                .map(|test| test.may_have_side_effects(self.expr_ctx))
293                .unwrap_or(false)
294        });
295
296        if let Some(has_side_effect) = has_side_effect {
297            last += has_side_effect + 1
298        }
299
300        let default = cases.iter().position(|case| case.test.is_none());
301
302        // if default is before empty cases, we must ensure empty case is preserved
303        if last < cases.len() && default.map(|idx| idx >= last).unwrap_or(true) {
304            self.changed = true;
305            report_change!("switches: Removing empty cases at the end");
306            cases.drain(last..);
307        }
308
309        if let Some(default) = default {
310            if cases.is_empty() {
311                return;
312            }
313
314            let end = cases
315                .iter()
316                .skip(default)
317                .position(|case| {
318                    case.cons
319                        .iter()
320                        .any(|stmt| stmt.may_have_side_effects(self.expr_ctx) || stmt.terminates())
321                })
322                .unwrap_or(0)
323                + default;
324
325            if end != cases.len() - 1 {
326                return;
327            }
328            let start = cases.iter().enumerate().rposition(|(idx, case)| {
329                case.test
330                    .as_deref()
331                    .map(|test| test.may_have_side_effects(self.expr_ctx))
332                    .unwrap_or(false)
333                    || (idx != end
334                        && case.cons.iter().any(|stmt| {
335                            stmt.may_have_side_effects(self.expr_ctx) || stmt.terminates()
336                        }))
337            });
338
339            let start = start.map(|s| s + 1).unwrap_or(0);
340
341            if start <= default {
342                if start < end {
343                    cases[start].cons = cases[end].cons.take();
344                    cases.drain((start + 1)..);
345                    cases[start].test = None;
346                }
347            } else {
348                if start <= end {
349                    cases[start].cons = cases[end].cons.take();
350                    cases.drain(start..);
351                }
352            }
353        }
354    }
355
356    /// If a case ends with break but content is same with the another case
357    /// without break case order, except the break statement, we merge
358    /// them.
359    fn merge_cases_with_same_cons(&mut self, cases: &mut Vec<SwitchCase>) {
360        let mut i = 0;
361        let len = cases.len();
362
363        // may some smarter person find a better solution
364        while i < len {
365            if cases[i].cons.is_empty() {
366                i += 1;
367                continue;
368            }
369            let mut block_start = i + 1;
370            let mut cannot_cross_block = false;
371
372            for j in (i + 1)..len {
373                cannot_cross_block |= cases[j]
374                    .test
375                    .as_deref()
376                    .map(|test| is_primitive(self.expr_ctx, test).is_none())
377                    .unwrap_or(false)
378                    || !(cases[j].cons.is_empty()
379                        || cases[j].cons.iter().rev().any(|s| s.terminates())
380                        || j == cases.len() - 1);
381
382                if cases[j].cons.is_empty() {
383                    continue;
384                }
385
386                if cannot_cross_block && block_start != i + 1 {
387                    break;
388                }
389
390                block_start = j + 1;
391
392                // To merge cases, the first one should be terminate the switch statement.
393                //
394                // Otherwise fallthough will be skipped
395                let case_i_terminates = cases[i]
396                    .cons
397                    .last()
398                    .map(|s| s.terminates())
399                    .unwrap_or(false);
400
401                // first case with a body and don't cross non-primitive branch
402                let found = case_i_terminates
403                    && if j != len - 1 {
404                        cases[i].cons.eq_ignore_span(&cases[j].cons)
405                    } else {
406                        if let Some(Stmt::Break(BreakStmt { label: None, .. })) =
407                            cases[i].cons.last()
408                        {
409                            SyntaxContext::within_ignored_ctxt(|| {
410                                cases[i].cons[..(cases[i].cons.len() - 1)]
411                                    .eq_ignore_span(&cases[j].cons)
412                            })
413                        } else {
414                            SyntaxContext::within_ignored_ctxt(|| {
415                                cases[i].cons.eq_ignore_span(&cases[j].cons)
416                            })
417                        }
418                    };
419
420                if found {
421                    self.changed = true;
422                    report_change!("switches: Merging cases with same cons");
423                    let mut len = 1;
424                    while len < j && cases[j - len].cons.is_empty() {
425                        len += 1;
426                    }
427                    cases[j].cons = cases[i].cons.take();
428                    cases[(i + 1)..=j].rotate_right(len);
429                    i += len;
430                }
431            }
432
433            i += 1;
434        }
435    }
436
437    /// Try turn switch into if and remove empty switch
438    fn optimize_small_switch(&mut self, s: &mut Stmt) {
439        if self.ctx.contains(Ctx::IS_LABEL_BODY) {
440            return;
441        }
442
443        if let Stmt::Switch(sw) = s {
444            match &mut *sw.cases {
445                [] => {
446                    self.changed = true;
447                    report_change!("switches: Removing empty switch");
448                    *s = ExprStmt {
449                        span: sw.span,
450                        expr: sw.discriminant.take(),
451                    }
452                    .into()
453                }
454                [case] => {
455                    if contains_nested_break(case) {
456                        return;
457                    }
458                    self.changed = true;
459                    report_change!("switches: Turn one case switch into if");
460                    drop_break_and_postfix(&mut case.cons);
461
462                    let case = case.take();
463                    let mut discriminant = sw.discriminant.take();
464
465                    if let Some(test) = case.test {
466                        let test = BinExpr {
467                            left: discriminant,
468                            right: test,
469                            op: op!("==="),
470                            span: DUMMY_SP,
471                        }
472                        .into();
473
474                        *s = IfStmt {
475                            span: sw.span,
476                            test,
477                            cons: Box::new(Stmt::Block(BlockStmt {
478                                span: DUMMY_SP,
479                                stmts: case.cons,
480                                ..Default::default()
481                            })),
482                            alt: None,
483                        }
484                        .into()
485                    } else {
486                        self.ignore_return_value(
487                            &mut discriminant,
488                            DropOpts::DROP_NUMBER.union(DropOpts::DROP_STR_LIT),
489                        );
490
491                        let mut stmts = vec![];
492                        if !discriminant.is_invalid() {
493                            stmts.push(discriminant.take().into_stmt());
494                        }
495                        stmts.extend(case.cons);
496                        *s = BlockStmt {
497                            span: sw.span,
498                            stmts,
499                            ..Default::default()
500                        }
501                        .into()
502                    }
503                }
504                [first, second] if first.test.is_none() || second.test.is_none() => {
505                    if contains_nested_break(first) || contains_nested_break(second) {
506                        return;
507                    }
508                    self.changed = true;
509                    report_change!("switches: Turn two cases switch into if else");
510                    let terminate = first.cons.iter().rev().any(|s| s.terminates());
511
512                    if terminate {
513                        remove_last_break(&mut first.cons);
514                        remove_last_break(&mut second.cons);
515                        // they cannot both be default as that's syntax error
516                        let (def, case) = if first.test.is_none() {
517                            (first, second)
518                        } else {
519                            (second, first)
520                        };
521                        *s = IfStmt {
522                            span: sw.span,
523                            test: BinExpr {
524                                span: DUMMY_SP,
525                                op: op!("==="),
526                                left: sw.discriminant.take(),
527                                right: case.test.take().unwrap(),
528                            }
529                            .into(),
530                            cons: Stmt::Block(BlockStmt {
531                                span: DUMMY_SP,
532                                stmts: case.cons.take(),
533                                ..Default::default()
534                            })
535                            .into(),
536                            alt: Some(
537                                Stmt::Block(BlockStmt {
538                                    span: DUMMY_SP,
539                                    stmts: def.cons.take(),
540                                    ..Default::default()
541                                })
542                                .into(),
543                            ),
544                        }
545                        .into()
546                    } else {
547                        let mut stmts = vec![Stmt::If(IfStmt {
548                            span: DUMMY_SP,
549                            test: Expr::Bin(if first.test.is_none() {
550                                BinExpr {
551                                    span: DUMMY_SP,
552                                    op: op!("!=="),
553                                    left: sw.discriminant.take(),
554                                    right: second.test.take().unwrap(),
555                                }
556                            } else {
557                                BinExpr {
558                                    span: DUMMY_SP,
559                                    op: op!("==="),
560                                    left: sw.discriminant.take(),
561                                    right: first.test.take().unwrap(),
562                                }
563                            })
564                            .into(),
565                            cons: Stmt::Block(BlockStmt {
566                                span: DUMMY_SP,
567                                stmts: first.cons.take(),
568                                ..Default::default()
569                            })
570                            .into(),
571                            alt: None,
572                        })];
573                        stmts.extend(second.cons.take());
574                        *s = BlockStmt {
575                            span: sw.span,
576                            stmts,
577                            ..Default::default()
578                        }
579                        .into()
580                    }
581                }
582                _ => (),
583            }
584        }
585    }
586
587    fn optimize_switch_with_default_on_last(&mut self, stmt: &mut Stmt) {
588        let Stmt::Switch(s) = stmt else {
589            return;
590        };
591
592        let is_default_last = matches!(s.cases.last(), Some(SwitchCase { test: None, .. }));
593
594        // True if all cases except default is empty.
595        let is_all_case_empty = s
596            .cases
597            .iter()
598            .all(|case| case.test.is_none() || case.cons.is_empty());
599
600        let is_all_case_side_effect_free = s.cases.iter().all(|case| {
601            case.test
602                .as_ref()
603                .map(|e| e.is_ident() || !e.may_have_side_effects(self.expr_ctx))
604                .unwrap_or(true)
605        });
606
607        if is_default_last
608            && is_all_case_empty
609            && is_all_case_side_effect_free
610            && !contains_nested_break(s.cases.last().unwrap())
611        {
612            let mut exprs = Vec::new();
613            self.ignore_return_value(
614                &mut s.discriminant,
615                DropOpts::DROP_NUMBER.union(DropOpts::DROP_STR_LIT),
616            );
617            if !s.discriminant.is_invalid() {
618                exprs.push(s.discriminant.take());
619            }
620
621            exprs.extend(
622                s.cases
623                    .iter_mut()
624                    .filter_map(|case| case.test.take())
625                    .filter_map(|mut e| {
626                        self.ignore_return_value(
627                            &mut e,
628                            DropOpts::DROP_NUMBER.union(DropOpts::DROP_STR_LIT),
629                        );
630                        if e.is_invalid() {
631                            None
632                        } else {
633                            Some(e)
634                        }
635                    }),
636            );
637
638            let mut stmts = s.cases.pop().unwrap().cons;
639            drop_break_and_postfix(&mut stmts);
640
641            if !exprs.is_empty() {
642                prepend_stmt(
643                    &mut stmts,
644                    ExprStmt {
645                        span: DUMMY_SP,
646                        expr: Expr::from_exprs(exprs),
647                    }
648                    .into(),
649                );
650            }
651
652            report_change!("switches: Turn switch with default on last into block");
653            self.changed = true;
654            let block: Stmt = BlockStmt {
655                span: s.span,
656                stmts,
657                ..Default::default()
658            }
659            .into();
660            *stmt = block;
661        }
662    }
663}
664
665fn drop_break_and_postfix(cons: &mut Vec<Stmt>) {
666    let terminates_rpos = cons.iter().rposition(|s| s.terminates());
667
668    if let Some(terminates_rpos) = terminates_rpos {
669        cons.truncate(terminates_rpos + 1);
670    }
671
672    if let Some(last) = cons.last_mut() {
673        if let Stmt::Break(BreakStmt { label: None, .. }) = last {
674            *last = Stmt::dummy();
675        }
676    }
677}
678
679fn remove_last_break(stmt: &mut Vec<Stmt>) -> bool {
680    match stmt.last_mut() {
681        Some(Stmt::Break(BreakStmt { label: None, .. })) => {
682            report_change!("switches: Removing `break` at the end");
683            stmt.pop();
684            true
685        }
686        Some(Stmt::If(i)) => {
687            let mut changed = false;
688            match &mut *i.cons {
689                Stmt::Break(BreakStmt { label: None, .. }) => {
690                    report_change!("switches: Removing `break` at the end");
691                    i.cons.take();
692                    changed = true
693                }
694                Stmt::Block(b) => changed |= remove_last_break(&mut b.stmts),
695                _ => (),
696            };
697            if let Some(alt) = i.alt.as_mut() {
698                match &mut **alt {
699                    Stmt::Break(BreakStmt { label: None, .. }) => {
700                        report_change!("switches: Removing `break` at the end");
701                        alt.take();
702                        changed = true
703                    }
704                    Stmt::Block(b) => changed |= remove_last_break(&mut b.stmts),
705                    _ => (),
706                };
707            }
708            changed
709        }
710        Some(Stmt::Try(t)) => {
711            let mut changed = false;
712            changed |= remove_last_break(&mut t.block.stmts);
713
714            if let Some(h) = t.handler.as_mut() {
715                changed |= remove_last_break(&mut h.body.stmts);
716            }
717            if let Some(f) = t.finalizer.as_mut() {
718                changed |= remove_last_break(&mut f.stmts);
719            }
720            changed
721        }
722        Some(Stmt::Block(BlockStmt { stmts, .. })) => remove_last_break(stmts),
723        _ => false,
724    }
725}
726
727fn contains_nested_break(case: &SwitchCase) -> bool {
728    // wait for DCE to work
729    let terminator = case.cons.iter().rposition(|s| s.terminates());
730    if terminator.is_some_and(|t| t != case.cons.len() - 1) {
731        return true;
732    }
733
734    let mut v = BreakFinder {
735        top_level: true,
736        nested_unlabelled_break: false,
737    };
738    case.visit_with(&mut v);
739    v.nested_unlabelled_break
740}
741
742#[derive(Default)]
743struct BreakFinder {
744    top_level: bool,
745    nested_unlabelled_break: bool,
746}
747
748impl Visit for BreakFinder {
749    noop_visit_type!(fail);
750
751    fn visit_break_stmt(&mut self, s: &BreakStmt) {
752        if !self.top_level && s.label.is_none() {
753            self.nested_unlabelled_break = true;
754        }
755    }
756
757    fn visit_if_stmt(&mut self, i: &IfStmt) {
758        if self.top_level {
759            self.top_level = false;
760            i.visit_children_with(self);
761            self.top_level = true;
762        } else {
763            i.visit_children_with(self);
764        }
765    }
766
767    /// We don't care about breaks in a loop
768    fn visit_for_stmt(&mut self, _: &ForStmt) {}
769
770    /// We don't care about breaks in a loop
771    fn visit_for_in_stmt(&mut self, _: &ForInStmt) {}
772
773    /// We don't care about breaks in a loop
774    fn visit_for_of_stmt(&mut self, _: &ForOfStmt) {}
775
776    /// We don't care about breaks in a loop
777    fn visit_do_while_stmt(&mut self, _: &DoWhileStmt) {}
778
779    /// We don't care about breaks in a loop
780    fn visit_while_stmt(&mut self, _: &WhileStmt) {}
781
782    fn visit_switch_stmt(&mut self, _: &SwitchStmt) {}
783
784    fn visit_function(&mut self, _: &Function) {}
785
786    fn visit_arrow_expr(&mut self, _: &ArrowExpr) {}
787
788    fn visit_class(&mut self, _: &Class) {}
789}