Skip to content

Commit 7a05e71

Browse files
committed
feat(minifier)!: add Treeshake options (#11786)
closes #9527
1 parent d5a8f18 commit 7a05e71

File tree

10 files changed

+139
-52
lines changed

10 files changed

+139
-52
lines changed

crates/oxc_ecmascript/src/side_effects/context.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,28 @@ pub trait MayHaveSideEffectsContext: IsGlobalReference {
1919
///
2020
/// Pure annotations are the comments that marks that a expression is pure.
2121
/// For example, `/* @__PURE__ */`, `/* #__NO_SIDE_EFFECTS__ */`.
22-
fn respect_annotations(&self) -> bool;
22+
///
23+
/// <https://rollupjs.org/configuration-options/#treeshake-annotations>
24+
fn annotations(&self) -> bool;
2325

2426
/// Whether to treat this function call as pure.
2527
///
2628
/// This function is called for normal function calls, new calls, and
2729
/// tagged template calls (`foo()`, `new Foo()`, ``foo`b` ``).
28-
fn is_pure_call(&self, callee: &Expression) -> bool;
30+
///
31+
/// <https://rollupjs.org/configuration-options/#treeshake-manualpurefunctions>
32+
fn manual_pure_functions(&self, callee: &Expression) -> bool;
2933

3034
/// Whether property read accesses have side effects.
35+
///
36+
/// <https://rollupjs.org/configuration-options/#treeshake-propertyreadsideeffects>
3137
fn property_read_side_effects(&self) -> PropertyReadSideEffects;
3238

3339
/// Whether accessing a global variable has side effects.
3440
///
3541
/// Accessing a non-existing global variable will throw an error.
3642
/// Global variable may be a getter that has side effects.
43+
///
44+
/// <https://rollupjs.org/configuration-options/#treeshake-unknownglobalsideeffects>
3745
fn unknown_global_side_effects(&self) -> bool;
3846
}

crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ fn get_array_minimum_length(arr: &ArrayExpression) -> usize {
495495

496496
impl MayHaveSideEffects for CallExpression<'_> {
497497
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool {
498-
if (self.pure && ctx.respect_annotations()) || ctx.is_pure_call(&self.callee) {
498+
if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
499499
self.arguments.iter().any(|e| e.may_have_side_effects(ctx))
500500
} else {
501501
true
@@ -505,7 +505,7 @@ impl MayHaveSideEffects for CallExpression<'_> {
505505

506506
impl MayHaveSideEffects for NewExpression<'_> {
507507
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool {
508-
if (self.pure && ctx.respect_annotations()) || ctx.is_pure_call(&self.callee) {
508+
if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
509509
self.arguments.iter().any(|e| e.may_have_side_effects(ctx))
510510
} else {
511511
true
@@ -515,7 +515,11 @@ impl MayHaveSideEffects for NewExpression<'_> {
515515

516516
impl MayHaveSideEffects for TaggedTemplateExpression<'_> {
517517
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool {
518-
if ctx.is_pure_call(&self.tag) { self.quasi.may_have_side_effects(ctx) } else { true }
518+
if ctx.manual_pure_functions(&self.tag) {
519+
self.quasi.may_have_side_effects(ctx)
520+
} else {
521+
true
522+
}
519523
}
520524
}
521525

crates/oxc_minifier/src/compressor.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::rc::Rc;
2+
13
use oxc_allocator::Allocator;
24
use oxc_ast::ast::*;
35
use oxc_semantic::{Scoping, SemanticBuilder};
@@ -14,12 +16,12 @@ use crate::{
1416

1517
pub struct Compressor<'a> {
1618
allocator: &'a Allocator,
17-
options: CompressOptions,
19+
options: Rc<CompressOptions>,
1820
}
1921

2022
impl<'a> Compressor<'a> {
2123
pub fn new(allocator: &'a Allocator, options: CompressOptions) -> Self {
22-
Self { allocator, options }
24+
Self { allocator, options: Rc::new(options) }
2325
}
2426

2527
pub fn build(self, program: &mut Program<'a>) {
@@ -28,11 +30,11 @@ impl<'a> Compressor<'a> {
2830
}
2931

3032
pub fn build_with_scoping(self, scoping: Scoping, program: &mut Program<'a>) {
31-
let state = MinifierState::default();
33+
let state = MinifierState::new(Rc::clone(&self.options));
3234
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
3335
let normalize_options =
3436
NormalizeOptions { convert_while_to_fors: true, convert_const_to_let: true };
35-
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
37+
Normalize::new(normalize_options).build(program, &mut ctx);
3638
PeepholeOptimizations::new(self.options.target, self.options.keep_names)
3739
.run_in_loop(program, &mut ctx);
3840
LatePeepholeOptimizations::new(self.options.target).build(program, &mut ctx);
@@ -44,11 +46,11 @@ impl<'a> Compressor<'a> {
4446
}
4547

4648
pub fn dead_code_elimination_with_scoping(self, scoping: Scoping, program: &mut Program<'a>) {
47-
let state = MinifierState::default();
49+
let state = MinifierState::new(Rc::clone(&self.options));
4850
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
4951
let normalize_options =
5052
NormalizeOptions { convert_while_to_fors: false, convert_const_to_let: false };
51-
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
53+
Normalize::new(normalize_options).build(program, &mut ctx);
5254
DeadCodeElimination::new().build(program, &mut ctx);
5355
}
5456
}

crates/oxc_minifier/src/ctx.rs

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{marker::PhantomData, ops::Deref};
1+
use std::{marker::PhantomData, ops::Deref, rc::Rc};
22

33
use oxc_ast::{AstBuilder, ast::*};
44
use oxc_ecmascript::constant_evaluation::{
@@ -8,9 +8,17 @@ use oxc_ecmascript::side_effects::{MayHaveSideEffects, PropertyReadSideEffects};
88
use oxc_semantic::{IsGlobalReference, Scoping};
99
use oxc_span::format_atom;
1010

11-
#[derive(Default)]
11+
use crate::CompressOptions;
12+
1213
pub struct MinifierState<'a> {
13-
data: PhantomData<&'a ()>,
14+
pub options: Rc<CompressOptions>,
15+
_data: PhantomData<&'a ()>,
16+
}
17+
18+
impl MinifierState<'_> {
19+
pub fn new(options: Rc<CompressOptions>) -> Self {
20+
Self { options, _data: PhantomData }
21+
}
1422
}
1523

1624
pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>;
@@ -38,20 +46,29 @@ impl oxc_ecmascript::is_global_reference::IsGlobalReference for Ctx<'_, '_> {
3846
}
3947

4048
impl oxc_ecmascript::side_effects::MayHaveSideEffectsContext for Ctx<'_, '_> {
41-
fn respect_annotations(&self) -> bool {
42-
true
43-
}
44-
45-
fn is_pure_call(&self, _callee: &Expression) -> bool {
49+
fn annotations(&self) -> bool {
50+
self.state.options.treeshake.annotations
51+
}
52+
53+
fn manual_pure_functions(&self, callee: &Expression) -> bool {
54+
if let Expression::Identifier(ident) = callee {
55+
return self
56+
.state
57+
.options
58+
.treeshake
59+
.manual_pure_functions
60+
.iter()
61+
.any(|name| ident.name.as_str() == name);
62+
}
4663
false
4764
}
4865

4966
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
50-
PropertyReadSideEffects::All
67+
self.state.options.treeshake.property_read_side_effects
5168
}
5269

5370
fn unknown_global_side_effects(&self) -> bool {
54-
true
71+
self.state.options.treeshake.unknown_global_side_effects
5572
}
5673
}
5774

crates/oxc_minifier/src/lib.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ use oxc_semantic::{Scoping, SemanticBuilder, Stats};
1818

1919
pub use oxc_mangler::{MangleOptions, MangleOptionsKeepNames};
2020

21-
pub use crate::{
22-
compressor::Compressor, options::CompressOptions, options::CompressOptionsKeepNames,
23-
};
21+
pub use crate::{compressor::Compressor, options::*};
2422

25-
#[derive(Debug, Clone, Copy)]
23+
#[derive(Debug, Clone)]
2624
pub struct MinifierOptions {
2725
pub mangle: Option<MangleOptions>,
2826
pub compress: Option<CompressOptions>,

crates/oxc_minifier/src/options.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use oxc_syntax::es_target::ESTarget;
22

3-
#[derive(Debug, Clone, Copy)]
3+
pub use oxc_ecmascript::side_effects::PropertyReadSideEffects;
4+
5+
#[derive(Debug, Clone)]
46
pub struct CompressOptions {
57
/// Set desired EcmaScript standard version for output.
68
///
@@ -12,9 +14,6 @@ pub struct CompressOptions {
1214
/// Default `ESTarget::ESNext`
1315
pub target: ESTarget,
1416

15-
/// Keep function / class names.
16-
pub keep_names: CompressOptionsKeepNames,
17-
1817
/// Remove `debugger;` statements.
1918
///
2019
/// Default `true`
@@ -24,6 +23,13 @@ pub struct CompressOptions {
2423
///
2524
/// Default `false`
2625
pub drop_console: bool,
26+
27+
/// Keep function / class names.
28+
pub keep_names: CompressOptionsKeepNames,
29+
30+
/// Treeshake Options .
31+
/// <https://rollupjs.org/configuration-options/#treeshake>
32+
pub treeshake: TreeShakeOptions,
2733
}
2834

2935
#[expect(clippy::derivable_impls)]
@@ -40,6 +46,7 @@ impl CompressOptions {
4046
keep_names: CompressOptionsKeepNames::all_false(),
4147
drop_debugger: true,
4248
drop_console: true,
49+
treeshake: TreeShakeOptions::default(),
4350
}
4451
}
4552

@@ -49,6 +56,7 @@ impl CompressOptions {
4956
keep_names: CompressOptionsKeepNames::all_true(),
5057
drop_debugger: false,
5158
drop_console: false,
59+
treeshake: TreeShakeOptions::default(),
5260
}
5361
}
5462
}
@@ -87,3 +95,52 @@ impl CompressOptionsKeepNames {
8795
Self { function: false, class: true }
8896
}
8997
}
98+
99+
#[derive(Debug, Clone)]
100+
pub struct TreeShakeOptions {
101+
/// Whether to respect the pure annotations.
102+
///
103+
/// Pure annotations are the comments that marks that a expression is pure.
104+
/// For example, `/* @__PURE__ */`, `/* #__NO_SIDE_EFFECTS__ */`.
105+
///
106+
/// <https://rollupjs.org/configuration-options/#treeshake-annotations>
107+
///
108+
/// Default `true`
109+
pub annotations: bool,
110+
111+
/// Whether to treat this function call as pure.
112+
///
113+
/// This function is called for normal function calls, new calls, and
114+
/// tagged template calls (`foo()`, `new Foo()`, ``foo`b` ``).
115+
///
116+
/// <https://rollupjs.org/configuration-options/#treeshake-manualpurefunctions>
117+
pub manual_pure_functions: Vec<String>,
118+
119+
/// Whether property read accesses have side effects.
120+
///
121+
/// <https://rollupjs.org/configuration-options/#treeshake-propertyreadsideeffects>
122+
///
123+
/// Default [PropertyReadSideEffects::All]
124+
pub property_read_side_effects: PropertyReadSideEffects,
125+
126+
/// Whether accessing a global variable has side effects.
127+
///
128+
/// Accessing a non-existing global variable will throw an error.
129+
/// Global variable may be a getter that has side effects.
130+
///
131+
/// <https://rollupjs.org/configuration-options/#treeshake-unknownglobalsideeffects>
132+
///
133+
/// Default `true`
134+
pub unknown_global_side_effects: bool,
135+
}
136+
137+
impl Default for TreeShakeOptions {
138+
fn default() -> Self {
139+
Self {
140+
annotations: true,
141+
manual_pure_functions: vec![],
142+
property_read_side_effects: PropertyReadSideEffects::default(),
143+
unknown_global_side_effects: true,
144+
}
145+
}
146+
}

crates/oxc_minifier/src/peephole/normalize.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ use oxc_span::GetSpan;
66
use oxc_syntax::scope::ScopeFlags;
77
use oxc_traverse::{Ancestor, ReusableTraverseCtx, Traverse, traverse_mut_with_ctx};
88

9-
use crate::{
10-
CompressOptions,
11-
ctx::{Ctx, MinifierState, TraverseCtx},
12-
};
9+
use crate::ctx::{Ctx, MinifierState, TraverseCtx};
1310

1411
#[derive(Default)]
1512
pub struct NormalizeOptions {
@@ -38,7 +35,6 @@ pub struct NormalizeOptions {
3835
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/Normalize.java>
3936
pub struct Normalize {
4037
options: NormalizeOptions,
41-
compress_options: CompressOptions,
4238
}
4339

4440
impl<'a> Normalize {
@@ -52,11 +48,11 @@ impl<'a> Normalize {
5248
}
5349

5450
impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
55-
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) {
51+
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
5652
stmts.retain(|stmt| {
5753
!(matches!(stmt, Statement::EmptyStatement(_))
58-
|| self.drop_debugger(stmt)
59-
|| self.drop_console(stmt))
54+
|| Self::drop_debugger(stmt, ctx)
55+
|| Self::drop_console(stmt, ctx))
6056
});
6157
}
6258

@@ -90,10 +86,10 @@ impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
9086
None
9187
}
9288
Expression::ArrowFunctionExpression(e) => {
93-
self.recover_arrow_expression_after_drop_console(e);
89+
Self::recover_arrow_expression_after_drop_console(e, ctx);
9490
None
9591
}
96-
Expression::CallExpression(_) if self.compress_options.drop_console => {
92+
Expression::CallExpression(_) if ctx.state.options.drop_console => {
9793
self.compress_console(expr, ctx)
9894
}
9995
Expression::StaticMemberExpression(e) => Self::fold_number_nan_to_nan(e, ctx),
@@ -113,33 +109,36 @@ impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
113109
}
114110

115111
impl<'a> Normalize {
116-
pub fn new(options: NormalizeOptions, compress_options: CompressOptions) -> Self {
117-
Self { options, compress_options }
112+
pub fn new(options: NormalizeOptions) -> Self {
113+
Self { options }
118114
}
119115

120116
/// Drop `drop_debugger` statement.
121117
///
122118
/// Enabled by `compress.drop_debugger`
123-
fn drop_debugger(&self, stmt: &Statement<'a>) -> bool {
124-
matches!(stmt, Statement::DebuggerStatement(_)) && self.compress_options.drop_debugger
119+
fn drop_debugger(stmt: &Statement<'a>, ctx: &TraverseCtx<'a>) -> bool {
120+
matches!(stmt, Statement::DebuggerStatement(_)) && ctx.state.options.drop_debugger
125121
}
126122

127123
fn compress_console(
128124
&self,
129125
expr: &Expression<'a>,
130126
ctx: &TraverseCtx<'a>,
131127
) -> Option<Expression<'a>> {
132-
debug_assert!(self.compress_options.drop_console);
128+
debug_assert!(ctx.state.options.drop_console);
133129
Self::is_console(expr).then(|| ctx.ast.void_0(expr.span()))
134130
}
135131

136-
fn drop_console(&self, stmt: &Statement<'a>) -> bool {
137-
self.compress_options.drop_console
132+
fn drop_console(stmt: &Statement<'a>, ctx: &TraverseCtx<'a>) -> bool {
133+
ctx.state.options.drop_console
138134
&& matches!(stmt, Statement::ExpressionStatement(expr) if Self::is_console(&expr.expression))
139135
}
140136

141-
fn recover_arrow_expression_after_drop_console(&self, expr: &mut ArrowFunctionExpression<'a>) {
142-
if self.compress_options.drop_console && expr.expression && expr.body.is_empty() {
137+
fn recover_arrow_expression_after_drop_console(
138+
expr: &mut ArrowFunctionExpression<'a>,
139+
ctx: &TraverseCtx<'a>,
140+
) {
141+
if ctx.state.options.drop_console && expr.expression && expr.body.is_empty() {
143142
expr.expression = false;
144143
}
145144
}

crates/oxc_minifier/tests/ecmascript/may_have_side_effects.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ impl IsGlobalReference for Ctx {
3131
}
3232
}
3333
impl MayHaveSideEffectsContext for Ctx {
34-
fn respect_annotations(&self) -> bool {
34+
fn annotations(&self) -> bool {
3535
self.annotation
3636
}
3737

38-
fn is_pure_call(&self, callee: &Expression) -> bool {
38+
fn manual_pure_functions(&self, callee: &Expression) -> bool {
3939
if let Expression::Identifier(id) = callee {
4040
self.pure_function_names.iter().any(|name| name == id.name.as_str())
4141
} else {

0 commit comments

Comments
 (0)