Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(minifier): add RemoveUnusedCode #8210

Merged
merged 1 commit into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading