Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/oxc_minifier/src/compressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ impl<'a> Compressor<'a> {
let normalize_options =
NormalizeOptions { convert_while_to_fors: true, convert_const_to_let: true };
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
PeepholeOptimizations::new(self.options.target).run_in_loop(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);
}

Expand Down
4 changes: 3 additions & 1 deletion crates/oxc_minifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use oxc_semantic::{Scoping, SemanticBuilder, Stats};

pub use oxc_mangler::MangleOptions;

pub use crate::{compressor::Compressor, options::CompressOptions};
pub use crate::{
compressor::Compressor, options::CompressOptions, options::CompressOptionsKeepNames,
};

#[derive(Debug, Clone, Copy)]
pub struct MinifierOptions {
Expand Down
52 changes: 50 additions & 2 deletions crates/oxc_minifier/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub struct CompressOptions {
/// Default `ESTarget::ESNext`
pub target: ESTarget,

/// Keep function / class names.
pub keep_names: CompressOptionsKeepNames,

/// Remove `debugger;` statements.
///
/// Default `true`
Expand All @@ -32,10 +35,55 @@ impl Default for CompressOptions {

impl CompressOptions {
pub fn smallest() -> Self {
Self { target: ESTarget::ESNext, drop_debugger: true, drop_console: true }
Self {
target: ESTarget::ESNext,
keep_names: CompressOptionsKeepNames::all_false(),
drop_debugger: true,
drop_console: true,
}
}

pub fn safest() -> Self {
Self { target: ESTarget::ESNext, drop_debugger: false, drop_console: false }
Self {
target: ESTarget::ESNext,
keep_names: CompressOptionsKeepNames::all_true(),
drop_debugger: false,
drop_console: false,
}
}
}

#[derive(Debug, Clone, Copy, Default)]
pub struct CompressOptionsKeepNames {
/// Keep function names so that `Function.prototype.name` is preserved.
///
/// This does not guarantee that the `undefined` name is preserved.
///
/// Default `false`
pub function: bool,

/// Keep class names so that `Class.prototype.name` is preserved.
///
/// This does not guarantee that the `undefined` name is preserved.
///
/// Default `false`
pub class: bool,
}

impl CompressOptionsKeepNames {
pub fn all_false() -> Self {
Self { function: false, class: false }
}

pub fn all_true() -> Self {
Self { function: true, class: true }
}

pub fn function_only() -> Self {
Self { function: true, class: false }
}

pub fn class_only() -> Self {
Self { function: false, class: true }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -423,10 +423,11 @@ impl<'a> PeepholeOptimizations {
if !matches!(consequent.left, AssignmentTarget::AssignmentTargetIdentifier(_)) {
return None;
}
// TODO: need this condition when `keep_fnames` is introduced
// if consequent.right.is_anonymous_function_definition() {
// return None;
// }
// NOTE: if the right hand side is an anonymous function, applying this compression will
// set the `name` property of that function.
// Since codes relying on the fact that function's name is undefined should be rare,
// we do this compression even if `keep_names` is enabled.

if consequent.operator != AssignmentOperator::Assign
|| consequent.operator != alternate.operator
|| consequent.left.content_ne(&alternate.left)
Expand Down
20 changes: 13 additions & 7 deletions crates/oxc_minifier/src/peephole/minimize_conditions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ impl<'a> PeepholeOptimizations {
}

let Expression::LogicalExpression(logical_expr) = &mut expr.right else { return false };
let new_op = logical_expr.operator.to_assignment_operator();
// NOTE: if the right hand side is an anonymous function, applying this compression will
// set the `name` property of that function.
// Since codes relying on the fact that function's name is undefined should be rare,
// we do this compression even if `keep_names` is enabled.

let (
AssignmentTarget::AssignmentTargetIdentifier(write_id_ref),
Expand All @@ -202,6 +205,7 @@ impl<'a> PeepholeOptimizations {
return false;
}

let new_op = logical_expr.operator.to_assignment_operator();
expr.operator = new_op;
expr.right = ctx.ast.move_expression(&mut logical_expr.right);
true
Expand Down Expand Up @@ -1317,11 +1321,8 @@ mod test {

// a.b might have a side effect
test_same("x ? a.b = 0 : a.b = 1");
// `a = x ? () => 'a' : () => 'b'` does not set the name property of the function
// TODO: need to pass these tests when `keep_fnames` are introduced
// test_same("x ? a = () => 'a' : a = () => 'b'");
// test_same("x ? a = function () { return 'a' } : a = function () { return 'b' }");
// test_same("x ? a = class { foo = 'a' } : a = class { foo = 'b' }");
// `a = x ? () => 'a' : () => 'b'` does not set the name property of the function, but we ignore that difference
test("x ? a = () => 'a' : a = () => 'b'", "a = x ? () => 'a' : () => 'b'");

// for non `=` operators, `GetValue(lref)` is called before `Evaluation of AssignmentExpression`
// so cannot be fold to `a += x ? 0 : 1`
Expand Down Expand Up @@ -1376,7 +1377,7 @@ mod test {
test("x && (x = g())", "x &&= g()");
test("x ?? (x = g())", "x ??= g()");

// `||=`, `&&=`, `??=` sets the name property of the function
// `||=`, `&&=`, `??=` sets the name property of the function, but we ignore that difference
// Example case: `let f = false; f || (f = () => {}); console.log(f.name)`
test("x || (x = () => 'a')", "x ||= () => 'a'");

Expand Down Expand Up @@ -1424,6 +1425,11 @@ mod test {
// This case is not supported, since the minifier does not support with statements
// test_same("var x; with (z) { x = x || 1 }");

// `||=`, `&&=`, `??=` sets the name property of the function, while `= x || y` does not
// but we ignore that difference
// Example case: `let f = false; f = f || (() => {}); console.log(f.name)`
test("var x; x = x || (() => 'a')", "var x; x ||= (() => 'a')");

let target = ESTarget::ES2019;
let code = "var x; x = x || 1";
assert_eq!(
Expand Down
13 changes: 10 additions & 3 deletions crates/oxc_minifier/src/peephole/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,13 @@ use oxc_data_structures::stack::NonEmptyStack;
use oxc_syntax::{es_target::ESTarget, scope::ScopeId};
use oxc_traverse::{ReusableTraverseCtx, Traverse, TraverseCtx, traverse_mut_with_ctx};

use crate::ctx::Ctx;
use crate::{ctx::Ctx, options::CompressOptionsKeepNames};

pub use self::normalize::{Normalize, NormalizeOptions};

pub struct PeepholeOptimizations {
target: ESTarget,
keep_names: CompressOptionsKeepNames,

/// Walk the ast in a fixed point loop until no changes are made.
/// `prev_function_changed`, `functions_changed` and `current_function` track changes
Expand All @@ -45,9 +46,10 @@ pub struct PeepholeOptimizations {
}

impl<'a> PeepholeOptimizations {
pub fn new(target: ESTarget) -> Self {
pub fn new(target: ESTarget, keep_names: CompressOptionsKeepNames) -> Self {
Self {
target,
keep_names,
iteration: 0,
prev_functions_changed: FxHashSet::default(),
functions_changed: FxHashSet::default(),
Expand Down Expand Up @@ -363,7 +365,12 @@ pub struct DeadCodeElimination {

impl<'a> DeadCodeElimination {
pub fn new() -> Self {
Self { inner: PeepholeOptimizations::new(ESTarget::ESNext) }
Self {
inner: PeepholeOptimizations::new(
ESTarget::ESNext,
CompressOptionsKeepNames::all_true(),
),
}
}

pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {
Expand Down
19 changes: 18 additions & 1 deletion crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,10 @@ impl<'a> PeepholeOptimizations {
///
/// This compression is not safe if the code relies on `Function::name`.
fn try_remove_name_from_functions(&mut self, func: &mut Function<'a>, ctx: Ctx<'a, '_>) {
if self.keep_names.function {
return;
}

if func.id.as_ref().is_some_and(|id| !ctx.scoping().symbol_is_used(id.symbol_id())) {
func.id = None;
self.mark_current_function_as_changed();
Expand All @@ -981,8 +985,12 @@ impl<'a> PeepholeOptimizations {
///
/// e.g. `var a = class C {}` -> `var a = class {}`
///
/// This compression is not safe if the code relies on `Function::name`.
/// This compression is not safe if the code relies on `Class::name`.
fn try_remove_name_from_classes(&mut self, class: &mut Class<'a>, ctx: Ctx<'a, '_>) {
if self.keep_names.class {
return;
}

if class.id.as_ref().is_some_and(|id| !ctx.scoping().symbol_is_used(id.symbol_id())) {
class.id = None;
self.mark_current_function_as_changed();
Expand Down Expand Up @@ -1206,9 +1214,16 @@ mod test {

use crate::{
CompressOptions,
options::CompressOptionsKeepNames,
tester::{run, test, test_same},
};

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

#[test]
fn test_fold_return_result() {
test("function f(){return !1;}", "function f(){return !1}");
Expand Down Expand Up @@ -1858,8 +1873,10 @@ mod test {
fn test_remove_name_from_expressions() {
test("var a = function f() {}", "var a = function () {}");
test_same("var a = function f() { return f; }");
test_same_keep_names(CompressOptionsKeepNames::function_only(), "var a = function f() {}");
test("var a = class C {}", "var a = class {}");
test_same("var a = class C { foo() { return C } }");
test_same_keep_names(CompressOptionsKeepNames::class_only(), "var a = class C {}");
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion crates/oxc_minifier/tests/peephole/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod esbuild;
use oxc_minifier::CompressOptions;

fn test(source_text: &str, expected: &str) {
let options = CompressOptions::safest();
let options = CompressOptions { drop_debugger: false, ..CompressOptions::default() };
crate::test(source_text, expected, options);
}

Expand Down
21 changes: 21 additions & 0 deletions napi/minify/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface CompressOptions {
* @default 'esnext'
*/
target?: 'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024'
/** Keep function / class names. */
keepNames?: CompressOptionsKeepNames
/**
* Pass true to discard calls to `console.*`.
*
Expand All @@ -37,6 +39,25 @@ export interface CompressOptions {
dropDebugger?: boolean
}

export interface CompressOptionsKeepNames {
/**
* Keep function names so that `Function.prototype.name` is preserved.
*
* This does not guarantee that the `undefined` name is preserved.
*
* @default false
*/
function: boolean
/**
* Keep class names so that `Class.prototype.name` is preserved.
*
* This does not guarantee that the `undefined` name is preserved.
*
* @default false
*/
class: boolean
}

export interface MangleOptions {
/**
* Pass `true` to mangle names declared in the top level scope.
Expand Down
29 changes: 28 additions & 1 deletion napi/minify/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub struct CompressOptions {
)]
pub target: Option<String>,

/// Keep function / class names.
pub keep_names: Option<CompressOptionsKeepNames>,

/// Pass true to discard calls to `console.*`.
///
/// @default false
Expand All @@ -36,7 +39,7 @@ pub struct CompressOptions {

impl Default for CompressOptions {
fn default() -> Self {
Self { target: None, drop_console: None, drop_debugger: Some(true) }
Self { target: None, keep_names: None, drop_console: None, drop_debugger: Some(true) }
}
}

Expand All @@ -51,12 +54,36 @@ 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),
})
}
}

#[napi(object)]
pub struct CompressOptionsKeepNames {
/// Keep function names so that `Function.prototype.name` is preserved.
///
/// This does not guarantee that the `undefined` name is preserved.
///
/// @default false
pub function: bool,

/// Keep class names so that `Class.prototype.name` is preserved.
///
/// This does not guarantee that the `undefined` name is preserved.
///
/// @default false
pub class: bool,
}

impl From<&CompressOptionsKeepNames> for oxc_minifier::CompressOptionsKeepNames {
fn from(o: &CompressOptionsKeepNames) -> Self {
oxc_minifier::CompressOptionsKeepNames { function: o.function, class: o.class }
}
}

#[napi(object)]
#[derive(Default)]
pub struct MangleOptions {
Expand Down
4 changes: 2 additions & 2 deletions napi/playground/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,10 +266,10 @@ impl Oxc {
CompressOptions {
drop_console: compress_options.drop_console,
drop_debugger: compress_options.drop_debugger,
..CompressOptions::safest()
..CompressOptions::default()
}
} else {
CompressOptions::safest()
CompressOptions::default()
}),
};
Minifier::new(options).build(&allocator, &mut program).scoping
Expand Down
Loading