Skip to content

Commit

Permalink
feat(minifier): add RemoveUnusedCode (#8210)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen committed Jan 2, 2025
1 parent d2d90b0 commit 2786dea
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 30 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/oxc_minifier/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
3 changes: 2 additions & 1 deletion crates/oxc_minifier/src/ast_passes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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,
Expand Down
90 changes: 90 additions & 0 deletions crates/oxc_minifier/src/ast_passes/remove_unused_code.rs
Original file line number Diff line number Diff line change
@@ -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
///
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/RemoveUnusedCode.java>
pub struct RemoveUnusedCode {
pub(crate) changed: bool,

symbol_ids_to_remove: FxHashSet<SymbolId>,
}

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");
}
}
5 changes: 4 additions & 1 deletion crates/oxc_minifier/src/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand Down Expand Up @@ -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);
Expand Down
6 changes: 4 additions & 2 deletions tasks/coverage/snapshots/minifier_test262.snap
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions tasks/coverage/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions tasks/coverage/src/test262/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
7 changes: 2 additions & 5 deletions tasks/coverage/src/tools/minifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion tasks/minsize/minsize.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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

28 changes: 10 additions & 18 deletions tasks/minsize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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)
Expand All @@ -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}");
}
}

Expand Down

0 comments on commit 2786dea

Please sign in to comment.