mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user