Skip to content

Commit dcd356e

Browse files
committedMar 18, 2025··
feat(minifier): support keep_names option (#9867)
Added support for `keep_names: { function: true, class: true }`. Setting `.name` is makes it difficult to treeshake later on (evanw/esbuild#3965), so it is better to avoid if possible. This PR only adds support for the compressor. I'll add support for the mangler later on (probably first add required information to the semantics). refs #9711
1 parent b9cf61e commit dcd356e

File tree

11 files changed

+153
-23
lines changed

11 files changed

+153
-23
lines changed
 

‎crates/oxc_minifier/src/compressor.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ impl<'a> Compressor<'a> {
3131
let normalize_options =
3232
NormalizeOptions { convert_while_to_fors: true, convert_const_to_let: true };
3333
Normalize::new(normalize_options, self.options).build(program, &mut ctx);
34-
PeepholeOptimizations::new(self.options.target).run_in_loop(program, &mut ctx);
34+
PeepholeOptimizations::new(self.options.target, self.options.keep_names)
35+
.run_in_loop(program, &mut ctx);
3536
LatePeepholeOptimizations::new(self.options.target).build(program, &mut ctx);
3637
}
3738

‎crates/oxc_minifier/src/lib.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ use oxc_semantic::{Scoping, SemanticBuilder, Stats};
1818

1919
pub use oxc_mangler::MangleOptions;
2020

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

2325
#[derive(Debug, Clone, Copy)]
2426
pub struct MinifierOptions {

‎crates/oxc_minifier/src/options.rs

+50-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ pub struct CompressOptions {
1212
/// Default `ESTarget::ESNext`
1313
pub target: ESTarget,
1414

15+
/// Keep function / class names.
16+
pub keep_names: CompressOptionsKeepNames,
17+
1518
/// Remove `debugger;` statements.
1619
///
1720
/// Default `true`
@@ -32,10 +35,55 @@ impl Default for CompressOptions {
3235

3336
impl CompressOptions {
3437
pub fn smallest() -> Self {
35-
Self { target: ESTarget::ESNext, drop_debugger: true, drop_console: true }
38+
Self {
39+
target: ESTarget::ESNext,
40+
keep_names: CompressOptionsKeepNames::all_false(),
41+
drop_debugger: true,
42+
drop_console: true,
43+
}
3644
}
3745

3846
pub fn safest() -> Self {
39-
Self { target: ESTarget::ESNext, drop_debugger: false, drop_console: false }
47+
Self {
48+
target: ESTarget::ESNext,
49+
keep_names: CompressOptionsKeepNames::all_true(),
50+
drop_debugger: false,
51+
drop_console: false,
52+
}
53+
}
54+
}
55+
56+
#[derive(Debug, Clone, Copy, Default)]
57+
pub struct CompressOptionsKeepNames {
58+
/// Keep function names so that `Function.prototype.name` is preserved.
59+
///
60+
/// This does not guarantee that the `undefined` name is preserved.
61+
///
62+
/// Default `false`
63+
pub function: bool,
64+
65+
/// Keep class names so that `Class.prototype.name` is preserved.
66+
///
67+
/// This does not guarantee that the `undefined` name is preserved.
68+
///
69+
/// Default `false`
70+
pub class: bool,
71+
}
72+
73+
impl CompressOptionsKeepNames {
74+
pub fn all_false() -> Self {
75+
Self { function: false, class: false }
76+
}
77+
78+
pub fn all_true() -> Self {
79+
Self { function: true, class: true }
80+
}
81+
82+
pub fn function_only() -> Self {
83+
Self { function: true, class: false }
84+
}
85+
86+
pub fn class_only() -> Self {
87+
Self { function: false, class: true }
4088
}
4189
}

‎crates/oxc_minifier/src/peephole/minimize_conditional_expression.rs

+5-4
Original file line numberDiff line numberDiff line change
@@ -423,10 +423,11 @@ impl<'a> PeepholeOptimizations {
423423
if !matches!(consequent.left, AssignmentTarget::AssignmentTargetIdentifier(_)) {
424424
return None;
425425
}
426-
// TODO: need this condition when `keep_fnames` is introduced
427-
// if consequent.right.is_anonymous_function_definition() {
428-
// return None;
429-
// }
426+
// NOTE: if the right hand side is an anonymous function, applying this compression will
427+
// set the `name` property of that function.
428+
// Since codes relying on the fact that function's name is undefined should be rare,
429+
// we do this compression even if `keep_names` is enabled.
430+
430431
if consequent.operator != AssignmentOperator::Assign
431432
|| consequent.operator != alternate.operator
432433
|| consequent.left.content_ne(&alternate.left)

‎crates/oxc_minifier/src/peephole/minimize_conditions.rs

+13-7
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,10 @@ impl<'a> PeepholeOptimizations {
187187
}
188188

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

192195
let (
193196
AssignmentTarget::AssignmentTargetIdentifier(write_id_ref),
@@ -202,6 +205,7 @@ impl<'a> PeepholeOptimizations {
202205
return false;
203206
}
204207

208+
let new_op = logical_expr.operator.to_assignment_operator();
205209
expr.operator = new_op;
206210
expr.right = ctx.ast.move_expression(&mut logical_expr.right);
207211
true
@@ -1317,11 +1321,8 @@ mod test {
13171321

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

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

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

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

1428+
// `||=`, `&&=`, `??=` sets the name property of the function, while `= x || y` does not
1429+
// but we ignore that difference
1430+
// Example case: `let f = false; f = f || (() => {}); console.log(f.name)`
1431+
test("var x; x = x || (() => 'a')", "var x; x ||= (() => 'a')");
1432+
14271433
let target = ESTarget::ES2019;
14281434
let code = "var x; x = x || 1";
14291435
assert_eq!(

‎crates/oxc_minifier/src/peephole/mod.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ use oxc_data_structures::stack::NonEmptyStack;
2525
use oxc_syntax::{es_target::ESTarget, scope::ScopeId};
2626
use oxc_traverse::{ReusableTraverseCtx, Traverse, TraverseCtx, traverse_mut_with_ctx};
2727

28-
use crate::ctx::Ctx;
28+
use crate::{ctx::Ctx, options::CompressOptionsKeepNames};
2929

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

3232
pub struct PeepholeOptimizations {
3333
target: ESTarget,
34+
keep_names: CompressOptionsKeepNames,
3435

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

4748
impl<'a> PeepholeOptimizations {
48-
pub fn new(target: ESTarget) -> Self {
49+
pub fn new(target: ESTarget, keep_names: CompressOptionsKeepNames) -> Self {
4950
Self {
5051
target,
52+
keep_names,
5153
iteration: 0,
5254
prev_functions_changed: FxHashSet::default(),
5355
functions_changed: FxHashSet::default(),
@@ -363,7 +365,12 @@ pub struct DeadCodeElimination {
363365

364366
impl<'a> DeadCodeElimination {
365367
pub fn new() -> Self {
366-
Self { inner: PeepholeOptimizations::new(ESTarget::ESNext) }
368+
Self {
369+
inner: PeepholeOptimizations::new(
370+
ESTarget::ESNext,
371+
CompressOptionsKeepNames::all_true(),
372+
),
373+
}
367374
}
368375

369376
pub fn build(&mut self, program: &mut Program<'a>, ctx: &mut ReusableTraverseCtx<'a>) {

‎crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -971,6 +971,10 @@ impl<'a> PeepholeOptimizations {
971971
///
972972
/// This compression is not safe if the code relies on `Function::name`.
973973
fn try_remove_name_from_functions(&mut self, func: &mut Function<'a>, ctx: Ctx<'a, '_>) {
974+
if self.keep_names.function {
975+
return;
976+
}
977+
974978
if func.id.as_ref().is_some_and(|id| !ctx.scoping().symbol_is_used(id.symbol_id())) {
975979
func.id = None;
976980
self.mark_current_function_as_changed();
@@ -981,8 +985,12 @@ impl<'a> PeepholeOptimizations {
981985
///
982986
/// e.g. `var a = class C {}` -> `var a = class {}`
983987
///
984-
/// This compression is not safe if the code relies on `Function::name`.
988+
/// This compression is not safe if the code relies on `Class::name`.
985989
fn try_remove_name_from_classes(&mut self, class: &mut Class<'a>, ctx: Ctx<'a, '_>) {
990+
if self.keep_names.class {
991+
return;
992+
}
993+
986994
if class.id.as_ref().is_some_and(|id| !ctx.scoping().symbol_is_used(id.symbol_id())) {
987995
class.id = None;
988996
self.mark_current_function_as_changed();
@@ -1206,9 +1214,16 @@ mod test {
12061214

12071215
use crate::{
12081216
CompressOptions,
1217+
options::CompressOptionsKeepNames,
12091218
tester::{run, test, test_same},
12101219
};
12111220

1221+
fn test_same_keep_names(keep_names: CompressOptionsKeepNames, code: &str) {
1222+
let result = run(code, Some(CompressOptions { keep_names, ..CompressOptions::smallest() }));
1223+
let expected = run(code, None);
1224+
assert_eq!(result, expected, "\nfor source\n{code}\ngot\n{result}");
1225+
}
1226+
12121227
#[test]
12131228
fn test_fold_return_result() {
12141229
test("function f(){return !1;}", "function f(){return !1}");
@@ -1858,8 +1873,10 @@ mod test {
18581873
fn test_remove_name_from_expressions() {
18591874
test("var a = function f() {}", "var a = function () {}");
18601875
test_same("var a = function f() { return f; }");
1876+
test_same_keep_names(CompressOptionsKeepNames::function_only(), "var a = function f() {}");
18611877
test("var a = class C {}", "var a = class {}");
18621878
test_same("var a = class C { foo() { return C } }");
1879+
test_same_keep_names(CompressOptionsKeepNames::class_only(), "var a = class C {}");
18631880
}
18641881

18651882
#[test]

‎crates/oxc_minifier/tests/peephole/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ mod esbuild;
44
use oxc_minifier::CompressOptions;
55

66
fn test(source_text: &str, expected: &str) {
7-
let options = CompressOptions::safest();
7+
let options = CompressOptions { drop_debugger: false, ..CompressOptions::default() };
88
crate::test(source_text, expected, options);
99
}
1010

‎napi/minify/index.d.ts

+21
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface CompressOptions {
2323
* @default 'esnext'
2424
*/
2525
target?: 'esnext' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022' | 'es2023' | 'es2024'
26+
/** Keep function / class names. */
27+
keepNames?: CompressOptionsKeepNames
2628
/**
2729
* Pass true to discard calls to `console.*`.
2830
*
@@ -37,6 +39,25 @@ export interface CompressOptions {
3739
dropDebugger?: boolean
3840
}
3941

42+
export interface CompressOptionsKeepNames {
43+
/**
44+
* Keep function names so that `Function.prototype.name` is preserved.
45+
*
46+
* This does not guarantee that the `undefined` name is preserved.
47+
*
48+
* @default false
49+
*/
50+
function: boolean
51+
/**
52+
* Keep class names so that `Class.prototype.name` is preserved.
53+
*
54+
* This does not guarantee that the `undefined` name is preserved.
55+
*
56+
* @default false
57+
*/
58+
class: boolean
59+
}
60+
4061
export interface MangleOptions {
4162
/**
4263
* Pass `true` to mangle names declared in the top level scope.

‎napi/minify/src/options.rs

+28-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pub struct CompressOptions {
2323
)]
2424
pub target: Option<String>,
2525

26+
/// Keep function / class names.
27+
pub keep_names: Option<CompressOptionsKeepNames>,
28+
2629
/// Pass true to discard calls to `console.*`.
2730
///
2831
/// @default false
@@ -36,7 +39,7 @@ pub struct CompressOptions {
3639

3740
impl Default for CompressOptions {
3841
fn default() -> Self {
39-
Self { target: None, drop_console: None, drop_debugger: Some(true) }
42+
Self { target: None, keep_names: None, drop_console: None, drop_debugger: Some(true) }
4043
}
4144
}
4245

@@ -51,12 +54,36 @@ impl TryFrom<&CompressOptions> for oxc_minifier::CompressOptions {
5154
.map(|s| ESTarget::from_str(s))
5255
.transpose()?
5356
.unwrap_or(default.target),
57+
keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
5458
drop_console: o.drop_console.unwrap_or(default.drop_console),
5559
drop_debugger: o.drop_debugger.unwrap_or(default.drop_debugger),
5660
})
5761
}
5862
}
5963

64+
#[napi(object)]
65+
pub struct CompressOptionsKeepNames {
66+
/// Keep function names so that `Function.prototype.name` is preserved.
67+
///
68+
/// This does not guarantee that the `undefined` name is preserved.
69+
///
70+
/// @default false
71+
pub function: bool,
72+
73+
/// Keep class names so that `Class.prototype.name` is preserved.
74+
///
75+
/// This does not guarantee that the `undefined` name is preserved.
76+
///
77+
/// @default false
78+
pub class: bool,
79+
}
80+
81+
impl From<&CompressOptionsKeepNames> for oxc_minifier::CompressOptionsKeepNames {
82+
fn from(o: &CompressOptionsKeepNames) -> Self {
83+
oxc_minifier::CompressOptionsKeepNames { function: o.function, class: o.class }
84+
}
85+
}
86+
6087
#[napi(object)]
6188
#[derive(Default)]
6289
pub struct MangleOptions {

‎napi/playground/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -266,10 +266,10 @@ impl Oxc {
266266
CompressOptions {
267267
drop_console: compress_options.drop_console,
268268
drop_debugger: compress_options.drop_debugger,
269-
..CompressOptions::safest()
269+
..CompressOptions::default()
270270
}
271271
} else {
272-
CompressOptions::safest()
272+
CompressOptions::default()
273273
}),
274274
};
275275
Minifier::new(options).build(&allocator, &mut program).scoping

0 commit comments

Comments
 (0)
Please sign in to comment.