diff --git a/crates/oxc_minifier/src/compressor.rs b/crates/oxc_minifier/src/compressor.rs index 479d55eaa5ca0..1f57da87093bd 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -32,7 +32,7 @@ impl<'a> Compressor<'a> { scoping: Scoping, options: CompressOptions, ) { - let state = MinifierState::new(options); + let state = MinifierState::new(program.source_type, options); let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator); let normalize_options = NormalizeOptions { convert_while_to_fors: true, convert_const_to_let: true }; @@ -52,7 +52,7 @@ impl<'a> Compressor<'a> { scoping: Scoping, options: CompressOptions, ) { - let state = MinifierState::new(options); + let state = MinifierState::new(program.source_type, options); let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator); let normalize_options = NormalizeOptions { convert_while_to_fors: false, convert_const_to_let: false }; diff --git a/crates/oxc_minifier/src/ctx.rs b/crates/oxc_minifier/src/ctx.rs index b7830616ffd75..5f4d17cddb786 100644 --- a/crates/oxc_minifier/src/ctx.rs +++ b/crates/oxc_minifier/src/ctx.rs @@ -101,6 +101,10 @@ impl<'a> Ctx<'a, '_> { &self.0.state.options } + pub fn source_type(&self) -> SourceType { + self.0.state.source_type + } + pub fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool { ident.is_global_reference(self.0.scoping()) } diff --git a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs index 932f1661ea2ac..d8f597a929935 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs @@ -589,15 +589,13 @@ mod test { use crate::{ CompressOptions, - tester::{run, test, test_same}, + tester::{test, test_options, test_same}, }; fn test_es2019(source_text: &str, expected: &str) { let target = ESTarget::ES2019; - assert_eq!( - run(source_text, Some(CompressOptions { target, ..CompressOptions::default() })), - run(expected, None) - ); + let options = CompressOptions { target, ..CompressOptions::default() }; + test_options(source_text, expected, &options); } #[test] diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index c6f0516915bbe..6994e2c32e561 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -303,7 +303,7 @@ impl<'a> PeepholeOptimizations { mod test { use crate::{ CompressOptions, - tester::{run, test, test_same}, + tester::{test, test_same, test_same_options}, }; use oxc_syntax::es_target::ESTarget; @@ -1429,11 +1429,8 @@ mod test { test_same("foo().a || (foo().a = 3)"); let target = ESTarget::ES2019; - let code = "x || (x = 3)"; - assert_eq!( - run(code, Some(CompressOptions { target, ..CompressOptions::default() })), - run(code, None) - ); + let options = CompressOptions { target, ..CompressOptions::default() }; + test_same_options("x || (x = 3)", &options); } #[test] @@ -1456,11 +1453,8 @@ mod test { test("var x; x = x || (() => 'a')", "var x; x ||= (() => 'a')"); let target = ESTarget::ES2019; - let code = "var x; x = x || 1"; - assert_eq!( - run(code, Some(CompressOptions { target, ..CompressOptions::default() })), - run(code, None) - ); + let options = CompressOptions { target, ..CompressOptions::default() }; + test_same_options("var x; x = x || 1", &options); } #[test] diff --git a/crates/oxc_minifier/src/peephole/minimize_statements.rs b/crates/oxc_minifier/src/peephole/minimize_statements.rs index 0bd67d88ffe37..10cb29432279b 100644 --- a/crates/oxc_minifier/src/peephole/minimize_statements.rs +++ b/crates/oxc_minifier/src/peephole/minimize_statements.rs @@ -8,7 +8,7 @@ use oxc_semantic::ScopeId; use oxc_span::{ContentEq, GetSpan}; use oxc_traverse::Ancestor; -use crate::{CompressOptionsUnused, ctx::Ctx, keep_var::KeepVar}; +use crate::{ctx::Ctx, keep_var::KeepVar}; use super::{PeepholeOptimizations, State}; @@ -385,7 +385,7 @@ impl<'a> PeepholeOptimizations { } let VariableDeclaration { span, kind, declarations, declare } = var_decl.unbox(); for mut decl in declarations { - if Self::is_declarator_unused(&decl, ctx) { + if Self::should_remove_unused_declarator(&decl, ctx) { state.changed = true; if let Some(init) = decl.init.take() { if init.may_have_side_effects(ctx) { @@ -406,25 +406,6 @@ impl<'a> PeepholeOptimizations { } } - fn is_declarator_unused(decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) -> bool { - if ctx.state.options.unused == CompressOptionsUnused::Keep { - return false; - } - // It is unsafe to remove if direct eval is involved. - if ctx.scoping().root_scope_flags().contains_direct_eval() { - return false; - } - if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind { - if let Some(symbol_id) = ident.symbol_id.get() { - return ctx - .scoping() - .get_resolved_references(symbol_id) - .all(|r| !r.flags().is_read()); - } - } - false - } - fn handle_expression_statement( &self, mut expr_stmt: Box<'a, ExpressionStatement<'a>>, diff --git a/crates/oxc_minifier/src/peephole/mod.rs b/crates/oxc_minifier/src/peephole/mod.rs index 6b92fdcde27aa..164c4c714915c 100644 --- a/crates/oxc_minifier/src/peephole/mod.rs +++ b/crates/oxc_minifier/src/peephole/mod.rs @@ -13,6 +13,7 @@ mod minimize_statements; mod normalize; mod remove_dead_code; mod remove_unused_expression; +mod remove_unused_variable_declaration; mod replace_known_methods; mod substitute_alternate_syntax; diff --git a/crates/oxc_minifier/src/peephole/remove_dead_code.rs b/crates/oxc_minifier/src/peephole/remove_dead_code.rs index 412a65e16703b..9a4a809e66e89 100644 --- a/crates/oxc_minifier/src/peephole/remove_dead_code.rs +++ b/crates/oxc_minifier/src/peephole/remove_dead_code.rs @@ -5,7 +5,7 @@ use oxc_ecmascript::{constant_evaluation::ConstantEvaluation, side_effects::MayH use oxc_span::GetSpan; use oxc_traverse::Ancestor; -use crate::{CompressOptionsUnused, ctx::Ctx, keep_var::KeepVar}; +use crate::{ctx::Ctx, keep_var::KeepVar}; use super::{LatePeepholeOptimizations, PeepholeOptimizations, State}; @@ -49,7 +49,6 @@ impl<'a> PeepholeOptimizations { self.try_fold_conditional_expression(e, state, ctx) } Expression::SequenceExpression(e) => self.try_fold_sequence_expression(e, state, ctx), - Expression::AssignmentExpression(e) => self.remove_dead_assignment_expression(e, ctx), _ => None, } { *expr = folded_expr; @@ -57,34 +56,6 @@ impl<'a> PeepholeOptimizations { } } - fn remove_dead_assignment_expression( - &self, - e: &mut AssignmentExpression<'a>, - ctx: &mut Ctx<'a, '_>, - ) -> Option> { - if matches!( - ctx.state.options.unused, - CompressOptionsUnused::Keep | CompressOptionsUnused::KeepAssign - ) { - return None; - } - let SimpleAssignmentTarget::AssignmentTargetIdentifier(ident) = - e.left.as_simple_assignment_target()? - else { - return None; - }; - let reference_id = ident.reference_id.get()?; - let symbol_id = ctx.scoping().get_reference(reference_id).symbol_id()?; - // Keep error for assigning to `const foo = 1; foo = 2`. - if ctx.scoping().symbol_flags(symbol_id).is_const_variable() { - return None; - } - if !ctx.scoping().get_resolved_references(symbol_id).all(|r| !r.flags().is_read()) { - return None; - } - Some(e.right.take_in(ctx.ast)) - } - /// Removes dead code thats comes after `return`, `throw`, `continue` and `break` statements. pub fn remove_dead_code_exit_statements( &self, @@ -566,21 +537,6 @@ impl<'a> PeepholeOptimizations { _ => false, } } - - fn remove_unused_function_declaration( - f: &Function<'a>, - ctx: &mut Ctx<'a, '_>, - ) -> Option> { - if ctx.state.options.unused == CompressOptionsUnused::Keep { - return None; - } - let id = f.id.as_ref()?; - let symbol_id = id.symbol_id.get()?; - if ctx.scoping().symbol_is_unused(symbol_id) { - return Some(ctx.ast.statement_empty(f.span)); - } - None - } } impl<'a> LatePeepholeOptimizations { @@ -603,10 +559,7 @@ impl<'a> LatePeepholeOptimizations { /// #[cfg(test)] mod test { - use crate::{ - CompressOptions, - tester::{test, test_options, test_same}, - }; + use crate::tester::{test, test_same}; #[test] fn test_fold_block() { @@ -813,10 +766,4 @@ mod test { fn remove_constant_value() { test("const foo = false; if (foo) { console.log('foo') }", "const foo = !1;"); } - - #[test] - fn remove_unused_function_declaration() { - let options = CompressOptions::smallest(); - test_options("function foo() {}", "", &options); - } } diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 7df7ac8f72029..84542ad210562 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -32,6 +32,9 @@ impl<'a> PeepholeOptimizations { Expression::ConditionalExpression(_) => self.fold_conditional_expression(e, state, ctx), Expression::BinaryExpression(_) => self.fold_binary_expression(e, state, ctx), Expression::CallExpression(_) => self.fold_call_expression(e, state, ctx), + Expression::AssignmentExpression(_) => { + self.remove_unused_assignment_expression(e, state, ctx) + } _ => !e.may_have_side_effects(ctx), } } diff --git a/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs b/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs new file mode 100644 index 0000000000000..1ead2df217621 --- /dev/null +++ b/crates/oxc_minifier/src/peephole/remove_unused_variable_declaration.rs @@ -0,0 +1,152 @@ +use oxc_allocator::TakeIn; +use oxc_ast::ast::*; +use oxc_ecmascript::side_effects::MayHaveSideEffects; + +use crate::{CompressOptionsUnused, ctx::Ctx}; + +use super::{PeepholeOptimizations, State}; + +impl<'a> PeepholeOptimizations { + pub fn should_remove_unused_declarator( + decl: &VariableDeclarator<'a>, + ctx: &mut Ctx<'a, '_>, + ) -> bool { + if ctx.state.options.unused == CompressOptionsUnused::Keep { + return false; + } + if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind { + if Self::keep_top_level_var_in_script_mode(ctx) { + return false; + } + // It is unsafe to remove if direct eval is involved. + if ctx.scoping().root_scope_flags().contains_direct_eval() { + return false; + } + if let Some(symbol_id) = ident.symbol_id.get() { + return ctx.scoping().symbol_is_unused(symbol_id); + } + } + false + } + + pub fn remove_unused_function_declaration( + f: &Function<'a>, + ctx: &mut Ctx<'a, '_>, + ) -> Option> { + if ctx.state.options.unused == CompressOptionsUnused::Keep { + return None; + } + if Self::keep_top_level_var_in_script_mode(ctx) { + return None; + } + let id = f.id.as_ref()?; + let symbol_id = id.symbol_id.get()?; + if ctx.scoping().symbol_is_unused(symbol_id) { + return Some(ctx.ast.statement_empty(f.span)); + } + None + } + + pub fn remove_unused_assignment_expression( + &self, + e: &mut Expression<'a>, + state: &mut State, + ctx: &mut Ctx<'a, '_>, + ) -> bool { + let Expression::AssignmentExpression(assign_expr) = e else { return false }; + if matches!( + ctx.state.options.unused, + CompressOptionsUnused::Keep | CompressOptionsUnused::KeepAssign + ) { + return false; + } + let Some(SimpleAssignmentTarget::AssignmentTargetIdentifier(ident)) = + assign_expr.left.as_simple_assignment_target() + else { + return false; + }; + if Self::keep_top_level_var_in_script_mode(ctx) { + return false; + } + let Some(reference_id) = ident.reference_id.get() else { return false }; + let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else { + return false; + }; + // Keep error for assigning to `const foo = 1; foo = 2`. + if ctx.scoping().symbol_flags(symbol_id).is_const_variable() { + return false; + } + if !ctx.scoping().get_resolved_references(symbol_id).all(|r| !r.flags().is_read()) { + return false; + } + state.changed = true; + if assign_expr.right.may_have_side_effects(ctx) { + *e = assign_expr.right.take_in(ctx.ast); + false + } else { + true + } + } + + /// Do remove top level vars in script mode. + fn keep_top_level_var_in_script_mode(ctx: &Ctx<'a, '_>) -> bool { + ctx.scoping.current_scope_id() == ctx.scoping().root_scope_id() + && ctx.source_type().is_script() + } +} + +#[cfg(test)] +mod test { + use oxc_span::SourceType; + + use crate::{ + CompressOptions, + tester::{ + test_options, test_options_source_type, test_same_options, + test_same_options_source_type, + }, + }; + + #[test] + fn remove_unused_variable_declaration() { + let options = CompressOptions::smallest(); + test_options("var x", "", &options); + test_options("var x = 1", "", &options); + test_options("var x = foo", "foo", &options); + test_same_options("var x; foo(x)", &options); + test_same_options("export var x", &options); + } + + #[test] + fn remove_unused_function_declaration() { + let options = CompressOptions::smallest(); + test_options("function foo() {}", "", &options); + test_same_options("function foo() {} foo()", &options); + test_same_options("export function foo() {} foo()", &options); + } + + #[test] + fn remove_unused_assignment_expression() { + let options = CompressOptions::smallest(); + test_options("var x = 1; x = 2;", "", &options); + test_options("var x = 1; x = 2;", "", &options); + test_options("var x = 1; x = foo();", "foo()", &options); + test_same_options("var x = 1; x = 2, foo(x)", &options); + test_same_options("function foo() { var t; return t = x(); } foo();", &options); + } + + #[test] + fn keep_in_script_mode() { + let options = CompressOptions::smallest(); + let source_type = SourceType::cjs(); + test_same_options_source_type("var x = 1; x = 2;", source_type, &options); + test_same_options_source_type("var x = 1; x = 2, foo(x)", source_type, &options); + + test_options_source_type( + "function foo() { var x = 1; x = 2; bar() } foo()", + "function foo() { bar() } foo()", + source_type, + &options, + ); + } +} diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index 9b73255fdc792..22e3c72258ddc 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -1069,12 +1069,12 @@ mod test { use crate::{ CompressOptions, - tester::{run, test, test_same}, + tester::{test, test_options, test_same}, }; fn test_es2015(code: &str, expected: &str) { - let opts = CompressOptions { target: ESTarget::ES2015, ..CompressOptions::default() }; - assert_eq!(run(code, Some(opts)), run(expected, None)); + let options = CompressOptions { target: ESTarget::ES2015, ..CompressOptions::default() }; + test_options(code, expected, &options); } fn test_value(code: &str, expected: &str) { diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index a9a402a16ba6e..8df94b7523c71 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -1275,15 +1275,9 @@ mod test { use crate::{ CompressOptions, options::CompressOptionsKeepNames, - tester::{default_options, run, test, test_same}, + tester::{default_options, test, test_same, test_same_options}, }; - fn test_same_keep_names(keep_names: CompressOptionsKeepNames, code: &str) { - let result = run(code, Some(CompressOptions { keep_names, ..default_options() })); - let expected = run(code, None); - assert_eq!(result, expected, "\nfor source\n{code}\ngot\n{result}"); - } - #[test] fn test_fold_return_result() { test("function f(){return !1;}", "function f(){return !1}"); @@ -1919,22 +1913,41 @@ mod test { test("try { foo } catch(e) { var e = 2 }", "try { foo } catch { var e = 2 }"); test_same("try { foo } catch(e) { var e = 2 } bar(e)"); + // FIXME catch(a) has no references but it cannot be removed. + // test_same( + // r#"var a = "PASS"; + // try { + // throw "FAIL1"; + // } catch (a) { + // var a = "FAIL2"; + // } + // console.log(a);"#, + // ); + let target = ESTarget::ES2018; - let code = "try { foo } catch(e) {}"; - assert_eq!( - run(code, Some(CompressOptions { target, ..CompressOptions::default() })), - run(code, None) - ); + let options = CompressOptions { target, ..CompressOptions::default() }; + test_same_options("try { foo } catch(e) {}", &options); } #[test] fn test_remove_name_from_expressions() { test("var a = function f() {}", "var a = function () {}"); test_same("var a = function f() { return f; }"); - test_same_keep_names(CompressOptionsKeepNames::function_only(), "var a = function f() {}"); + test("var a = class C {}", "var a = class {}"); test_same("var a = class C { foo() { return C } }"); - test_same_keep_names(CompressOptionsKeepNames::class_only(), "var a = class C {}"); + + let options = CompressOptions { + keep_names: CompressOptionsKeepNames::function_only(), + ..default_options() + }; + test_same_options("var a = function f() {}", &options); + + let options = CompressOptions { + keep_names: CompressOptionsKeepNames::class_only(), + ..default_options() + }; + test_same_options("var a = class C {}", &options); } #[test] diff --git a/crates/oxc_minifier/src/state.rs b/crates/oxc_minifier/src/state.rs index 13611cfd0e240..fcc5a5a351b8f 100644 --- a/crates/oxc_minifier/src/state.rs +++ b/crates/oxc_minifier/src/state.rs @@ -2,10 +2,13 @@ use rustc_hash::FxHashMap; use oxc_ecmascript::constant_evaluation::ConstantValue; use oxc_semantic::SymbolId; +use oxc_span::SourceType; use crate::CompressOptions; pub struct MinifierState<'a> { + pub source_type: SourceType, + pub options: CompressOptions, /// Constant values evaluated from expressions. @@ -16,7 +19,7 @@ pub struct MinifierState<'a> { } impl MinifierState<'_> { - pub fn new(options: CompressOptions) -> Self { - Self { options, constant_values: FxHashMap::default() } + pub fn new(source_type: SourceType, options: CompressOptions) -> Self { + Self { source_type, options, constant_values: FxHashMap::default() } } } diff --git a/crates/oxc_minifier/src/tester.rs b/crates/oxc_minifier/src/tester.rs index 69a774347197a..2123ebbc93bec 100644 --- a/crates/oxc_minifier/src/tester.rs +++ b/crates/oxc_minifier/src/tester.rs @@ -19,6 +19,15 @@ pub fn test_same_options(source_text: &str, options: &CompressOptions) { test_options(source_text, source_text, options); } +#[track_caller] +pub fn test_same_options_source_type( + source_text: &str, + source_type: SourceType, + options: &CompressOptions, +) { + test_options_source_type(source_text, source_text, source_type, options); +} + #[track_caller] pub fn test(source_text: &str, expected: &str) { test_options(source_text, expected, &default_options()); @@ -26,15 +35,25 @@ pub fn test(source_text: &str, expected: &str) { #[track_caller] pub fn test_options(source_text: &str, expected: &str, options: &CompressOptions) { - let result = run(source_text, Some(options.clone())); - let expected = run(expected, None); + let source_type = SourceType::mjs(); + test_options_source_type(source_text, expected, source_type, options); +} + +#[track_caller] +pub fn test_options_source_type( + source_text: &str, + expected: &str, + source_type: SourceType, + options: &CompressOptions, +) { + let result = run(source_text, source_type, Some(options.clone())); + let expected = run(expected, source_type, None); assert_eq!(result, expected, "\nfor source\n{source_text}\nexpect\n{expected}\ngot\n{result}"); } #[track_caller] -pub fn run(source_text: &str, options: Option) -> String { +fn run(source_text: &str, source_type: SourceType, options: Option) -> String { let allocator = Allocator::default(); - let source_type = SourceType::mjs(); let ret = Parser::new(&allocator, source_text, source_type) .with_options(ParseOptions { allow_return_outside_function: true, diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index f835ace0c522e..cdffd933888cc 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -11,17 +11,17 @@ Original | minified | minified | gzip | gzip | Fixture 544.10 kB | 71.38 kB | 72.48 kB | 25.85 kB | 26.20 kB | lodash.js -555.77 kB | 270.80 kB | 270.13 kB | 88.24 kB | 90.80 kB | d3.js +555.77 kB | 270.82 kB | 270.13 kB | 88.25 kB | 90.80 kB | d3.js 1.01 MB | 440.17 kB | 458.89 kB | 122.37 kB | 126.71 kB | bundle.min.js 1.25 MB | 647 kB | 646.76 kB | 160.28 kB | 163.73 kB | three.js -2.14 MB | 716.10 kB | 724.14 kB | 161.76 kB | 181.07 kB | victory.js +2.14 MB | 716.11 kB | 724.14 kB | 161.77 kB | 181.07 kB | victory.js 3.20 MB | 1.01 MB | 1.01 MB | 324.08 kB | 331.56 kB | echarts.js -6.69 MB | 2.24 MB | 2.31 MB | 462.45 kB | 488.28 kB | antd.js +6.69 MB | 2.25 MB | 2.31 MB | 463.06 kB | 488.28 kB | antd.js -10.95 MB | 3.34 MB | 3.49 MB | 856.90 kB | 915.50 kB | typescript.js +10.95 MB | 3.35 MB | 3.49 MB | 860.92 kB | 915.50 kB | typescript.js