diff --git a/Cargo.lock b/Cargo.lock index 4b40a3703bcfc..4edba4c9116b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1858,7 +1858,6 @@ dependencies = [ "oxc_syntax", "oxc_traverse", "pico-args", - "rustc-hash", ] [[package]] diff --git a/crates/oxc_minifier/Cargo.toml b/crates/oxc_minifier/Cargo.toml index 5212e6e03a5d8..60da703a74c9c 100644 --- a/crates/oxc_minifier/Cargo.toml +++ b/crates/oxc_minifier/Cargo.toml @@ -33,7 +33,6 @@ oxc_syntax = { workspace = true } oxc_traverse = { workspace = true } cow-utils = { workspace = true } -rustc-hash = { workspace = true } [dev-dependencies] oxc_parser = { workspace = true } diff --git a/crates/oxc_minifier/src/ast_passes/exploit_assigns.rs b/crates/oxc_minifier/src/ast_passes/exploit_assigns.rs deleted file mode 100644 index 4363d036ed258..0000000000000 --- a/crates/oxc_minifier/src/ast_passes/exploit_assigns.rs +++ /dev/null @@ -1,284 +0,0 @@ -use oxc_allocator::Vec; -use oxc_ast::ast::*; -use oxc_traverse::TraverseCtx; - -use super::PeepholeOptimizations; - -impl<'a> PeepholeOptimizations { - /// Tries to chain assignments together. - /// - /// - #[expect(clippy::unused_self)] - pub fn exploit_assigns( - &mut self, - _stmts: &mut Vec<'a, Statement<'a>>, - _ctx: &mut TraverseCtx<'a>, - ) { - } -} - -/// -#[cfg(test)] -mod test { - use crate::tester::{test, test_same}; - - #[test] - #[ignore] - fn test_expr_exploitation_types() { - test("a = true; b = true", "b = a = true"); - test("a = !0; b = !0", "b = a = !0"); - test("a = !1; b = !1", "b = a = !1"); - test("a = void 0; b = void 0", "b = a = void 0"); - test("a = -Infinity; b = -Infinity", "b = a = -Infinity"); - } - - #[test] - #[ignore] - fn test_expr_exploitation_types2() { - test("a = !0; b = !0", "b = a = !0"); - } - - #[test] - #[ignore] - fn nullish_coalesce() { - test("a = null; a ?? b;", "(a = null)??b"); - test("a = true; if (a ?? a) { foo(); }", "if ((a = true) ?? a) { foo() }"); - test("a = !0; if (a ?? a) { foo(); }", "if ((a = !0) ?? a) { foo() }"); - } - - #[test] - #[ignore] - fn test_expr_exploitation() { - test("a = null; b = null; var c = b", "var c = b = a = null"); - test("a = null; b = null", "b = a = null"); - test("a = undefined; b = undefined", "b = a = undefined"); - test("a = 0; b = 0", "b=a=0"); - test("a = 'foo'; b = 'foo'", "b = a = \"foo\""); - test("a = c; b = c", "b=a=c"); - - test_same("a = 0; b = 1"); - test_same("a = \"foo\"; b = \"foox\""); - - test("a = null; a && b;", "(a = null)&&b"); - test("a = null; a || b;", "(a = null)||b"); - - test("a = null; a ? b : c;", "(a = null) ? b : c"); - - test("a = null; this.foo = null;", "this.foo = a = null"); - test("function f(){ a = null; return null; }", "function f(){return a = null}"); - - test("a = true; if (a) { foo(); }", "if (a = true) { foo() }"); - test("a = true; if (a && a) { foo(); }", "if ((a = true) && a) { foo() }"); - test("a = false; if (a) { foo(); }", "if (a = false) { foo() }"); - - test("a = !0; if (a) { foo(); }", "if (a = !0) { foo() }"); - test("a = !0; if (a && a) { foo(); }", "if ((a = !0) && a) { foo() }"); - test("a = !1; if (a) { foo(); }", "if (a = !1) { foo() }"); - - test_same("a = this.foo; a();"); - test("a = b; b = a;", "b = a = b"); - test_same("a = b; a.c = a"); - test("this.foo = null; this.bar = null;", "this.bar = this.foo = null"); - test( - "this.foo = null; this.bar = null; this.baz = this.bar", - "this.baz = this.bar = this.foo = null", - ); - test( - "this.foo = null; this.bar = null; this.baz = this?.bar", - "this.bar = this.foo = null; this.baz = this?.bar;", - ); - test("this.foo = null; a = null;", "a = this.foo = null"); - test("this.foo = null; a = this.foo;", "a = this.foo = null"); - test_same("this.foo = null; a = this?.foo;"); - test("a.b.c=null; a=null;", "a = a.b.c = null"); - test_same("a = null; a.b.c = null"); - test("(a=b).c = null; this.b = null;", "this.b = (a=b).c = null"); - test_same("if(x) a = null; else b = a"); - } - - #[test] - #[ignore] - fn test_let_const_assignment() { - test("a = null; b = null; let c = b", "let c = b = a = null"); - } - - #[test] - #[ignore] - fn test_block_scope() { - test("{ a = null; b = null; c = b }", "{ c = b = a = null }"); - - // TODO (simranarora) What should we have as the intended behavior with block scoping? - test( - "a = null; b = null; { c = b; }", - // "{ c = b = a = null; } - "b = a = null; { c = b; }", - ); - } - - #[test] - #[ignore] - fn test_exploit_in_arrow_function() { - test("() => { a = null; return null; }", "() => { return a = null }"); - } - - #[test] - #[ignore] - fn test_nested_expr_exploitation() { - test( - "this.foo = null; this.bar = null; this.baz = null;", - "this.baz = this.bar = this.foo = null", - ); - - test( - "a = 3; this.foo = a; this.bar = a; this.baz = 3;", - "this.baz = this.bar = this.foo = a = 3", - ); - test( - "a = 3; this.foo = a; this.bar = this.foo; this.baz = a;", - "this.baz = this.bar = this.foo = a = 3", - ); - // recursively optimize assigns until optional chaining on RHS - test( - "a = 3; this.foo = a; this.bar = this?.foo; this.baz = a;", - "this.foo = a = 3; this.bar = this?.foo; this.baz = a;", - ); - test( - "a = 3; this.foo = a; this.bar = 3; this.baz = this.foo;", - "this.baz = this.bar = this.foo = a = 3", - ); - // recursively optimize assigns until optional chaining on RHS - test( - "a = 3; this.foo = a; this.bar = 3; this.baz = this?.foo;", - "this.bar = this.foo = a = 3; this.baz = this?.foo;", - ); - // test( - // "a = 3; this.foo = a; a = 3; this.bar = 3; " + "a = 3; this.baz = this.foo;", - // "this.baz = a = this.bar = a = this.foo = a = 3", - // ); - // recursively optimize assigns until optional chaining on RHS - // test( - // lines("a = 3; this.foo = a; a = 3; this.bar = 3; a = 3; this.baz = this?.foo;"), - // lines("a = this.bar = a = this.foo = a = 3; this.baz = this?.foo;"), - // ); - - // test( - // "a = 4; this.foo = a; a = 3; this.bar = 3; " + "a = 3; this.baz = this.foo;", - // "this.foo = a = 4; a = this.bar = a = 3; this.baz = this.foo", - // ); - // recursively optimize assigns until optional chaining on RHS - // test( - // lines("a = 4; this.foo = a; a = 3; this.bar = 3; a = 3; this.baz = this?.foo;"), - // lines("this.foo = a = 4;", "a = this.bar = a = 3;", "this.baz = this?.foo;"), - // ); - - // test( - // "a = 3; this.foo = a; a = 4; this.bar = 3; " + "a = 3; this.baz = this.foo;", - // "this.foo = a = 3; a = 4; a = this.bar = 3; this.baz = this.foo", - // ); - // test( - // lines("a = 3; this.foo = a; a = 4; this.bar = 3; ", "a = 3; this.baz = this?.foo;"), - // lines("this.foo = a = 3;", "a = 4;", "a = this.bar = 3;", "this.baz = this?.foo;"), - // ); - // test( - // "a = 3; this.foo = a; a = 3; this.bar = 3; " + "a = 4; this.baz = this.foo;", - // "this.bar = a = this.foo = a = 3; a = 4; this.baz = this.foo", - // ); - // test( - // lines("a = 3; this.foo = a; a = 3; this.bar = 3; a = 4; this.baz = this?.foo;"), - // lines("this.bar = a = this.foo = a = 3;", "a = 4;", "this.baz = this?.foo;"), - // ); - } - - #[test] - #[ignore] - fn test_bug1840071() { - // Some external properties are implemented as setters. Let's - // make sure that we don't collapse them inappropriately. - test("a.b = a.x; if (a.x) {}", "if (a.b = a.x) {}"); - test_same("a.b = a?.x; if (a?.x) {}"); - test_same("a.b = a.x; if (a.b) {}"); - test("a.b = a.c = a.x; if (a.x) {}", "if (a.b = a.c = a.x) {}"); - test_same("a.b = a.c = a?.x; if (a?.x) {}"); - - test_same("a.b = a.c = a.x; if (a.c) {}"); - test_same("a.b = a.c = a.x; if (a.b) {}"); - } - - #[test] - #[ignore] - fn test_bug2072343() { - test_same("a = a.x;a = a.x"); - test_same("a = a.x;b = a.x"); - test("b = a.x;a = a.x", "a = b = a.x"); - test_same("b = a?.x;a = a?.x"); - test_same("a.x = a;a = a.x"); - test_same("a.b = a.b.x;a.b = a.b.x"); - test_same("a.y = a.y.x;b = a.y;c = a.y.x"); - test("a = a.x;b = a;c = a.x", "b = a = a.x;c = a.x"); - test("b = a.x;a = b;c = a.x", "a = b = a.x;c = a.x"); - } - - #[test] - #[ignore] - fn test_bad_collapse_into_call() { - // Can't collapse this, because if we did, 'foo' would be called - // in the wrong 'this' context. - test_same("this.foo = function() {}; this.foo();"); - } - - #[test] - #[ignore] - fn test_bad_collapse() { - test_same("this.$e$ = []; this.$b$ = null;"); - } - - #[test] - #[ignore] - fn test_issue1017() { - test_same("x = x.parentNode.parentNode; x = x.parentNode.parentNode;"); - } - - #[test] - #[ignore] - fn test_destructuring_lhs_array_ideal_behaviours() { - test_same("a => { ([a] = a); return a; }"); // `a` is being reassigned. - test_same("a => { ([b] = a); return a; }"); // Evaluating `b` could side-effect `a`. - test_same("a => { ([a = foo()] = a); return a; }"); // `foo` may be invoked. - test_same("(a, b) => { (a = [a] = b); return b; }"); // Evaluating `a` could side-effect `b`. - } - - #[test] - #[ignore] - fn test_destructuring_lhs_array_backoff_behaviours() { - // TODO(b/123102446): We really like to collapse some of these chained assignments. - - test_same("(a, b) => { ([a] = a = b); return b; }"); // The middle `a` is redundant. - test_same("(a, b) => { ([a] = a = b); return a; }"); // The middle `a` is redundant. - test( - "(a, b) => { (a = [a] = b); return a; }", // The final `a` is redundant. - "(a, b) => { return (a = [a] = b); }", - ); - } - - #[test] - #[ignore] - fn test_destructuring_lhs_object_ideal_behaviours() { - test_same("a => { ({a} = a); return a; }"); // `a` is being reassigned. - test_same("a => { ({b} = a); return a; }"); // Evaluating `b` could side-effect `a`. - test_same("a => { ({a = foo()} = a); return a; }"); // `foo` may be invoked. - test_same("(a, b) => { (a = {a} = b); return b; }"); // Evaluating `a` could side-effect `b`. - } - - #[test] - #[ignore] - fn test_destructuring_lhs_object_backoff_behaviours() { - // TODO(b/123102446): We really like to collapse some of these chained assignments. - - test_same("(a, b) => { ({a} = a = b); return b; }"); // The middle `a` is redundant. - test_same("(a, b) => { ({a} = a = b); return a; }"); // The middle `a` is redundant. - test( - "(a, b) => { (a = {a} = b); return a; }", // The final `a` is redundant. - "(a, b) => { return (a = {a} = b); }", - ); - } -} diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 4859525fd9d4b..ede26831a8f62 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -5,7 +5,6 @@ use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, Travers mod collapse_variable_declarations; mod convert_to_dotted_properties; -mod exploit_assigns; mod minimize_exit_points; mod normalize; mod peephole_fold_constants; @@ -13,16 +12,9 @@ mod peephole_minimize_conditions; mod peephole_remove_dead_code; mod peephole_replace_known_methods; mod peephole_substitute_alternate_syntax; -mod remove_unused_code; mod statement_fusion; pub use normalize::{Normalize, NormalizeOptions}; -#[expect(unused)] -pub use remove_unused_code::RemoveUnusedCode; - -pub trait CompressorPass<'a>: Traverse<'a> { - fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>); -} pub struct PeepholeOptimizations { target: ESTarget, @@ -32,16 +24,16 @@ pub struct PeepholeOptimizations { in_fixed_loop: bool, } -impl PeepholeOptimizations { +impl<'a> PeepholeOptimizations { pub fn new(target: ESTarget, in_fixed_loop: bool) -> Self { Self { target, changed: false, in_fixed_loop } } - pub fn run_in_loop<'a>( - &mut self, - program: &mut Program<'a>, - ctx: &mut ReusableTraverseCtx<'a>, - ) { + pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { + traverse_mut_with_ctx(self, program, ctx); + } + + pub fn run_in_loop(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { let mut i = 0; loop { self.changed = false; @@ -58,16 +50,9 @@ impl PeepholeOptimizations { } } -impl<'a> CompressorPass<'a> for PeepholeOptimizations { - fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { - traverse_mut_with_ctx(self, program, ctx); - } -} - impl<'a> Traverse<'a> for PeepholeOptimizations { fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) { self.statement_fusion_exit_statements(stmts, ctx); - self.exploit_assigns(stmts, ctx); self.collapse_variable_declarations(stmts, ctx); self.minimize_conditions_exit_statements(stmts, ctx); self.remove_dead_code_exit_statements(stmts, ctx); @@ -167,14 +152,12 @@ pub struct DeadCodeElimination { inner: PeepholeOptimizations, } -impl DeadCodeElimination { +impl<'a> DeadCodeElimination { pub fn new() -> Self { Self { inner: PeepholeOptimizations::new(ESTarget::ESNext, false) } } -} -impl<'a> CompressorPass<'a> for DeadCodeElimination { - fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { + pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { traverse_mut_with_ctx(self, program, ctx); } } diff --git a/crates/oxc_minifier/src/ast_passes/normalize.rs b/crates/oxc_minifier/src/ast_passes/normalize.rs index c9a096f54f122..bd96457f31b86 100644 --- a/crates/oxc_minifier/src/ast_passes/normalize.rs +++ b/crates/oxc_minifier/src/ast_passes/normalize.rs @@ -5,7 +5,7 @@ use oxc_span::GetSpan; use oxc_syntax::scope::ScopeFlags; use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; -use crate::{ctx::Ctx, CompressOptions, CompressorPass}; +use crate::{ctx::Ctx, CompressOptions}; #[derive(Default)] pub struct NormalizeOptions { @@ -33,8 +33,8 @@ pub struct Normalize { compress_options: CompressOptions, } -impl<'a> CompressorPass<'a> for Normalize { - fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { +impl<'a> Normalize { + pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { traverse_mut_with_ctx(self, program, ctx); } } diff --git a/crates/oxc_minifier/src/ast_passes/remove_unused_code.rs b/crates/oxc_minifier/src/ast_passes/remove_unused_code.rs deleted file mode 100644 index 82694270d91c5..0000000000000 --- a/crates/oxc_minifier/src/ast_passes/remove_unused_code.rs +++ /dev/null @@ -1,81 +0,0 @@ -use rustc_hash::FxHashSet; - -use oxc_allocator::Vec as ArenaVec; -use oxc_ast::ast::*; -use oxc_syntax::symbol::SymbolId; -use oxc_traverse::{traverse_mut_with_ctx, ReusableTraverseCtx, Traverse, TraverseCtx}; - -use crate::CompressorPass; - -/// Remove Unused Code -/// -/// -pub struct RemoveUnusedCode { - pub(crate) changed: bool, - - symbol_ids_to_remove: FxHashSet, -} - -impl<'a> CompressorPass<'a> for RemoveUnusedCode { - fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) { - self.changed = false; - traverse_mut_with_ctx(self, program, ctx); - } -} - -impl<'a> Traverse<'a> for RemoveUnusedCode { - fn enter_program(&mut self, _node: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { - let symbols = ctx.symbols(); - for symbol_id in symbols.symbol_ids() { - if symbols.get_resolved_references(symbol_id).count() == 0 { - self.symbol_ids_to_remove.insert(symbol_id); - } - } - } - - fn exit_statements( - &mut self, - stmts: &mut ArenaVec<'a, Statement<'a>>, - _ctx: &mut TraverseCtx<'a>, - ) { - if self.changed { - stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_))); - } - } - - fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { - if let Statement::VariableDeclaration(decl) = stmt { - decl.declarations.retain(|d| { - if let BindingPatternKind::BindingIdentifier(ident) = &d.id.kind { - if d.init.is_none() && self.symbol_ids_to_remove.contains(&ident.symbol_id()) { - return false; - } - } - true - }); - if decl.declarations.is_empty() { - self.changed = true; - *stmt = ctx.ast.statement_empty(decl.span); - } - } - } -} - -impl RemoveUnusedCode { - #[allow(dead_code)] - pub fn new() -> Self { - Self { changed: false, symbol_ids_to_remove: FxHashSet::default() } - } -} - -#[cfg(test)] -mod test { - use crate::tester::{test, test_same}; - - #[test] - #[ignore] - fn simple() { - test("var x", ""); - test_same("var x = 1"); - } -} diff --git a/crates/oxc_minifier/src/compressor.rs b/crates/oxc_minifier/src/compressor.rs index b7b19cb6736b9..e850a9f947e4f 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -5,7 +5,7 @@ use oxc_traverse::ReusableTraverseCtx; use crate::{ ast_passes::{DeadCodeElimination, Normalize, NormalizeOptions, PeepholeOptimizations}, - CompressOptions, CompressorPass, + CompressOptions, }; pub struct Compressor<'a> { diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index f94309e224eca..9f5084e424d20 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -16,7 +16,7 @@ use oxc_semantic::{SemanticBuilder, Stats}; pub use oxc_mangler::MangleOptions; -pub use crate::{ast_passes::CompressorPass, compressor::Compressor, options::CompressOptions}; +pub use crate::{compressor::Compressor, options::CompressOptions}; #[derive(Debug, Clone, Copy)] pub struct MinifierOptions {