diff --git a/Cargo.lock b/Cargo.lock index 088074654..788810c5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1777,6 +1777,7 @@ 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 07d6f5fa2..a31318c3a 100644 --- a/crates/oxc_minifier/Cargo.toml +++ b/crates/oxc_minifier/Cargo.toml @@ -33,6 +33,7 @@ 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/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 0600d3630..23925cc7f 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -12,6 +12,7 @@ mod peephole_remove_dead_code; mod peephole_replace_known_methods; mod peephole_substitute_alternate_syntax; mod remove_syntax; +mod remove_unused_code; mod statement_fusion; pub use collapse_variable_declarations::CollapseVariableDeclarations; @@ -24,6 +25,7 @@ pub use peephole_remove_dead_code::PeepholeRemoveDeadCode; pub use peephole_replace_known_methods::PeepholeReplaceKnownMethods; pub use peephole_substitute_alternate_syntax::PeepholeSubstituteAlternateSyntax; pub use remove_syntax::RemoveSyntax; +pub use remove_unused_code::RemoveUnusedCode; pub use statement_fusion::StatementFusion; use crate::CompressOptions; @@ -32,7 +34,6 @@ pub trait CompressorPass<'a>: Traverse<'a> { fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>); } -// See `latePeepholeOptimizations` pub struct PeepholeOptimizations { x0_statement_fusion: StatementFusion, x1_minimize_exit_points: MinimizeExitPoints, diff --git a/crates/oxc_minifier/src/ast_passes/remove_unused_code.rs b/crates/oxc_minifier/src/ast_passes/remove_unused_code.rs new file mode 100644 index 000000000..3f02cd011 --- /dev/null +++ b/crates/oxc_minifier/src/ast_passes/remove_unused_code.rs @@ -0,0 +1,90 @@ +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 rustc_hash::FxHashSet; + +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 { + pub fn new() -> Self { + Self { changed: false, symbol_ids_to_remove: FxHashSet::default() } + } +} + +#[cfg(test)] +mod test { + use oxc_allocator::Allocator; + + use crate::tester; + + fn test(source_text: &str, expected: &str) { + let allocator = Allocator::default(); + let mut pass = super::RemoveUnusedCode::new(); + tester::test(&allocator, source_text, expected, &mut pass); + } + + fn test_same(source_text: &str) { + test(source_text, source_text); + } + + #[test] + 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 3a0b81888..9b16d0796 100644 --- a/crates/oxc_minifier/src/compressor.rs +++ b/crates/oxc_minifier/src/compressor.rs @@ -4,7 +4,9 @@ use oxc_semantic::{ScopeTree, SemanticBuilder, SymbolTable}; use oxc_traverse::ReusableTraverseCtx; use crate::{ - ast_passes::{DeadCodeElimination, Normalize, PeepholeOptimizations, RemoveSyntax}, + ast_passes::{ + DeadCodeElimination, Normalize, PeepholeOptimizations, RemoveSyntax, RemoveUnusedCode, + }, CompressOptions, CompressorPass, }; @@ -32,6 +34,7 @@ impl<'a> Compressor<'a> { ) { let mut ctx = ReusableTraverseCtx::new(scopes, symbols, self.allocator); RemoveSyntax::new(self.options).build(program, &mut ctx); + RemoveUnusedCode::new().build(program, &mut ctx); Normalize::new().build(program, &mut ctx); PeepholeOptimizations::new(true, self.options).run_in_loop(program, &mut ctx); PeepholeOptimizations::new(false, self.options).build(program, &mut ctx); diff --git a/tasks/coverage/snapshots/minifier_test262.snap b/tasks/coverage/snapshots/minifier_test262.snap index ede289e7e..a7e3fb9a8 100644 --- a/tasks/coverage/snapshots/minifier_test262.snap +++ b/tasks/coverage/snapshots/minifier_test262.snap @@ -1,5 +1,7 @@ commit: c4317b0c minifier_test262 Summary: -AST Parsed : 44101/44101 (100.00%) -Positive Passed: 44101/44101 (100.00%) +AST Parsed : 41696/41696 (100.00%) +Positive Passed: 41694/41696 (100.00%) +Compress: tasks/coverage/test262/test/language/module-code/instn-local-bndng-for-dup.js +Compress: tasks/coverage/test262/test/language/types/undefined/S8.1_A1_T1.js diff --git a/tasks/coverage/src/runtime/mod.rs b/tasks/coverage/src/runtime/mod.rs index 66a3ef969..748d5c746 100644 --- a/tasks/coverage/src/runtime/mod.rs +++ b/tasks/coverage/src/runtime/mod.rs @@ -153,8 +153,8 @@ impl Case for Test262RuntimeCase { return; } - // Unable to minify `script`, which may contain syntaxes that the minifier do not support (e.g. `with`). - if !self.base.is_module() { + // Unable to minify non-strict code, which may contain syntaxes that the minifier do not support (e.g. `with`). + if self.base.is_no_strict() { self.base.set_result(TestResult::Passed); return; } diff --git a/tasks/coverage/src/test262/mod.rs b/tasks/coverage/src/test262/mod.rs index 134912507..638284058 100644 --- a/tasks/coverage/src/test262/mod.rs +++ b/tasks/coverage/src/test262/mod.rs @@ -84,6 +84,10 @@ impl Test262Case { self.meta.flags.contains(&TestFlag::OnlyStrict) } + pub fn is_no_strict(&self) -> bool { + self.meta.flags.contains(&TestFlag::NoStrict) + } + pub fn is_raw(&self) -> bool { self.meta.flags.contains(&TestFlag::Raw) } diff --git a/tasks/coverage/src/tools/minifier.rs b/tasks/coverage/src/tools/minifier.rs index 0fb4c7624..b188fb4f1 100644 --- a/tasks/coverage/src/tools/minifier.rs +++ b/tasks/coverage/src/tools/minifier.rs @@ -41,17 +41,14 @@ impl Case for MinifierTest262Case { fn skip_test_case(&self) -> bool { self.base.should_fail() || self.base.skip_test_case() + // Unable to minify non-strict code, which may contain syntaxes that the minifier do not support (e.g. `with`). + || self.base.is_no_strict() } fn run(&mut self) { let source_text = self.base.code(); let is_module = self.base.is_module(); let source_type = SourceType::default().with_module(is_module); - // Unable to minify `script`, which may contain syntaxes that the minifier do not support (e.g. `with`). - if source_type.is_script() { - self.base.set_result(TestResult::Passed); - return; - } let result = get_result(source_text, source_type); self.base.set_result(result); } diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 48400ccd9..37e4eb5ef 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -21,7 +21,7 @@ Original | minified | minified | gzip | gzip | Fixture 3.20 MB | 1.01 MB | 1.01 MB | 332.13 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.99 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 493.00 kB | 488.28 kB | antd.js 10.95 MB | 3.51 MB | 3.49 MB | 910.06 kB | 915.50 kB | typescript.js diff --git a/tasks/minsize/src/lib.rs b/tasks/minsize/src/lib.rs index 638a48287..cfe25d356 100644 --- a/tasks/minsize/src/lib.rs +++ b/tasks/minsize/src/lib.rs @@ -10,7 +10,7 @@ use flate2::{write::GzEncoder, Compression}; use humansize::{format_size, DECIMAL}; use oxc_allocator::Allocator; use oxc_codegen::{CodeGenerator, CodegenOptions}; -use oxc_minifier::{CompressOptions, MangleOptions, Minifier, MinifierOptions}; +use oxc_minifier::{Minifier, MinifierOptions}; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; @@ -126,17 +126,13 @@ pub fn run() -> Result<(), io::Error> { fn minify_twice(file: &TestFile) -> String { let source_type = SourceType::from_path(&file.file_name).unwrap(); - let options = MinifierOptions { - mangle: Some(MangleOptions::default()), - compress: CompressOptions::default(), - }; - let source_text1 = minify(&file.source_text, source_type, options); - let source_text2 = minify(&source_text1, source_type, options); - assert_eq_minified_code(&source_text1, &source_text2, &file.file_name); - source_text2 + let code1 = minify(&file.source_text, source_type); + let code2 = minify(&code1, source_type); + assert_eq_minified_code(&code1, &code2, &file.file_name); + code2 } -fn minify(source_text: &str, source_type: SourceType, options: MinifierOptions) -> String { +fn minify(source_text: &str, source_type: SourceType) -> String { let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); let mut program = ret.program; @@ -147,7 +143,7 @@ fn minify(source_text: &str, source_type: SourceType, options: MinifierOptions) ReplaceGlobalDefinesConfig::new(&[("process.env.NODE_ENV", "'development'")]).unwrap(), ) .build(symbols, scopes, &mut program); - let ret = Minifier::new(options).build(&allocator, &mut program); + let ret = Minifier::new(MinifierOptions::default()).build(&allocator, &mut program); CodeGenerator::new() .with_options(CodegenOptions { minify: true, ..CodegenOptions::default() }) .with_mangler(ret.mangler) @@ -164,13 +160,9 @@ fn gzip_size(s: &str) -> usize { fn assert_eq_minified_code(s1: &str, s2: &str, filename: &str) { if s1 != s2 { - let normalized_left = normalize_minified_code(s1); - let normalized_right = normalize_minified_code(s2); - similar_asserts::assert_eq!( - normalized_left, - normalized_right, - "Minification failed for {filename}" - ); + let left = normalize_minified_code(s1); + let right = normalize_minified_code(s2); + similar_asserts::assert_eq!(left, right, "Minification failed for {filename}"); } }