diff --git a/Cargo.lock b/Cargo.lock index 30b26fe4868a4..516ab64a5231d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2352,6 +2352,7 @@ dependencies = [ "oxc_parser 0.95.0", "oxc_sourcemap 6.0.0", "oxc_span 0.95.0", + "rustc-hash", ] [[package]] diff --git a/crates/oxc_minifier/src/options.rs b/crates/oxc_minifier/src/options.rs index d5a3a47d78bfc..c9aeae6d9e508 100644 --- a/crates/oxc_minifier/src/options.rs +++ b/crates/oxc_minifier/src/options.rs @@ -1,4 +1,5 @@ use oxc_compat::EngineTargets; +use rustc_hash::FxHashSet; pub use oxc_ecmascript::side_effects::PropertyReadSideEffects; @@ -44,6 +45,13 @@ pub struct CompressOptions { /// pub treeshake: TreeShakeOptions, + /// Set of label names to drop from the code. + /// + /// Labeled statements matching these names will be removed during minification. + /// + /// Default: empty (no labels dropped) + pub drop_labels: FxHashSet, + /// Limit the maximum number of iterations for debugging purpose. pub max_iterations: Option, } @@ -65,6 +73,7 @@ impl CompressOptions { sequences: true, unused: CompressOptionsUnused::Remove, treeshake: TreeShakeOptions::default(), + drop_labels: FxHashSet::default(), max_iterations: None, } } @@ -79,6 +88,7 @@ impl CompressOptions { sequences: true, unused: CompressOptionsUnused::Keep, treeshake: TreeShakeOptions::default(), + drop_labels: FxHashSet::default(), max_iterations: None, } } @@ -93,6 +103,7 @@ impl CompressOptions { sequences: false, unused: CompressOptionsUnused::Remove, treeshake: TreeShakeOptions::default(), + drop_labels: FxHashSet::default(), max_iterations: None, } } diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index a39fea69d02fa..15f3e31c2a725 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -197,6 +197,13 @@ impl<'a> PeepholeOptimizations { pub fn try_fold_labeled(stmt: &mut Statement<'a>, ctx: &mut Ctx<'a, '_>) { let Statement::LabeledStatement(s) = stmt else { return }; let id = s.label.name.as_str(); + + if ctx.options().drop_labels.contains(id) { + *stmt = ctx.ast.statement_empty(s.span); + ctx.state.changed = true; + return; + } + // Check the first statement in the block, or just the `break [id] ` statement. // Check if we need to remove the whole block. match &mut s.body { diff --git a/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs b/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs index f6655edb3d009..084c7932bee4f 100644 --- a/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs +++ b/crates/oxc_minifier/tests/peephole/dead_code_elimination.rs @@ -1,4 +1,5 @@ use cow_utils::CowUtils; +use rustc_hash::FxHashSet; use oxc_allocator::Allocator; use oxc_codegen::Codegen; @@ -36,6 +37,18 @@ fn test_same(source_text: &str) { test(source_text, source_text); } +#[track_caller] +fn test_with_options(source_text: &str, expected: &str, options: CompressOptions) { + let allocator = Allocator::default(); + let source_type = SourceType::default(); + let mut ret = Parser::new(&allocator, source_text, source_type).parse(); + let program = &mut ret.program; + Compressor::new(&allocator).dead_code_elimination(program, options); + let result = Codegen::new().build(program).code; + let expected = run(expected, source_type, None); + assert_eq!(result, expected, "\nfor source\n{source_text}\nexpect\n{expected}\ngot\n{result}"); +} + #[test] fn dce_if_statement() { test("if (true) { foo }", "foo"); @@ -346,3 +359,48 @@ console.log([ ", ); } + +#[test] +fn drop_labels() { + let mut options = CompressOptions::dce(); + let mut drop_labels = FxHashSet::default(); + drop_labels.insert("PURE".to_string()); + options.drop_labels = drop_labels; + + test_with_options("PURE: { foo(); bar(); }", "", options); +} + +#[test] +fn drop_multiple_labels() { + let mut options = CompressOptions::dce(); + let mut drop_labels = FxHashSet::default(); + drop_labels.insert("PURE".to_string()); + drop_labels.insert("TEST".to_string()); + options.drop_labels = drop_labels; + + test_with_options( + "PURE: { foo(); } TEST: { bar(); } OTHER: { baz(); }", + "OTHER: baz();", + options, + ); +} + +#[test] +fn drop_labels_nested() { + let mut options = CompressOptions::dce(); + let mut drop_labels = FxHashSet::default(); + drop_labels.insert("PURE".to_string()); + options.drop_labels = drop_labels; + + test_with_options("PURE: { PURE: { foo(); } }", "", options); +} + +#[test] +fn drop_labels_with_vars() { + let mut options = CompressOptions::dce(); + let mut drop_labels = FxHashSet::default(); + drop_labels.insert("PURE".to_string()); + options.drop_labels = drop_labels; + + test_with_options("PURE: { var x = 1; foo(x); }", "", options); +} diff --git a/napi/minify/Cargo.toml b/napi/minify/Cargo.toml index bf3ae456f10a9..f9d77bd196080 100644 --- a/napi/minify/Cargo.toml +++ b/napi/minify/Cargo.toml @@ -34,6 +34,7 @@ oxc_span = { workspace = true } napi = { workspace = true } napi-derive = { workspace = true } +rustc-hash = { workspace = true } [target.'cfg(not(any(target_os = "linux", target_os = "freebsd", target_arch = "arm", target_family = "wasm")))'.dependencies] mimalloc-safe = { workspace = true, optional = true, features = ["skip_collect_on_exit"] } diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs index e5d9796563809..f9f2a3a2df654 100644 --- a/napi/minify/src/options.rs +++ b/napi/minify/src/options.rs @@ -3,6 +3,7 @@ use napi_derive::napi; use oxc_compat::EngineTargets; use oxc_minifier::TreeShakeOptions; +use rustc_hash::FxHashSet; #[napi(object)] pub struct CompressOptions { @@ -81,6 +82,7 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions { }, keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(), treeshake: TreeShakeOptions::default(), + drop_labels: FxHashSet::default(), max_iterations: o.max_iterations, }) }