Skip to content

Commit b18cc1c

Browse files
committed
feat(minifier): remove unused variable declaration
part of #10033
1 parent 6f58abc commit b18cc1c

File tree

11 files changed

+135
-32
lines changed

11 files changed

+135
-32
lines changed

crates/oxc_minifier/examples/minifier.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ fn minify(
5050
let mut program = ret.program;
5151
let options = MinifierOptions {
5252
mangle: mangle.then(MangleOptions::default),
53-
compress: Some(CompressOptions::default()),
53+
compress: Some(CompressOptions::smallest()),
5454
};
5555
let ret = Minifier::new(options).build(allocator, &mut program);
5656
Codegen::new()

crates/oxc_minifier/src/options.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ pub struct CompressOptions {
2424
/// Default `false`
2525
pub drop_console: bool,
2626

27+
/// Drop unreferenced functions and variables.
28+
pub unused: CompressOptionsUnused,
29+
2730
/// Keep function / class names.
2831
pub keep_names: CompressOptionsKeepNames,
2932

@@ -46,6 +49,7 @@ impl CompressOptions {
4649
keep_names: CompressOptionsKeepNames::all_false(),
4750
drop_debugger: true,
4851
drop_console: true,
52+
unused: CompressOptionsUnused::Remove,
4953
treeshake: TreeShakeOptions::default(),
5054
}
5155
}
@@ -56,11 +60,20 @@ impl CompressOptions {
5660
keep_names: CompressOptionsKeepNames::all_true(),
5761
drop_debugger: false,
5862
drop_console: false,
63+
unused: CompressOptionsUnused::Keep,
5964
treeshake: TreeShakeOptions::default(),
6065
}
6166
}
6267
}
6368

69+
#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
70+
pub enum CompressOptionsUnused {
71+
#[default]
72+
Remove,
73+
KeepAssign,
74+
Keep,
75+
}
76+
6477
#[derive(Debug, Clone, Copy, Default)]
6578
pub struct CompressOptionsKeepNames {
6679
/// Keep function names so that `Function.prototype.name` is preserved.

crates/oxc_minifier/src/peephole/minimize_statements.rs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use oxc_semantic::ScopeId;
88
use oxc_span::{ContentEq, GetSpan};
99
use oxc_traverse::Ancestor;
1010

11-
use crate::{ctx::Ctx, keep_var::KeepVar};
11+
use crate::{CompressOptionsUnused, ctx::Ctx, keep_var::KeepVar};
1212

1313
use super::{PeepholeOptimizations, State};
1414

@@ -303,7 +303,7 @@ impl<'a> PeepholeOptimizations {
303303
result.push(Statement::ContinueStatement(s));
304304
}
305305
Statement::VariableDeclaration(var_decl) => {
306-
self.handle_variable_declaration(var_decl, result, state);
306+
self.handle_variable_declaration(var_decl, result, state, ctx);
307307
}
308308
Statement::ExpressionStatement(expr_stmt) => {
309309
self.handle_expression_statement(expr_stmt, result, state, ctx);
@@ -367,20 +367,59 @@ impl<'a> PeepholeOptimizations {
367367
false
368368
}
369369

370+
/// For variable declarations:
371+
/// * merge with the previous variable declarator if their kinds are the same
372+
/// * remove the variable declarator if it is unused
373+
/// * keep the initializer if it has side effects
370374
fn handle_variable_declaration(
371375
&self,
372-
mut var_decl: Box<'a, VariableDeclaration<'a>>,
376+
var_decl: Box<'a, VariableDeclaration<'a>>,
373377
result: &mut Vec<'a, Statement<'a>>,
374378
state: &mut State,
379+
ctx: &mut Ctx<'a, '_>,
375380
) {
376-
if let Some(Statement::VariableDeclaration(prev_var_decl)) = result.last_mut() {
381+
if let Some(Statement::VariableDeclaration(prev_var_decl)) = result.last() {
377382
if var_decl.kind == prev_var_decl.kind {
378-
prev_var_decl.declarations.extend(var_decl.declarations.drain(..));
379383
state.changed = true;
380-
return;
381384
}
382385
}
383-
result.push(Statement::VariableDeclaration(var_decl));
386+
let VariableDeclaration { span, kind, declarations, declare } = var_decl.unbox();
387+
for mut decl in declarations {
388+
if Self::is_declarator_unused(&decl, ctx) {
389+
state.changed = true;
390+
if let Some(init) = decl.init.take() {
391+
if init.may_have_side_effects(ctx) {
392+
result.push(ctx.ast.statement_expression(init.span(), init));
393+
}
394+
}
395+
} else {
396+
if let Some(Statement::VariableDeclaration(prev_var_decl)) = result.last_mut() {
397+
if kind == prev_var_decl.kind {
398+
prev_var_decl.declarations.push(decl);
399+
continue;
400+
}
401+
}
402+
let decls = ctx.ast.vec1(decl);
403+
let new_decl = ctx.ast.alloc_variable_declaration(span, kind, decls, declare);
404+
result.push(Statement::VariableDeclaration(new_decl));
405+
}
406+
}
407+
}
408+
409+
fn is_declarator_unused(decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) -> bool {
410+
if ctx.state.options.unused == CompressOptionsUnused::Keep {
411+
return false;
412+
}
413+
// It is unsafe to remove if direct eval is involved.
414+
if ctx.scoping().root_scope_flags().contains_direct_eval() {
415+
return false;
416+
}
417+
if let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind {
418+
if let Some(symbol_id) = ident.symbol_id.get() {
419+
return ctx.scoping().symbol_is_unused(symbol_id);
420+
}
421+
}
422+
false
384423
}
385424

386425
fn handle_expression_statement(

crates/oxc_minifier/src/peephole/mod.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ mod remove_unused_expression;
1616
mod replace_known_methods;
1717
mod substitute_alternate_syntax;
1818

19+
use oxc_ast_visit::Visit;
20+
use oxc_semantic::ReferenceId;
1921
use rustc_hash::FxHashSet;
2022

2123
use oxc_allocator::Vec;
@@ -99,8 +101,9 @@ impl<'a> PeepholeOptimizations {
99101

100102
#[inline]
101103
fn is_prev_function_changed(&self) -> bool {
102-
let (_, prev_changed, _) = self.current_function.last();
103-
*prev_changed
104+
true
105+
// let (_, prev_changed, _) = self.current_function.last();
106+
// *prev_changed
104107
}
105108

106109
fn enter_program_or_function(&mut self, scope_id: ScopeId) {
@@ -150,8 +153,20 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
150153
self.enter_program_or_function(program.scope_id());
151154
}
152155

153-
fn exit_program(&mut self, _program: &mut Program<'a>, _ctx: &mut TraverseCtx<'a>) {
156+
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
157+
let refs_before =
158+
ctx.scoping().resolved_references().flatten().copied().collect::<FxHashSet<_>>();
159+
154160
self.exit_program_or_function();
161+
162+
let mut counter = ReferencesCounter::default();
163+
counter.visit_program(program);
164+
let refs_after = counter.refs;
165+
166+
let removed_refs = refs_before.difference(&refs_after);
167+
for reference_id_to_remove in removed_refs {
168+
ctx.scoping_mut().delete_reference(*reference_id_to_remove);
169+
}
155170
}
156171

157172
fn enter_function(&mut self, func: &mut Function<'a>, _ctx: &mut TraverseCtx<'a>) {
@@ -489,3 +504,16 @@ impl<'a> Traverse<'a, MinifierState<'a>> for DeadCodeElimination {
489504
self.inner.remove_dead_code_exit_expression(expr, &mut state, &mut ctx);
490505
}
491506
}
507+
508+
#[derive(Default)]
509+
struct ReferencesCounter {
510+
refs: FxHashSet<ReferenceId>,
511+
}
512+
513+
impl<'a> Visit<'a> for ReferencesCounter {
514+
fn visit_identifier_reference(&mut self, it: &IdentifierReference<'a>) {
515+
if let Some(reference_id) = it.reference_id.get() {
516+
self.refs.insert(reference_id);
517+
}
518+
}
519+
}

crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1275,11 +1275,11 @@ mod test {
12751275
use crate::{
12761276
CompressOptions,
12771277
options::CompressOptionsKeepNames,
1278-
tester::{run, test, test_same},
1278+
tester::{default_options, run, test, test_same},
12791279
};
12801280

12811281
fn test_same_keep_names(keep_names: CompressOptionsKeepNames, code: &str) {
1282-
let result = run(code, Some(CompressOptions { keep_names, ..CompressOptions::smallest() }));
1282+
let result = run(code, Some(CompressOptions { keep_names, ..default_options() }));
12831283
let expected = run(code, None);
12841284
assert_eq!(result, expected, "\nfor source\n{code}\ngot\n{result}");
12851285
}

crates/oxc_minifier/src/tester.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ use oxc_codegen::{Codegen, CodegenOptions};
33
use oxc_parser::{ParseOptions, Parser};
44
use oxc_span::SourceType;
55

6-
use crate::{CompressOptions, Compressor};
6+
use crate::{CompressOptions, CompressOptionsUnused, Compressor};
7+
8+
pub fn default_options() -> CompressOptions {
9+
CompressOptions { unused: CompressOptionsUnused::Keep, ..CompressOptions::smallest() }
10+
}
711

812
#[track_caller]
913
pub fn test_same(source_text: &str) {
@@ -17,7 +21,7 @@ pub fn test_same_options(source_text: &str, options: &CompressOptions) {
1721

1822
#[track_caller]
1923
pub fn test(source_text: &str, expected: &str) {
20-
test_options(source_text, expected, &CompressOptions::smallest());
24+
test_options(source_text, expected, &default_options());
2125
}
2226

2327
#[track_caller]

crates/oxc_minifier/tests/peephole/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ mod minimize_exit_points;
55
mod oxc;
66
mod statement_fusion;
77

8-
use oxc_minifier::CompressOptions;
8+
use oxc_minifier::{CompressOptions, CompressOptionsUnused};
99

1010
#[track_caller]
1111
fn test(source_text: &str, expected: &str) {
12-
let options = CompressOptions { drop_debugger: false, ..CompressOptions::default() };
12+
let options = CompressOptions {
13+
drop_debugger: false,
14+
drop_console: false,
15+
unused: CompressOptionsUnused::Keep,
16+
..CompressOptions::smallest()
17+
};
1318
crate::test(source_text, expected, options);
1419
}
1520

crates/oxc_semantic/src/scoping.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,12 @@ impl Scoping {
370370
});
371371
}
372372

373+
/// Delete a reference.
374+
pub fn delete_reference(&mut self, reference_id: ReferenceId) {
375+
let Some(symbol_id) = self.get_reference(reference_id).symbol_id() else { return };
376+
self.delete_resolved_reference(symbol_id, reference_id);
377+
}
378+
373379
/// Delete a reference to a symbol.
374380
///
375381
/// # Panics

napi/minify/index.d.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ export interface CompressOptions {
2323
* @default 'esnext'
2424
*/
2525
target?: 'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024'
26-
/** Keep function / class names. */
27-
keepNames?: CompressOptionsKeepNames
2826
/**
2927
* Pass true to discard calls to `console.*`.
3028
*
@@ -37,6 +35,14 @@ export interface CompressOptions {
3735
* @default true
3836
*/
3937
dropDebugger?: boolean
38+
/**
39+
* Drop unreferenced functions and variables.
40+
*
41+
* Simple direct variable assignments do not count as references unless set to "keep_assign".
42+
*/
43+
unused?: true | false | 'keep_assign'
44+
/** Keep function / class names. */
45+
keepNames?: CompressOptionsKeepNames
4046
}
4147

4248
export interface CompressOptionsKeepNames {

napi/minify/src/options.rs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@ pub struct CompressOptions {
2323
)]
2424
pub target: Option<String>,
2525

26-
/// Keep function / class names.
27-
pub keep_names: Option<CompressOptionsKeepNames>,
28-
2926
/// Pass true to discard calls to `console.*`.
3027
///
3128
/// @default false
@@ -35,12 +32,15 @@ pub struct CompressOptions {
3532
///
3633
/// @default true
3734
pub drop_debugger: Option<bool>,
38-
}
3935

40-
impl Default for CompressOptions {
41-
fn default() -> Self {
42-
Self { target: None, keep_names: None, drop_console: None, drop_debugger: Some(true) }
43-
}
36+
/// Drop unreferenced functions and variables.
37+
///
38+
/// Simple direct variable assignments do not count as references unless set to "keep_assign".
39+
#[napi(ts_type = "true | false | 'keep_assign'")]
40+
pub unused: Option<String>,
41+
42+
/// Keep function / class names.
43+
pub keep_names: Option<CompressOptionsKeepNames>,
4444
}
4545

4646
impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions {
@@ -56,6 +56,8 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions {
5656
.unwrap_or(default.target),
5757
drop_console: o.drop_console.unwrap_or(default.drop_console),
5858
drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger),
59+
// TODO
60+
unused: oxc_minifier::CompressOptionsUnused::Keep,
5961
keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
6062
treeshake: TreeShakeOptions::default(),
6163
})

0 commit comments

Comments
 (0)