mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(minifier): expand switch optimiation to remove empty cases (#24520)
remove empty cases: - before default or at end (if no default). - only if they don't affect behavior. refactored implementation from #17523 ref #17544 --------- Co-authored-by: Dunqing <dengqing0821@gmail.com>
This commit is contained in:
1 parent
1e50a2d6a0
commit
3acf4c1789
7 files changed
+105
-47
No files matched your search
@@ -722,6 +722,22 @@ impl<'a> PeepholeOptimizations {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_switch_case_removable(stmt: &SwitchCase, allow_break: bool) -> bool {
|
||||
let is_empty = if stmt.consequent.len() == 1 {
|
||||
match stmt.consequent.last() {
|
||||
Some(Statement::EmptyStatement(_)) => true,
|
||||
Some(Statement::BreakStatement(break_stmt)) => {
|
||||
allow_break && break_stmt.label.is_none()
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
stmt.consequent.is_empty()
|
||||
};
|
||||
|
||||
is_empty && stmt.test.as_ref().is_none_or(Expression::is_literal)
|
||||
}
|
||||
|
||||
fn handle_switch_statement(
|
||||
mut switch_stmt: ArenaBox<'a, SwitchStatement<'a>>,
|
||||
result: &mut ArenaVec<'a, Statement<'a>>,
|
||||
@@ -745,33 +761,56 @@ impl<'a> PeepholeOptimizations {
|
||||
ctx.drop_statement(&dropped);
|
||||
}
|
||||
|
||||
// Prune empty case clauses before a trailing default
|
||||
// e.g., `switch (x) { case 0: foo(); break; case 1: default: bar() }`
|
||||
// => `switch (x) { case 0: foo(); break; default: bar() }`
|
||||
// Remove empty case clauses that don't affect behavior.
|
||||
// Handles fall-through semantics: remove empty cases before default or at end (if no default).
|
||||
// e.g., `switch(x){ case 0: foo(); break; case 1: default: bar() }`
|
||||
// => `switch(x){ case 0: foo(); break; default: bar() }`
|
||||
// https://github.com/evanw/esbuild/commit/add452ed51333953dd38a26f28a775bb220ea2e9
|
||||
if let Some(last_case) = switch_stmt.cases.last()
|
||||
&& last_case.test.is_none()
|
||||
{
|
||||
let default_idx = switch_stmt.cases.len() - 1;
|
||||
let mut first_empty_idx = default_idx;
|
||||
// Iterate backward through preceding cases
|
||||
while first_empty_idx > 0 {
|
||||
let case = &switch_stmt.cases[first_empty_idx - 1];
|
||||
// Only remove empty cases with primitive literal tests
|
||||
if case.consequent.is_empty()
|
||||
&& case.test.as_ref().is_some_and(Expression::is_literal)
|
||||
{
|
||||
first_empty_idx -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
let case_count = switch_stmt.cases.len();
|
||||
if case_count == 1 {
|
||||
// Remove sole case if empty and has no side-effect test
|
||||
if Self::is_switch_case_removable(&switch_stmt.cases[0], true) {
|
||||
ctx.drop_switch_case(&switch_stmt.cases.pop().unwrap());
|
||||
}
|
||||
// If we found cases to remove, keep cases [0..first_empty_idx] + [default]
|
||||
if first_empty_idx < default_idx {
|
||||
let default_case = switch_stmt.cases.pop().unwrap();
|
||||
switch_stmt.cases.truncate(first_empty_idx);
|
||||
switch_stmt.cases.push(default_case);
|
||||
ctx.notice_change();
|
||||
} else if case_count > 1 {
|
||||
// Determine the range [0, end] to check for removable cases.
|
||||
// 1. default exists and is empty: check the full switch.
|
||||
// 2. default exists and is non-removable and last: check only cases before that default.
|
||||
// 3. default exists, is non-removable, and is not last: skip this optimization (`end = 0`).
|
||||
// 4. no default case: check the full switch and allow a trailing unlabeled `break`.
|
||||
let default_pos = switch_stmt.cases.iter().rposition(SwitchCase::is_default_case);
|
||||
let (end, allow_break) = if let Some(default_pos) = default_pos {
|
||||
if Self::is_switch_case_removable(&switch_stmt.cases[default_pos], true) {
|
||||
(case_count, true)
|
||||
} else if default_pos == case_count - 1 {
|
||||
(default_pos, false)
|
||||
} else {
|
||||
(0, false)
|
||||
}
|
||||
} else {
|
||||
(case_count, true)
|
||||
};
|
||||
|
||||
if end > 0 {
|
||||
// Last non-removable case index in [0, end]. Returns None if all cases are removable.
|
||||
let last_non_removable_case_before_end = switch_stmt.cases[..end]
|
||||
.iter()
|
||||
.rposition(|case| !Self::is_switch_case_removable(case, allow_break));
|
||||
|
||||
// Calculate the start of the removable suffix.
|
||||
// 1. next case after last non-removable: remove from pos + 1
|
||||
// 2. no non-removable case: all cases are removable, start from 0
|
||||
let start = match last_non_removable_case_before_end {
|
||||
Some(pos) => pos + 1,
|
||||
None => 0,
|
||||
};
|
||||
|
||||
// Remove the removable suffix if any
|
||||
if start < end && default_pos.is_none_or(|pos| pos >= start) {
|
||||
for removed_case in switch_stmt.cases.drain(start..end) {
|
||||
ctx.drop_switch_case(&removed_case);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -522,4 +522,14 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
self.dirty_diff().visit_variable_declarator(decl);
|
||||
self.state.record_mutation();
|
||||
}
|
||||
|
||||
/// Mark a switch case subtree as about to be dropped. Walks the entire case —
|
||||
/// test expression (if present) and all statements in the consequent. Same contract
|
||||
/// as `drop_expression`. Use this helper when removing a case from a switch statement's
|
||||
/// case vector to properly notify the scope tracking system about dropped references.
|
||||
#[inline]
|
||||
pub fn drop_switch_case(&mut self, switch_case: &SwitchCase<'a>) {
|
||||
self.dirty_diff().visit_switch_case(switch_case);
|
||||
self.state.record_mutation();
|
||||
}
|
||||
}
|
||||
@@ -2425,5 +2425,5 @@ fn prune_empty_case_before_default() {
|
||||
test("switch (x) { default: bar() }", "switch (x) { default: bar();}");
|
||||
|
||||
// No default - nothing to prune
|
||||
test("switch (x) { case 0: foo(); case 1: }", "switch (x) { case 0: foo(); case 1:}");
|
||||
test("switch (x) { case 0: foo(); case 1: }", "switch (x) { case 0: foo(); }");
|
||||
}
|
||||
@@ -29,7 +29,7 @@ fn test_fold_one_child_blocks() {
|
||||
test_same("function f(){try{foo()}catch(e){bar(e)}finally{baz()}}");
|
||||
|
||||
// Try it out with switch statements
|
||||
test_same("function f(){switch(x){case 1:break}}");
|
||||
test("function f(){switch(x){case 1:break}}", "function f(){x;}");
|
||||
|
||||
// Do while loops stay in a block if that's where they started
|
||||
test(
|
||||
|
||||
@@ -126,46 +126,50 @@ fn test_object_property_shorthand_normalisation_skips_proto_setter() {
|
||||
|
||||
#[test]
|
||||
fn test_handle_switch_statement() {
|
||||
test("switch (a()){}", "a()"); // a()
|
||||
test_same("switch (a) { default: }"); // a;
|
||||
test_same("switch (a) { default: break;}"); // a;
|
||||
test("switch (a()) {}", "a()");
|
||||
test("switch (a) { default: }", "a;");
|
||||
test("switch (a) { default: break;}", " a;");
|
||||
test_same("switch (a) { default: var b; break;}"); // a; var b;
|
||||
test_same("switch (a) { default: b()}"); // a; b();
|
||||
test_same("switch (a) { default: b(); return;}"); // a, b(); return;
|
||||
|
||||
test_same("switch (a) { case 1: break;}"); // a;
|
||||
test("switch (a) { case 1: break;}", "a;");
|
||||
test_same("switch (a) { case 1: b();}"); // a === 1 && b();
|
||||
test_same("switch (a) { case 1: b();break; }"); // a === 1 && b();
|
||||
test_same("switch (a) { case 1: b();return; }"); // if (a === 1) { b(); return; }
|
||||
|
||||
test_same("switch (a) { default: case 1: }"); // a;
|
||||
test("switch (a) { case 1: default: }", "switch (a) { default:}"); // a;
|
||||
test("switch (a) { default: case 1: }", "a;");
|
||||
test("switch (a) { case 1: default: }", "a;");
|
||||
test_same("switch (a) { case 1: default: break; case 2: b()}");
|
||||
test_same("switch (a) { case 1: b(); default: c()}"); // a === 1 && b(); c();
|
||||
test_same("switch (a) { case 1: default: b(); case 2: c();}");
|
||||
test_same("switch (a) { case 1: b(); default: break; case 2: c()}");
|
||||
test_same("switch (a) { case 1: b(); case 2: break; case 3: c()}");
|
||||
test_same("switch (a) { case 1: b(); break; case 2: c();break;}");
|
||||
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_same("switch (a) { case 1: var c=2; break;}"); // if (a === 1) { var c=2; }
|
||||
test(
|
||||
"switch (a) { case 1: case 2: default: b(); break;}",
|
||||
"switch (a) { default: b(); break;}",
|
||||
); // a, b();
|
||||
|
||||
test_same("switch (a) { default: break; case 1: break;}"); // a;
|
||||
test("switch (a) { default: break; case 1: break;}", "a;");
|
||||
test_same("switch (a) { default: b();break;case 1: c();break;}"); // a === 1 ? c() : b();
|
||||
test(
|
||||
"switch (a) { default: {b();break;} case 1: {c();break;}}",
|
||||
"switch (a) { default: b();break;case 1: c();break;}",
|
||||
); // a === 1 ? c() : b();
|
||||
|
||||
test_same("switch (a) { case b(): default:}"); // a, b();
|
||||
test_same("switch (a) { case 2: case 1: break; default: break;}"); // a;
|
||||
test_same("switch (a) { case 3: b(); break; case 2: break;}"); // a === 3 && b();
|
||||
test_same("switch (a) { case 3: b(); case 2: break;}"); // a === 3 && b();
|
||||
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(); break; }"); // a === 3 && b();
|
||||
test("switch (a) { case 3: b(); case 2: break;}", "switch (a) { case 3: b(); }"); // a === 3 && b();
|
||||
test_same("switch (a) { case 3: b(); case 2: c(); break;}");
|
||||
test_same("switch (a) { case 3: b(); case 2: case 1: break;}"); // a === 3 && b();
|
||||
test_same("switch (a) { case 3: b(); case 2: case 1: }"); // a === 3 && b();
|
||||
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_same("switch (x) { default: case 1: foo(); case 2: }"); // x !== 2 && foo();
|
||||
test_same("switch (a) { case 3: if (b) break }"); // a === 3 && b;
|
||||
test(
|
||||
"switch (a) { case 3: { if(b) {c()} else {break;} }}",
|
||||
@@ -189,7 +193,10 @@ fn test_handle_switch_statement() {
|
||||
); // 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_same("function f(){switch ('x') { case 'x': var x = 1;break; case 'y': break; }}");
|
||||
test(
|
||||
"function f(){switch ('x') { case 'x': var x = 1;break; case 'y': break; }}",
|
||||
"function f(){switch ('x') { case 'x': var x = 1;break; }}",
|
||||
);
|
||||
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();
|
||||
@@ -226,4 +233,6 @@ fn test_handle_switch_statement() {
|
||||
test_same("switch (1) { case 2: var x=0;}"); // if (0) var x;
|
||||
test_same("switch (b) { case 2: switch (a) { case 2: a();break;case 3: foo();break;}}"); // ;
|
||||
test_same("switch (b) { case 2: switch (a) { case 2: foo()}}"); // if (b === 2 && a === 2) foo()
|
||||
|
||||
test_same("function f(){ switch (0) { case x: break; } let x = 1; }"); // TDZ
|
||||
}
|
||||
@@ -21,7 +21,7 @@ Original | minified | minified | gzip | gzip | Iterations | Fi
|
||||
|
||||
3.20 MB | 1.00 MB | 1.01 MB | 322.59 kB | 331.56 kB | 2 | echarts.js
|
||||
|
||||
6.69 MB | 2.13 MB | 2.31 MB | 455.50 kB | 488.28 kB | 5 | antd.js
|
||||
6.69 MB | 2.13 MB | 2.31 MB | 455.48 kB | 488.28 kB | 5 | antd.js
|
||||
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.06 kB | 915.50 kB | 4 | typescript.js
|
||||
10.95 MB | 3.33 MB | 3.49 MB | 853.03 kB | 915.50 kB | 4 | typescript.js
|
||||
|
||||
@@ -26,7 +26,7 @@ pdf.mjs:
|
||||
file size: 567296 # 567.30 kB
|
||||
sys allocs: 2469
|
||||
sys reallocs: 205
|
||||
arena allocs: 47471
|
||||
arena allocs: 47467
|
||||
arena reallocs: 7742
|
||||
arena size: 6356528 # 6.36 MB
|
||||
|
||||
@@ -34,9 +34,9 @@ antd.js:
|
||||
file size: 6686316 # 6.69 MB
|
||||
sys allocs: 1044
|
||||
sys reallocs: 56
|
||||
arena allocs: 372632
|
||||
arena allocs: 372552
|
||||
arena reallocs: 76745
|
||||
arena size: 49033120 # 49.03 MB
|
||||
arena size: 49031840 # 49.03 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
@@ -50,7 +50,7 @@ kitchen-sink.tsx:
|
||||
file size: 732898 # 732.90 kB
|
||||
sys allocs: 2309
|
||||
sys reallocs: 175
|
||||
arena allocs: 88129
|
||||
arena allocs: 88097
|
||||
arena reallocs: 15654
|
||||
arena size: 10190368 # 10.19 MB
|
||||
|
||||
Reference in new issue
Block a user