Skip to content

Commit aac45ef

Browse files
committed
feat(minifier): remove unused private class members (#14026)
Compress ```js class C { #unused = 1; #used = 2; method() { return this.#used; } } ``` to ```js class C { #used = 2; method() { return this.#used; } } ``` . The symbol usage is collected in the main traverse. The removal happens in `exit_class_body`. This way we don't need additional traverse.
1 parent 314c27d commit aac45ef

File tree

4 files changed

+228
-5
lines changed

4 files changed

+228
-5
lines changed

crates/oxc_minifier/src/peephole/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod normalize;
1313
mod remove_dead_code;
1414
mod remove_unused_declaration;
1515
mod remove_unused_expression;
16+
mod remove_unused_private_members;
1617
mod replace_known_methods;
1718
mod substitute_alternate_syntax;
1819

@@ -123,6 +124,7 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
123124
ctx.scoping_mut().delete_reference(*reference_id_to_remove);
124125
}
125126
}
127+
debug_assert!(ctx.state.class_symbols_stack.is_empty());
126128
}
127129

128130
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
@@ -362,15 +364,37 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
362364
Self::convert_to_dotted_properties(expr, &ctx);
363365
}
364366

367+
fn enter_class_body(&mut self, _body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
368+
ctx.state.class_symbols_stack.push_class_scope();
369+
}
370+
365371
fn exit_class_body(&mut self, body: &mut ClassBody<'a>, ctx: &mut TraverseCtx<'a>) {
366372
let ctx = &mut Ctx::new(ctx);
367373
Self::remove_dead_code_exit_class_body(body, ctx);
374+
Self::remove_unused_private_members(body, ctx);
375+
ctx.state.class_symbols_stack.pop_class_scope();
368376
}
369377

370378
fn exit_catch_clause(&mut self, catch: &mut CatchClause<'a>, ctx: &mut TraverseCtx<'a>) {
371379
let ctx = Ctx::new(ctx);
372380
Self::substitute_catch_clause(catch, &ctx);
373381
}
382+
383+
fn exit_private_field_expression(
384+
&mut self,
385+
node: &mut PrivateFieldExpression<'a>,
386+
ctx: &mut TraverseCtx<'a>,
387+
) {
388+
ctx.state.class_symbols_stack.push_private_member_to_current_class(node.field.name);
389+
}
390+
391+
fn exit_private_in_expression(
392+
&mut self,
393+
node: &mut PrivateInExpression<'a>,
394+
ctx: &mut TraverseCtx<'a>,
395+
) {
396+
ctx.state.class_symbols_stack.push_private_member_to_current_class(node.left.name);
397+
}
374398
}
375399

376400
pub struct DeadCodeElimination {
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
use oxc_ast::ast::*;
2+
use oxc_ecmascript::side_effects::MayHaveSideEffects;
3+
4+
use crate::ctx::Ctx;
5+
6+
use super::PeepholeOptimizations;
7+
8+
impl<'a> PeepholeOptimizations {
9+
/// Remove unused private fields and methods from class body
10+
///
11+
/// This function uses the private member usage collected during the main traverse
12+
/// to remove unused private fields and methods from the class body.
13+
pub fn remove_unused_private_members(body: &mut ClassBody<'a>, ctx: &mut Ctx<'a, '_>) {
14+
let old_len = body.body.len();
15+
body.body.retain(|element| match element {
16+
ClassElement::PropertyDefinition(prop) => {
17+
let PropertyKey::PrivateIdentifier(private_id) = &prop.key else {
18+
return true;
19+
};
20+
if ctx
21+
.state
22+
.class_symbols_stack
23+
.is_private_member_used_in_current_class(&private_id.name)
24+
{
25+
return true;
26+
}
27+
prop.value.as_ref().is_some_and(|value| value.may_have_side_effects(ctx))
28+
}
29+
ClassElement::MethodDefinition(method) => {
30+
let PropertyKey::PrivateIdentifier(private_id) = &method.key else {
31+
return true;
32+
};
33+
ctx.state
34+
.class_symbols_stack
35+
.is_private_member_used_in_current_class(&private_id.name)
36+
}
37+
ClassElement::AccessorProperty(accessor) => {
38+
let PropertyKey::PrivateIdentifier(private_id) = &accessor.key else {
39+
return true;
40+
};
41+
if ctx
42+
.state
43+
.class_symbols_stack
44+
.is_private_member_used_in_current_class(&private_id.name)
45+
{
46+
return true;
47+
}
48+
accessor.value.as_ref().is_some_and(|value| value.may_have_side_effects(ctx))
49+
}
50+
ClassElement::StaticBlock(_) => true,
51+
ClassElement::TSIndexSignature(_) => {
52+
unreachable!("TypeScript syntax should be transformed away")
53+
}
54+
});
55+
if body.body.len() != old_len {
56+
ctx.state.changed = true;
57+
}
58+
}
59+
}
60+
61+
#[cfg(test)]
62+
mod test {
63+
use crate::tester::{test, test_same};
64+
65+
#[test]
66+
fn test_remove_unused_private_fields() {
67+
test(
68+
"class C { #unused = 1; #used = 2; method() { return this.#used; } } new C();",
69+
"class C { #used = 2; method() { return this.#used; } } new C();",
70+
);
71+
test(
72+
"class C { #unused = 1; #used = 2; method(foo) { return #used in foo; } } new C();",
73+
"class C { #used = 2; method(foo) { return #used in foo; } } new C();",
74+
);
75+
test(
76+
"class C { #unused; #used; method() { return this.#used; } } new C();",
77+
"class C { #used; method() { return this.#used; } } new C();",
78+
);
79+
test("class C { #a = 1; #b = 2; #c = 3; } new C();", "class C { } new C();");
80+
test(
81+
"class C { static #unused = 1; static #used = 2; static method() { return C.#used; } }",
82+
"class C { static #used = 2; static method() { return C.#used; } }",
83+
);
84+
test(
85+
"class C { public = 1; #unused = 2; #used = 3; method() { return this.public + this.#used; } } new C();",
86+
"class C { public = 1; #used = 3; method() { return this.public + this.#used; } } new C();",
87+
);
88+
test_same("class C { #unused = foo(); method() { return 1; } } new C();");
89+
}
90+
91+
#[test]
92+
fn test_remove_unused_private_methods() {
93+
test(
94+
"class C { #unusedMethod() { return 1; } #usedMethod() { return 2; } method() { return this.#usedMethod(); } } new C();",
95+
"class C { #usedMethod() { return 2; } method() { return this.#usedMethod(); } } new C();",
96+
);
97+
test("class C { #a() {} #b() {} #c() {} } new C();", "class C { } new C();");
98+
test(
99+
"class C { static #unusedMethod() { return 1; } static #usedMethod() { return 2; } static method() { return C.#usedMethod(); } }",
100+
"class C { static #usedMethod() { return 2; } static method() { return C.#usedMethod(); } }",
101+
);
102+
test_same(
103+
"class C { #helper() { return 1; } method() { return this.#helper(); } } new C();",
104+
);
105+
}
106+
107+
#[test]
108+
fn test_remove_unused_private_accessors() {
109+
test(
110+
"class C { accessor #unused = 1; accessor #used = 2; method() { return this.#used; } } new C();",
111+
"class C { accessor #used = 2; method() { return this.#used; } } new C();",
112+
);
113+
test_same("class C { accessor #unused = foo(); method() { return 1; } } new C();");
114+
}
115+
116+
#[test]
117+
fn test_nested_classes() {
118+
test(
119+
r"class Outer {
120+
#shared = 1;
121+
#unusedOuter = 2;
122+
123+
method() {
124+
return this.#shared;
125+
}
126+
127+
getInner() {
128+
return class Inner {
129+
#shared = 3;
130+
#unusedInner = 4;
131+
132+
method() {
133+
return this.#shared;
134+
}
135+
};
136+
}
137+
} new Outer();",
138+
r"class Outer {
139+
#shared = 1;
140+
141+
method() {
142+
return this.#shared;
143+
}
144+
145+
getInner() {
146+
return class {
147+
#shared = 3;
148+
149+
method() {
150+
return this.#shared;
151+
}
152+
};
153+
}
154+
} new Outer();",
155+
);
156+
}
157+
}

crates/oxc_minifier/src/state.rs

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use oxc_ecmascript::constant_evaluation::ConstantValue;
2-
use rustc_hash::FxHashMap;
2+
use rustc_hash::{FxHashMap, FxHashSet};
33

4-
use oxc_span::SourceType;
4+
use oxc_span::{Atom, SourceType};
55
use oxc_syntax::symbol::SymbolId;
66

77
use crate::{CompressOptions, symbol_value::SymbolValues};
@@ -16,6 +16,9 @@ pub struct MinifierState<'a> {
1616

1717
pub symbol_values: SymbolValues<'a>,
1818

19+
/// Private member usage for classes
20+
pub class_symbols_stack: ClassSymbolsStack<'a>,
21+
1922
pub changed: bool,
2023
}
2124

@@ -26,7 +29,46 @@ impl MinifierState<'_> {
2629
options,
2730
pure_functions: FxHashMap::default(),
2831
symbol_values: SymbolValues::default(),
32+
class_symbols_stack: ClassSymbolsStack::new(),
2933
changed: false,
3034
}
3135
}
3236
}
37+
38+
/// Stack to track class symbol information
39+
pub struct ClassSymbolsStack<'a> {
40+
stack: Vec<FxHashSet<Atom<'a>>>,
41+
}
42+
43+
impl<'a> ClassSymbolsStack<'a> {
44+
pub fn new() -> Self {
45+
Self { stack: Vec::new() }
46+
}
47+
48+
/// Check if the stack is empty
49+
pub fn is_empty(&self) -> bool {
50+
self.stack.is_empty()
51+
}
52+
53+
/// Enter a new class scope
54+
pub fn push_class_scope(&mut self) {
55+
self.stack.push(FxHashSet::default());
56+
}
57+
58+
/// Exit the current class scope
59+
pub fn pop_class_scope(&mut self) {
60+
self.stack.pop();
61+
}
62+
63+
/// Add a private member to the current class scope
64+
pub fn push_private_member_to_current_class(&mut self, name: Atom<'a>) {
65+
if let Some(current_class) = self.stack.last_mut() {
66+
current_class.insert(name);
67+
}
68+
}
69+
70+
/// Check if a private member is used in the current class scope
71+
pub fn is_private_member_used_in_current_class(&self, name: &Atom<'a>) -> bool {
72+
self.stack.last().is_some_and(|current_class| current_class.contains(name))
73+
}
74+
}

tasks/track_memory_allocations/allocs_minifier.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs | Arena bytes
22
-------------------------------------------------------------------------------------------------------------------------------------------
3-
checker.ts | 2.92 MB || 84073 | 14190 || 153691 | 29463 | 5.625 MB
3+
checker.ts | 2.92 MB || 84074 | 14190 || 153691 | 29463 | 5.625 MB
44

5-
cal.com.tsx | 1.06 MB || 40525 | 3033 || 37074 | 4733 | 1.654 MB
5+
cal.com.tsx | 1.06 MB || 40526 | 3033 || 37074 | 4733 | 1.654 MB
66

77
RadixUIAdoptionSection.jsx | 2.52 kB || 82 | 8 || 30 | 6 | 992 B
88

9-
pdf.mjs | 567.30 kB || 19576 | 2900 || 47400 | 7781 | 1.624 MB
9+
pdf.mjs | 567.30 kB || 19823 | 2900 || 47400 | 7781 | 1.624 MB
1010

1111
antd.js | 6.69 MB || 99854 | 13518 || 331725 | 70117 | 17.407 MB
1212

0 commit comments

Comments
 (0)