swc_ecma_minifier/compress/pure/
switches.rs1use 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
15impl 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 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 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 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 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 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 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 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 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 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 fn merge_cases_with_same_cons(&mut self, cases: &mut Vec<SwitchCase>) {
360 let mut i = 0;
361 let len = cases.len();
362
363 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 let case_i_terminates = cases[i]
396 .cons
397 .last()
398 .map(|s| s.terminates())
399 .unwrap_or(false);
400
401 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 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 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 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 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 fn visit_for_stmt(&mut self, _: &ForStmt) {}
769
770 fn visit_for_in_stmt(&mut self, _: &ForInStmt) {}
772
773 fn visit_for_of_stmt(&mut self, _: &ForOfStmt) {}
775
776 fn visit_do_while_stmt(&mut self, _: &DoWhileStmt) {}
778
779 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}