mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(minifier): add RemoveUnusedCode (#8210)
This commit is contained in:
parent
d2d90b077b
commit
2786dea164
11 changed files with 121 additions and 30 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1777,6 +1777,7 @@ dependencies = [
|
|||
"oxc_syntax",
|
||||
"oxc_traverse",
|
||||
"pico-args",
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
90
crates/oxc_minifier/src/ast_passes/remove_unused_code.rs
Normal file
90
crates/oxc_minifier/src/ast_passes/remove_unused_code.rs
Normal file
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue