feat(minifier): convert switch with one case to if statement (#24611)

Inline switch with 1 case to if statement or in case of default to
statement expressions + block statement


https://github.com/oxc-project/oxc/actions/runs/29759622291#summary-88410679091

change extracted from #17523
ref #17544
This commit is contained in:
Armano authored and GitHub committed 2026-07-26 08:01:40 +08:00
1 parent fbab4c31d2
commit 34f555945c
7 files changed
+140 -47

No files matched your search

@@ -3,13 +3,13 @@ use std::{iter, ops::ControlFlow};
use crate::generated::ancestor::Ancestor;
use oxc_allocator::{ArenaBox, ArenaVec, TakeIn};
use oxc_ast::ast::*;
use oxc_ast_visit::VisitJs;
use oxc_ast_visit::{VisitJs, walk_js};
use oxc_ecmascript::{
constant_evaluation::{DetermineValueType, IsLiteralValue, ValueType},
side_effects::MayHaveSideEffects,
};
use oxc_semantic::ScopeFlags;
use oxc_span::{ContentEq, GetSpan, GetSpanMut};
use oxc_span::{ContentEq, GetSpan, GetSpanMut, SPAN};
use crate::{TraverseCtx, keep_var::KeepVar};
@@ -724,6 +724,17 @@ impl<'a> PeepholeOptimizations {
is_empty && stmt.test.as_ref().is_none_or(Expression::is_literal)
}
/// Check if a switch case can be inlined by verifying:
/// - The test expression has no side effects
/// - All statements can be safely inlined (no unlabeled breaks)
pub fn can_switch_case_be_inlined(case: &SwitchCase<'a>) -> bool {
if !case.test.as_ref().is_none_or(Expression::is_literal) {
return false;
}
case.consequent.is_empty() || !FindNestedBreak::has_unlabelled_break_in_switch_case(case)
}
fn handle_switch_statement(
mut switch_stmt: ArenaBox<'a, SwitchStatement<'a>>,
result: &mut ArenaVec<'a, Statement<'a>>,
@@ -815,6 +826,55 @@ impl<'a> PeepholeOptimizations {
ctx.drop_statement(&dropped);
}
if !ctx.is_tree_shake_only()
&& switch_stmt.cases.len() == 1
&& Self::can_switch_case_be_inlined(&switch_stmt.cases[0])
&& let Some(mut case) = switch_stmt.cases.pop()
{
ctx.notice_change();
let block_stmt = if case.consequent.len() == 1
&& matches!(case.consequent[0], Statement::BlockStatement(_))
{
case.consequent.pop().unwrap()
} else {
Statement::new_block_statement_with_scope_id(
case.span,
case.consequent.take_in(ctx),
switch_stmt.scope_id(),
ctx,
)
};
if let Some(test) = case.test {
result.push(Statement::new_if_statement(
switch_stmt.span,
Expression::new_binary_expression(
SPAN,
switch_stmt.discriminant.take_in(ctx),
BinaryOperator::StrictEquality,
test,
ctx,
),
block_stmt,
None,
ctx,
));
return;
}
if !switch_stmt.discriminant.is_literal() {
result.push(Statement::new_expression_statement(
switch_stmt.discriminant.span(),
switch_stmt.discriminant.take_in(ctx),
ctx,
));
}
result.push(block_stmt);
return;
}
result.push(Statement::SwitchStatement(switch_stmt));
}
@@ -2069,3 +2129,39 @@ impl<'a> PeepholeOptimizations {
Some(false)
}
}
#[derive(Default)]
struct FindNestedBreak {
found_unlabelled_break: bool,
}
impl FindNestedBreak {
fn has_unlabelled_break_in_switch_case(node: &SwitchCase) -> bool {
let mut visitor = Self::default();
visitor.visit_switch_case(node);
visitor.found_unlabelled_break
}
}
impl<'a> VisitJs<'a> for FindNestedBreak {
fn visit_expression(&mut self, _it: &Expression<'a>) {
// do nothing
}
fn visit_statement(&mut self, it: &Statement<'a>) {
if self.found_unlabelled_break || it.is_declaration() || it.is_iteration_statement() {
return;
}
match it {
Statement::ThrowStatement(_)
| Statement::SwitchStatement(_)
| Statement::ContinueStatement(_)
| Statement::ReturnStatement(_)
| Statement::ExpressionStatement(_) => {}
Statement::BreakStatement(it) if it.label.is_none() => {
self.found_unlabelled_break = true;
}
_ => walk_js::walk_statement(self, it),
}
}
}
@@ -2021,7 +2021,7 @@ fn test_inline_single_use_variable() {
);
test(
"function wrapper(arg0, arg1) { let x = arg0; switch (x) { case 0: return 1; }}",
"function wrapper(arg0, arg1) { switch (arg0) { case 0: return 1; }}",
"function wrapper(arg0, arg1) { if (arg0 === 0) return 1; }",
);
test(
"function wrapper(arg0, arg1) { let x = arg0; let y = x; return y + y;}",
@@ -2407,13 +2407,13 @@ fn prune_empty_case_before_default() {
test("switch (x) { default: case 1: bar() }", "switch (x) { default: case 1: bar();}");
// String literals should also be removed
test("switch (x) { case 'a': case 'b': default: bar() }", "switch (x) { default: bar();}");
test("switch (x) { case 'a': case 'b': default: bar() }", "x, bar();");
// null literal (booleans get transformed to !0/!1 before this optimization runs)
test("switch (x) { case null: default: bar() }", "switch (x) { default: bar();}");
test("switch (x) { case null: default: bar() }", "x, bar();");
// BigInt literals
test("switch (x) { case 1n: case 2n: default: bar() }", "switch (x) { default: bar();}");
test("switch (x) { case 1n: case 2n: default: bar() }", "x, bar();");
// Non-empty case should stop the pruning
test(
@@ -2422,8 +2422,8 @@ fn prune_empty_case_before_default() {
);
// Only default - nothing to prune
test("switch (x) { default: bar() }", "switch (x) { default: bar();}");
test("switch (x) { default: bar() }", "x, bar();");
// No default - nothing to prune
test("switch (x) { case 0: foo(); case 1: }", "switch (x) { case 0: foo(); }");
test("switch (x) { case 0: foo(); case 1: }", "x === 0 && foo();");
}
@@ -266,7 +266,7 @@ fn test_dont_test_break_in_do_while_if_condition_has_side_effects() {
#[test]
fn test_switch_exit_points1() {
test("switch (x) { case 1: f(); break; }", "switch (x) { case 1: f(); }");
test("switch (x) { case 1: f(); break; }", "x === 1 && f();");
test(
"switch (x) { case 1: f(); break; case 2: g(); break; }",
"switch (x) { case 1: f(); break; case 2: g(); }",
@@ -129,14 +129,14 @@ fn test_handle_switch_statement() {
test("switch (a()) {}", "a()");
test("switch (a) { default: }", "a;");
test("switch (a) { default: break;}", " a;");
test("switch (a) { default: var b; break;}", "switch (a) { default: var b; }"); // a; var b;
test_same("switch (a) { default: b()}"); // a; b();
test_same("switch (a) { default: b(); return;}"); // a, b(); return;
test("switch (a) { default: var b; break;}", "a; var b;");
test("switch (a) { default: b()}", "a, b();");
test("switch (a) { default: b(); return;}", "a, b(); return;");
test("switch (a) { case 1: break;}", "a;");
test_same("switch (a) { case 1: b();}"); // a === 1 && b();
test("switch (a) { case 1: b();break; }", "switch (a) { case 1: b(); }"); // a === 1 && b();
test_same("switch (a) { case 1: b();return; }"); // if (a === 1) { b(); return; }
test("switch (a) { case 1: b();}", "a === 1 && b();");
test("switch (a) { case 1: b();break; }", "a === 1 && b();");
test("switch (a) { case 1: b();return; }", "if (a === 1) { b(); return; }");
test("switch (a) { default: case 1: }", "a;");
test("switch (a) { case 1: default: }", "a;");
@@ -152,8 +152,8 @@ fn test_handle_switch_statement() {
test_same("switch (x) { default: foo(); case 1: }");
test_same("switch (a) { case 1: b(); case 2: b();}");
test_same("switch (a) { case 1: case 2: b(); }");
test("switch (a) { case 1: var c=2; break;}", "switch (a) { case 1: var c=2; }"); // if (a === 1) { var c=2; }
test("switch (a) { case 1: case 2: default: b(); break;}", "switch (a) { default: b(); }"); // a, b();
test("switch (a) { case 1: var c=2; break;}", "if (a === 1) var c=2;");
test("switch (a) { case 1: case 2: default: b(); break;}", "a, b();");
test("switch (a) { default: break; case 1: break;}", "a;");
test(
@@ -167,16 +167,17 @@ fn test_handle_switch_statement() {
test("switch (a) { case b(): default:}", "switch (a) { case b(): }"); // a, b();
test("switch (a) { case 2: case 1: break; default: break;}", "a;");
test("switch (a) { case 3: b(); break; case 2: break;}", "switch (a) { case 3: b(); }"); // a === 3 && b();
test("switch (a) { case 3: b(); case 2: break;}", "switch (a) { case 3: b(); }"); // a === 3 && b();
test("switch (a) { case 3: b(); break; case 2: break;}", "a === 3 && b();");
test("switch (a) { case 3: b(); case 2: break;}", "a === 3 && b();");
test(
"switch (a) { case 3: b(); case 2: c(); break;}",
"switch (a) { case 3: b(); case 2: c(); }",
"switch (a) { case 3: b(); case 2: c();}",
);
test("switch (a) { case 3: b(); case 2: case 1: break;}", "switch (a) { case 3: b(); }"); // a === 3 && b();
test("switch (a) { case 3: b(); case 2: case 1: }", "switch (a) { case 3: b(); }"); // a === 3 && b();
test("switch (a) { case 3: b(); case 2: case 1: break;}", "a === 3 && b();");
test("switch (a) { case 3: b(); case 2: case 1: }", "a === 3 && b();");
test_same("switch (x) { default: case 1: foo(); case 2: }"); // x !== 2 && foo();
test("switch (a) { case 3: if (b) break }", "switch (a) { case 3: if (b) break; }"); // a === 3 && b;
test_same("switch (a) { case 3: if (b) break }"); // a === 3 && b;
test_same("switch (a) { case 1: if (b) break; c(); }");
test(
"switch (a) { case 3: { if(b) {c()} else {break;} }}",
"switch (a) { case 3: if (b) c(); else break; }",
@@ -185,29 +186,26 @@ fn test_handle_switch_statement() {
"switch (a) { case 3: { if(b) {c(); break;} else { d(); break;} }}",
"switch (a) { case 3: if(b) {c(); break;} else { d(); break;} }",
); // if (a === 3) b ? c() : d();
test("switch (a) { case 3: { for (;;) break } }", "switch (a) { case 3: for (;;) break; }"); // if (a === 3) for (;;) break;
test(
"switch (a) { case 3: { for (b of c) break; } }",
"switch (a) { case 3: for (b of c) break; }",
); // if (a === 3) for (b of c) break;
test("switch (a) { case 3: { for (;;) break } }", "if (a === 3) for (;;) break;");
test("switch (a) { case 3: { for (b of c) break; } }", "if (a === 3) for (b of c) break;");
test_same("switch (a) { case 3: with(b) break}");
test("switch (a) { case 3: while(!0) break}", "switch (a) { case 3: for (;;) break; }"); // if (a === 3) while(!0) break;
test("switch (a) { case 3: while(!0) break}", "if (a === 3) for (;;) break;");
test(
"switch (a) { case 1: c(); case 2: default: b();break;}",
"switch (a) { case 1: c(); default: b(); }",
); // a === 1 && c(); b();
test_same("function f() { switch (a) { case 1: return;} }"); // function f() { a; }
test("switch (a()) { default: {let y;} }", "switch (a()) { default: { let y; } }"); // a(); { let y; }
test("function f() { switch (a) { case 1: return;} }", "function f() { a; }");
test("switch (a()) { default: {let y;} }", "a(); { let y; }");
test(
"function f(){switch ('x') { case 'x': var x = 1;break; case 'y': break; }}",
"function f(){switch ('x') { case 'x': var x = 1; }}",
"function f(){ var x = 1; }",
);
test("switch (a) { default: if(a) {break;}c();}", "switch (a) { default: if(a) break;c();}"); // a, !a && c();
test("switch (a) { case 1: if(a) {b();}c();}", "switch (a) { case 1: a && b(), c(); }"); // if (a === 1) { a && b(), c(); }
test("switch ('\\v') { case '\\u000B': foo();}", "switch ('\\v') { case '\\v': foo(); }"); // foo();
test("switch (a) { case 1: if(a) {b();}c();}", "a === 1 && (a && b(), c());");
test("switch ('\\v') { case '\\u000B': foo();}", "foo();");
test_same("x: switch (a) { case 1: break x;}"); // x: { a; break x; }
test_same("x: switch (a) { case 1: break x;}"); // x: if (a === 1) { break x; }
test_same("x: switch (a) { case 2: break x; case 1: break x;}"); // x: { a; break x; }
test_same("x: switch (2) { case 2: f(); break outer; }"); // x: { f(); break outer; }
test(
@@ -256,19 +254,18 @@ fn test_handle_switch_statement() {
"switch (0) { case NaN: foobar();break;case -0: foo();break; case 2: bar();break;}",
"switch (0) { case NaN: foobar();break;case -0: foo();break; case 2: bar();}",
); // foo()
test(
"let x = 1; switch ('x') { case 'x': let x = 2; break;}",
"let x = 1; switch ('x') { case 'x': let x = 2; }",
); // let x = 1; { let x = 2; }
test_same("switch (1) { case 2: var x=0;}"); // if (0) var x;
test("let x = 1; switch ('x') { case 'x': let x = 2; break;}", "let x = 1; { let x = 2; }");
test("switch (1) { case 2: var x=0;}", "if (0) var x;");
test(
"switch (b) { case 2: switch (a) { case 2: a();break;case 3: foo();break;}}",
"switch (b) { case 2: switch (a) { case 2: a();break;case 3: foo();}}",
"if (b === 2) switch (a) { case 2: a();break;case 3: foo();}",
); // ;
test_same("switch (b) { case 2: switch (a) { case 2: foo()}}"); // if (b === 2 && a === 2) foo()
test("switch (b) { case 2: switch (a) { case 2: foo()}}", "b === 2 && a === 2 && foo();");
// TODO: expected TDZ issue, when folding if without body https://github.com/oxc-project/oxc/issues/24589
test(
"function f(){ switch (0) { case x: break; } let x = 1; }",
"function f(){ switch (0) { case x: } let x = 1; }",
); // TDZ
); // TDZ; function f() { x; let x = 1; }
test_same("function f(){ switch (0) { case x: case y: } let x = 1; }"); // TDZ; function f() { x, y; let x = 1; }
}
@@ -845,7 +845,7 @@ fn keep_script_root_var_in_nested_statement_after_cycle_removed() {
let source = "function outer() { function d1() { return x + d2() } function d2() { return d1() } return 1 } outer(); switch (1) { case 1: var x = 42; }";
// Script globals can be rebound through global-object properties without a
// resolved write reference, so the call cannot reuse a pure summary.
let expected = "function outer() { return 1 } switch (outer(), 1) { case 1: var x = 42; }";
let expected = "function outer() { return 1 } if (outer(), !0) var x = 42;";
test_options_source_type(source, expected, SourceType::script(), &options);
// CommonJS top-level vars are wrapper-local, so ordinary counts may remove them.
+2 -2
View File
@@ -3,7 +3,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
-------------------------------------------------------------------------------------
72.14 kB | 23.11 kB | 23.70 kB | 8.34 kB | 8.54 kB | 2 | react.development.js
173.90 kB | 59.37 kB | 59.82 kB | 19.15 kB | 19.33 kB | 2 | moment.js
173.90 kB | 59.35 kB | 59.82 kB | 19.15 kB | 19.33 kB | 2 | moment.js
287.63 kB | 89.24 kB | 90.07 kB | 30.91 kB | 31.95 kB | 2 | jquery.js
@@ -23,5 +23,5 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
6.69 MB | 2.12 MB | 2.31 MB | 453.79 kB | 488.28 kB | 5 | antd.js
10.95 MB | 3.33 MB | 3.49 MB | 852.93 kB | 915.50 kB | 4 | typescript.js
10.95 MB | 3.33 MB | 3.49 MB | 852.94 kB | 915.50 kB | 4 | typescript.js
@@ -50,7 +50,7 @@ antd.js:
sys alloc bytes: 29975903 # 29.98 MB
sys peak growth: 19934725 # 19.93 MB
arena allocs: 371546
arena reallocs: 76276
arena reallocs: 76271
arena size: 49106184 # 49.11 MB
binder.ts: