feat(minifier)!: add Treeshake options (#11786)

closes #9527
This commit is contained in:
Boshen committed 2025-06-18 06:42:03 +00:00
1 parent d5a8f18cc1
commit 7a05e711d5
10 files changed
+137 -50

No files matched your search

@@ -19,20 +19,28 @@ pub trait MayHaveSideEffectsContext: IsGlobalReference {
///
/// Pure annotations are the comments that marks that a expression is pure.
/// For example, `/* @__PURE__ */`, `/* #__NO_SIDE_EFFECTS__ */`.
fn respect_annotations(&self) -> bool;
///
/// <https://rollupjs.org/configuration-options/#treeshake-annotations>
fn annotations(&self) -> bool;
/// Whether to treat this function call as pure.
///
/// This function is called for normal function calls, new calls, and
/// tagged template calls (`foo()`, `new Foo()`, ``foo`b` ``).
fn is_pure_call(&self, callee: &Expression) -> bool;
///
/// <https://rollupjs.org/configuration-options/#treeshake-manualpurefunctions>
fn manual_pure_functions(&self, callee: &Expression) -> bool;
/// Whether property read accesses have side effects.
///
/// <https://rollupjs.org/configuration-options/#treeshake-propertyreadsideeffects>
fn property_read_side_effects(&self) -> PropertyReadSideEffects;
/// Whether accessing a global variable has side effects.
///
/// Accessing a non-existing global variable will throw an error.
/// Global variable may be a getter that has side effects.
///
/// <https://rollupjs.org/configuration-options/#treeshake-unknownglobalsideeffects>
fn unknown_global_side_effects(&self) -> bool;
}
@@ -495,7 +495,7 @@ fn get_array_minimum_length(arr: &ArrayExpression) -> usize {
impl MayHaveSideEffects for CallExpression<'_> {
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool {
if (self.pure && ctx.respect_annotations()) || ctx.is_pure_call(&self.callee) {
if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
self.arguments.iter().any(|e| e.may_have_side_effects(ctx))
} else {
true
@@ -505,7 +505,7 @@ impl MayHaveSideEffects for CallExpression<'_> {
impl MayHaveSideEffects for NewExpression<'_> {
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool {
if (self.pure && ctx.respect_annotations()) || ctx.is_pure_call(&self.callee) {
if (self.pure && ctx.annotations()) || ctx.manual_pure_functions(&self.callee) {
self.arguments.iter().any(|e| e.may_have_side_effects(ctx))
} else {
true
@@ -515,7 +515,11 @@ impl MayHaveSideEffects for NewExpression<'_> {
impl MayHaveSideEffects for TaggedTemplateExpression<'_> {
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext) -> bool {
if ctx.is_pure_call(&self.tag) { self.quasi.may_have_side_effects(ctx) } else { true }
if ctx.manual_pure_functions(&self.tag) {
self.quasi.may_have_side_effects(ctx)
} else {
true
}
}
}
+8 -6
View File
@@ -1,3 +1,5 @@
use std::rc::Rc;
use oxc_allocator::Allocator;
use oxc_ast::ast::*;
use oxc_semantic::{Scoping, SemanticBuilder};
@@ -14,12 +16,12 @@ use crate::{
pub struct Compressor<'a> {
allocator: &'a Allocator,
options: CompressOptions,
options: Rc<CompressOptions>,
}
impl<'a> Compressor<'a> {
pub fn new(allocator: &'a Allocator, options: CompressOptions) -> Self {
Self { allocator, options }
Self { allocator, options: Rc::new(options) }
}
pub fn build(self, program: &mut Program<'a>) {
@@ -28,11 +30,11 @@ impl<'a> Compressor<'a> {
}
pub fn build_with_scoping(self, scoping: Scoping, program: &mut Program<'a>) {
let state = MinifierState::default();
let state = MinifierState::new(Rc::clone(&self.options));
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
let normalize_options =
NormalizeOptions { convert_while_to_fors: true, convert_const_to_let: true };
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
Normalize::new(normalize_options).build(program, &mut ctx);
PeepholeOptimizations::new(self.options.target, self.options.keep_names)
.run_in_loop(program, &mut ctx);
LatePeepholeOptimizations::new(self.options.target).build(program, &mut ctx);
@@ -44,11 +46,11 @@ impl<'a> Compressor<'a> {
}
pub fn dead_code_elimination_with_scoping(self, scoping: Scoping, program: &mut Program<'a>) {
let state = MinifierState::default();
let state = MinifierState::new(Rc::clone(&self.options));
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
let normalize_options =
NormalizeOptions { convert_while_to_fors: false, convert_const_to_let: false };
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
Normalize::new(normalize_options).build(program, &mut ctx);
DeadCodeElimination::new().build(program, &mut ctx);
}
}
+25 -8
View File
@@ -1,4 +1,4 @@
use std::{marker::PhantomData, ops::Deref};
use std::{marker::PhantomData, ops::Deref, rc::Rc};
use oxc_ast::{AstBuilder, ast::*};
use oxc_ecmascript::constant_evaluation::{
@@ -8,9 +8,17 @@ use oxc_ecmascript::side_effects::{MayHaveSideEffects, PropertyReadSideEffects};
use oxc_semantic::{IsGlobalReference, Scoping};
use oxc_span::format_atom;
#[derive(Default)]
use crate::CompressOptions;
pub struct MinifierState<'a> {
data: PhantomData<&'a ()>,
pub options: Rc<CompressOptions>,
_data: PhantomData<&'a ()>,
}
impl MinifierState<'_> {
pub fn new(options: Rc<CompressOptions>) -> Self {
Self { options, _data: PhantomData }
}
}
pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>;
@@ -38,20 +46,29 @@ impl oxc_ecmascript::is_global_reference::IsGlobalReference for Ctx<'_, '_> {
}
impl oxc_ecmascript::side_effects::MayHaveSideEffectsContext for Ctx<'_, '_> {
fn respect_annotations(&self) -> bool {
true
fn annotations(&self) -> bool {
self.state.options.treeshake.annotations
}
fn is_pure_call(&self, _callee: &Expression) -> bool {
fn manual_pure_functions(&self, callee: &Expression) -> bool {
if let Expression::Identifier(ident) = callee {
return self
.state
.options
.treeshake
.manual_pure_functions
.iter()
.any(|name| ident.name.as_str() == name);
}
false
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
PropertyReadSideEffects::All
self.state.options.treeshake.property_read_side_effects
}
fn unknown_global_side_effects(&self) -> bool {
true
self.state.options.treeshake.unknown_global_side_effects
}
}
+2 -4
View File
@@ -18,11 +18,9 @@ use oxc_semantic::{Scoping, SemanticBuilder, Stats};
pub use oxc_mangler::{MangleOptions, MangleOptionsKeepNames};
pub use crate::{
compressor::Compressor, options::CompressOptions, options::CompressOptionsKeepNames,
};
pub use crate::{compressor::Compressor, options::*};
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct MinifierOptions {
pub mangle: Option<MangleOptions>,
pub compress: Option<CompressOptions>,
+61 -4
View File
@@ -1,6 +1,8 @@
use oxc_syntax::es_target::ESTarget;
#[derive(Debug, Clone, Copy)]
pub use oxc_ecmascript::side_effects::PropertyReadSideEffects;
#[derive(Debug, Clone)]
pub struct CompressOptions {
/// Set desired EcmaScript standard version for output.
///
@@ -12,9 +14,6 @@ pub struct CompressOptions {
/// Default `ESTarget::ESNext`
pub target: ESTarget,
/// Keep function / class names.
pub keep_names: CompressOptionsKeepNames,
/// Remove `debugger;` statements.
///
/// Default `true`
@@ -24,6 +23,13 @@ pub struct CompressOptions {
///
/// Default `false`
pub drop_console: bool,
/// Keep function / class names.
pub keep_names: CompressOptionsKeepNames,
/// Treeshake Options .
/// <https://rollupjs.org/configuration-options/#treeshake>
pub treeshake: TreeShakeOptions,
}
#[expect(clippy::derivable_impls)]
@@ -40,6 +46,7 @@ impl CompressOptions {
keep_names: CompressOptionsKeepNames::all_false(),
drop_debugger: true,
drop_console: true,
treeshake: TreeShakeOptions::default(),
}
}
@@ -49,6 +56,7 @@ impl CompressOptions {
keep_names: CompressOptionsKeepNames::all_true(),
drop_debugger: false,
drop_console: false,
treeshake: TreeShakeOptions::default(),
}
}
}
@@ -87,3 +95,52 @@ impl CompressOptionsKeepNames {
Self { function: false, class: true }
}
}
#[derive(Debug, Clone)]
pub struct TreeShakeOptions {
/// Whether to respect the pure annotations.
///
/// Pure annotations are the comments that marks that a expression is pure.
/// For example, `/* @__PURE__ */`, `/* #__NO_SIDE_EFFECTS__ */`.
///
/// <https://rollupjs.org/configuration-options/#treeshake-annotations>
///
/// Default `true`
pub annotations: bool,
/// Whether to treat this function call as pure.
///
/// This function is called for normal function calls, new calls, and
/// tagged template calls (`foo()`, `new Foo()`, ``foo`b` ``).
///
/// <https://rollupjs.org/configuration-options/#treeshake-manualpurefunctions>
pub manual_pure_functions: Vec<String>,
/// Whether property read accesses have side effects.
///
/// <https://rollupjs.org/configuration-options/#treeshake-propertyreadsideeffects>
///
/// Default [PropertyReadSideEffects::All]
pub property_read_side_effects: PropertyReadSideEffects,
/// Whether accessing a global variable has side effects.
///
/// Accessing a non-existing global variable will throw an error.
/// Global variable may be a getter that has side effects.
///
/// <https://rollupjs.org/configuration-options/#treeshake-unknownglobalsideeffects>
///
/// Default `true`
pub unknown_global_side_effects: bool,
}
impl Default for TreeShakeOptions {
fn default() -> Self {
Self {
annotations: true,
manual_pure_functions: vec![],
property_read_side_effects: PropertyReadSideEffects::default(),
unknown_global_side_effects: true,
}
}
}
+18 -19
View File
@@ -6,10 +6,7 @@ use oxc_span::GetSpan;
use oxc_syntax::scope::ScopeFlags;
use oxc_traverse::{Ancestor, ReusableTraverseCtx, Traverse, traverse_mut_with_ctx};
use crate::{
CompressOptions,
ctx::{Ctx, MinifierState, TraverseCtx},
};
use crate::ctx::{Ctx, MinifierState, TraverseCtx};
#[derive(Default)]
pub struct NormalizeOptions {
@@ -38,7 +35,6 @@ pub struct NormalizeOptions {
/// <https://github.com/google/closure-compiler/blob/v20240609/src/com/google/javascript/jscomp/Normalize.java>
pub struct Normalize {
options: NormalizeOptions,
compress_options: CompressOptions,
}
impl<'a> Normalize {
@@ -52,11 +48,11 @@ impl<'a> Normalize {
}
impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, _ctx: &mut TraverseCtx<'a>) {
fn exit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>, ctx: &mut TraverseCtx<'a>) {
stmts.retain(|stmt| {
!(matches!(stmt, Statement::EmptyStatement(_))
|| self.drop_debugger(stmt)
|| self.drop_console(stmt))
|| Self::drop_debugger(stmt, ctx)
|| Self::drop_console(stmt, ctx))
});
}
@@ -90,10 +86,10 @@ impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
None
}
Expression::ArrowFunctionExpression(e) => {
self.recover_arrow_expression_after_drop_console(e);
Self::recover_arrow_expression_after_drop_console(e, ctx);
None
}
Expression::CallExpression(_) if self.compress_options.drop_console => {
Expression::CallExpression(_) if ctx.state.options.drop_console => {
self.compress_console(expr, ctx)
}
Expression::StaticMemberExpression(e) => Self::fold_number_nan_to_nan(e, ctx),
@@ -113,15 +109,15 @@ impl<'a> Traverse<'a, MinifierState<'a>> for Normalize {
}
impl<'a> Normalize {
pub fn new(options: NormalizeOptions, compress_options: CompressOptions) -> Self {
Self { options, compress_options }
pub fn new(options: NormalizeOptions) -> Self {
Self { options }
}
/// Drop `drop_debugger` statement.
///
/// Enabled by `compress.drop_debugger`
fn drop_debugger(&self, stmt: &Statement<'a>) -> bool {
matches!(stmt, Statement::DebuggerStatement(_)) && self.compress_options.drop_debugger
fn drop_debugger(stmt: &Statement<'a>, ctx: &TraverseCtx<'a>) -> bool {
matches!(stmt, Statement::DebuggerStatement(_)) && ctx.state.options.drop_debugger
}
fn compress_console(
@@ -129,17 +125,20 @@ impl<'a> Normalize {
expr: &Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
debug_assert!(self.compress_options.drop_console);
debug_assert!(ctx.state.options.drop_console);
Self::is_console(expr).then(|| ctx.ast.void_0(expr.span()))
}
fn drop_console(&self, stmt: &Statement<'a>) -> bool {
self.compress_options.drop_console
fn drop_console(stmt: &Statement<'a>, ctx: &TraverseCtx<'a>) -> bool {
ctx.state.options.drop_console
&& matches!(stmt, Statement::ExpressionStatement(expr) if Self::is_console(&expr.expression))
}
fn recover_arrow_expression_after_drop_console(&self, expr: &mut ArrowFunctionExpression<'a>) {
if self.compress_options.drop_console && expr.expression && expr.body.is_empty() {
fn recover_arrow_expression_after_drop_console(
expr: &mut ArrowFunctionExpression<'a>,
ctx: &TraverseCtx<'a>,
) {
if ctx.state.options.drop_console && expr.expression && expr.body.is_empty() {
expr.expression = false;
}
}
@@ -31,11 +31,11 @@ impl IsGlobalReference for Ctx {
}
}
impl MayHaveSideEffectsContext for Ctx {
fn respect_annotations(&self) -> bool {
fn annotations(&self) -> bool {
self.annotation
}
fn is_pure_call(&self, callee: &Expression) -> bool {
fn manual_pure_functions(&self, callee: &Expression) -> bool {
if let Expression::Identifier(id) = callee {
self.pure_function_names.iter().any(|name| name == id.name.as_str())
} else {
+1 -1
View File
@@ -13,7 +13,7 @@ use oxc_span::SourceType;
#[track_caller]
pub(crate) fn test(source_text: &str, expected: &str, options: CompressOptions) {
let source_type = SourceType::default();
let first = run(source_text, source_type, Some(options));
let first = run(source_text, source_type, Some(options.clone()));
let expected = run(expected, source_type, None);
assert_eq!(first, expected, "\nfor source\n{source_text}\nexpect\n{expected}\ngot\n{first}");
+3 -1
View File
@@ -3,6 +3,7 @@ use std::str::FromStr;
use napi::Either;
use napi_derive::napi;
use oxc_minifier::TreeShakeOptions;
use oxc_sourcemap::napi::SourceMap;
use oxc_syntax::es_target::ESTarget;
@@ -54,9 +55,10 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions {
.map(|s| ESTarget::from_str(s))
.transpose()?
.unwrap_or(default.target),
keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
drop_console: o.drop_console.unwrap_or(default.drop_console),
drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger),
keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
treeshake: TreeShakeOptions::default(),
})
}
}