Skip to content

Commit 421163f

Browse files
committed
feat(mangler): support keepNames option
1 parent af0a969 commit 421163f

File tree

7 files changed

+175
-17
lines changed

7 files changed

+175
-17
lines changed

crates/oxc_mangler/src/lib.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,45 @@ pub struct MangleOptions {
2121
/// Default: `false`
2222
pub top_level: bool,
2323

24+
/// Keep function / class names
25+
pub keep_names: MangleOptionsKeepNames,
26+
2427
/// Use more readable mangled names
2528
/// (e.g. `slot_0`, `slot_1`, `slot_2`, ...) for debugging.
2629
///
2730
/// Uses base54 if false.
2831
pub debug: bool,
2932
}
3033

34+
#[derive(Debug, Clone, Copy, Default)]
35+
pub struct MangleOptionsKeepNames {
36+
/// Keep function names so that `Function.prototype.name` is preserved.
37+
///
38+
/// Default `false`
39+
pub function: bool,
40+
41+
/// Keep class names so that `Class.prototype.name` is preserved.
42+
///
43+
/// Default `false`
44+
pub class: bool,
45+
}
46+
47+
impl MangleOptionsKeepNames {
48+
pub fn all_false() -> Self {
49+
Self { function: false, class: false }
50+
}
51+
52+
pub fn all_true() -> Self {
53+
Self { function: true, class: true }
54+
}
55+
}
56+
57+
impl From<bool> for MangleOptionsKeepNames {
58+
fn from(keep_names: bool) -> Self {
59+
if keep_names { Self::all_true() } else { Self::all_false() }
60+
}
61+
}
62+
3163
type Slot = usize;
3264

3365
/// # Name Mangler / Symbol Minification
@@ -206,6 +238,8 @@ impl Mangler {
206238
} else {
207239
Default::default()
208240
};
241+
let (keep_name_names, keep_name_symbols) =
242+
Mangler::collect_keep_name_symbols(self.options.keep_names, &scoping);
209243

210244
let allocator = Allocator::default();
211245

@@ -225,6 +259,16 @@ impl Mangler {
225259
continue;
226260
}
227261

262+
// Sort `bindings` in declaration order.
263+
tmp_bindings.clear();
264+
tmp_bindings.extend(
265+
bindings.values().copied().filter(|binding| !keep_name_symbols.contains(binding)),
266+
);
267+
tmp_bindings.sort_unstable();
268+
if tmp_bindings.is_empty() {
269+
continue;
270+
}
271+
228272
let mut slot = slot_liveness.len();
229273

230274
reusable_slots.clear();
@@ -235,11 +279,11 @@ impl Mangler {
235279
.enumerate()
236280
.filter(|(_, slot_liveness)| !slot_liveness.contains(scope_id.index()))
237281
.map(|(slot, _)| slot)
238-
.take(bindings.len()),
282+
.take(tmp_bindings.len()),
239283
);
240284

241285
// The number of new slots that needs to be allocated.
242-
let remaining_count = bindings.len() - reusable_slots.len();
286+
let remaining_count = tmp_bindings.len() - reusable_slots.len();
243287
reusable_slots.extend(slot..slot + remaining_count);
244288

245289
slot += remaining_count;
@@ -248,10 +292,6 @@ impl Mangler {
248292
.resize_with(slot, || FixedBitSet::with_capacity(scoping.scopes_len()));
249293
}
250294

251-
// Sort `bindings` in declaration order.
252-
tmp_bindings.clear();
253-
tmp_bindings.extend(bindings.values().copied());
254-
tmp_bindings.sort_unstable();
255295
for (&symbol_id, assigned_slot) in
256296
tmp_bindings.iter().zip(reusable_slots.iter().copied())
257297
{
@@ -282,6 +322,7 @@ impl Mangler {
282322
let frequencies = self.tally_slot_frequencies(
283323
&scoping,
284324
&exported_symbols,
325+
&keep_name_symbols,
285326
total_number_of_slots,
286327
&slots,
287328
&allocator,
@@ -304,6 +345,8 @@ impl Mangler {
304345
&& !root_unresolved_references.contains_key(n)
305346
&& !(root_bindings.contains_key(n)
306347
&& (!self.options.top_level || exported_names.contains(n)))
348+
// TODO: only skip the names that are kept in the current scope
349+
&& !keep_name_names.contains(n)
307350
{
308351
break name;
309352
}
@@ -368,6 +411,7 @@ impl Mangler {
368411
&'a self,
369412
scoping: &Scoping,
370413
exported_symbols: &FxHashSet<SymbolId>,
414+
keep_name_symbols: &FxHashSet<SymbolId>,
371415
total_number_of_slots: usize,
372416
slots: &[Slot],
373417
allocator: &'a Allocator,
@@ -388,6 +432,9 @@ impl Mangler {
388432
if is_special_name(scoping.symbol_name(symbol_id)) {
389433
continue;
390434
}
435+
if keep_name_symbols.contains(&symbol_id) {
436+
continue;
437+
}
391438
let index = slot;
392439
frequencies[index].slot = slot;
393440
frequencies[index].frequency += scoping.get_resolved_reference_ids(symbol_id).len();
@@ -421,6 +468,21 @@ impl Mangler {
421468
.map(|id| (id.name, id.symbol_id()))
422469
.collect()
423470
}
471+
472+
fn collect_keep_name_symbols(
473+
keep_names: MangleOptionsKeepNames,
474+
scoping: &Scoping,
475+
) -> (FxHashSet<&str>, FxHashSet<SymbolId>) {
476+
let ids: FxHashSet<SymbolId> = keep_names
477+
.function
478+
.then(|| scoping.function_name_symbols())
479+
.into_iter()
480+
.flatten()
481+
.chain(keep_names.class.then(|| scoping.class_name_symbols()).into_iter().flatten())
482+
.copied()
483+
.collect();
484+
(ids.iter().map(|id| scoping.symbol_name(*id)).collect(), ids)
485+
}
424486
}
425487

426488
fn is_special_name(name: &str) -> bool {

crates/oxc_minifier/examples/mangler.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use pico_args::Arguments;
1515
fn main() -> std::io::Result<()> {
1616
let mut args = Arguments::from_env();
1717

18+
let keep_names = args.contains("--keep-names");
1819
let debug = args.contains("--debug");
1920
let twice = args.contains("--twice");
2021
let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string());
@@ -23,23 +24,27 @@ fn main() -> std::io::Result<()> {
2324
let source_text = std::fs::read_to_string(path)?;
2425
let source_type = SourceType::from_path(path).unwrap();
2526

26-
let printed = mangler(&source_text, source_type, debug);
27+
let printed = mangler(&source_text, source_type, keep_names, debug);
2728
println!("{printed}");
2829

2930
if twice {
30-
let printed2 = mangler(&printed, source_type, debug);
31+
let printed2 = mangler(&printed, source_type, keep_names, debug);
3132
println!("{printed2}");
3233
println!("same = {}", printed == printed2);
3334
}
3435

3536
Ok(())
3637
}
3738

38-
fn mangler(source_text: &str, source_type: SourceType, debug: bool) -> String {
39+
fn mangler(source_text: &str, source_type: SourceType, keep_names: bool, debug: bool) -> String {
3940
let allocator = Allocator::default();
4041
let ret = Parser::new(&allocator, source_text, source_type).parse();
4142
let symbol_table = Mangler::new()
42-
.with_options(MangleOptions { debug, top_level: source_type.is_module() })
43+
.with_options(MangleOptions {
44+
keep_names: keep_names.into(),
45+
debug,
46+
top_level: source_type.is_module(),
47+
})
4348
.build(&ret.program);
4449
CodeGenerator::new().with_scoping(Some(symbol_table)).build(&ret.program).code
4550
}

crates/oxc_minifier/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use oxc_ast::ast::Program;
1616
use oxc_mangler::Mangler;
1717
use oxc_semantic::{Scoping, SemanticBuilder, Stats};
1818

19-
pub use oxc_mangler::MangleOptions;
19+
pub use oxc_mangler::{MangleOptions, MangleOptionsKeepNames};
2020

2121
pub use crate::{
2222
compressor::Compressor, options::CompressOptions, options::CompressOptionsKeepNames,

crates/oxc_minifier/tests/mangler/mod.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@ use oxc_mangler::{MangleOptions, Mangler};
66
use oxc_parser::Parser;
77
use oxc_span::SourceType;
88

9-
fn mangle(source_text: &str, top_level: bool) -> String {
9+
fn mangle(source_text: &str, top_level: bool, keep_names: bool) -> String {
1010
let allocator = Allocator::default();
1111
let source_type = SourceType::mjs();
1212
let ret = Parser::new(&allocator, source_text, source_type).parse();
1313
let program = ret.program;
14-
let symbol_table =
15-
Mangler::new().with_options(MangleOptions { debug: false, top_level }).build(&program);
14+
let symbol_table = Mangler::new()
15+
.with_options(MangleOptions { keep_names: keep_names.into(), debug: false, top_level })
16+
.build(&program);
1617
CodeGenerator::new().with_scoping(Some(symbol_table)).build(&program).code
1718
}
1819

1920
#[test]
2021
fn direct_eval() {
2122
let source_text = "function foo() { let NO_MANGLE; eval('') }";
22-
let mangled = mangle(source_text, false);
23+
let mangled = mangle(source_text, false, false);
2324
assert_eq!(mangled, "function foo() {\n\tlet NO_MANGLE;\n\teval(\"\");\n}\n");
2425
}
2526

@@ -61,14 +62,25 @@ fn mangler() {
6162
"export const foo = 1; foo",
6263
"const foo = 1; foo; export { foo }",
6364
];
65+
let keep_name_cases = [
66+
"function _() { function foo() { var x } }",
67+
"function _() { var foo = function() { var x } }",
68+
"function _() { var foo = () => { var x } }",
69+
"function _() { class Foo { foo() { var x } } }",
70+
"function _() { var Foo = class { foo() { var x } } }",
71+
];
6472

6573
let mut snapshot = String::new();
6674
cases.into_iter().fold(&mut snapshot, |w, case| {
67-
write!(w, "{case}\n{}\n", mangle(case, false)).unwrap();
75+
write!(w, "{case}\n{}\n", mangle(case, false, false)).unwrap();
6876
w
6977
});
7078
top_level_cases.into_iter().fold(&mut snapshot, |w, case| {
71-
write!(w, "{case}\n{}\n", mangle(case, true)).unwrap();
79+
write!(w, "{case}\n{}\n", mangle(case, true, false)).unwrap();
80+
w
81+
});
82+
keep_name_cases.into_iter().fold(&mut snapshot, |w, case| {
83+
write!(w, "{case}\n{}\n", mangle(case, false, true)).unwrap();
7284
w
7385
});
7486

crates/oxc_minifier/tests/mangler/snapshots/mangler.snap

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,42 @@ const foo = 1; foo; export { foo }
235235
const e = 1;
236236
e;
237237
export { e as foo };
238+
239+
function _() { function foo() { var x } }
240+
function _() {
241+
function foo() {
242+
var e;
243+
}
244+
}
245+
246+
function _() { var foo = function() { var x } }
247+
function _() {
248+
var foo = function() {
249+
var e;
250+
};
251+
}
252+
253+
function _() { var foo = () => { var x } }
254+
function _() {
255+
var foo = () => {
256+
var e;
257+
};
258+
}
259+
260+
function _() { class Foo { foo() { var x } } }
261+
function _() {
262+
class Foo {
263+
foo() {
264+
var e;
265+
}
266+
}
267+
}
268+
269+
function _() { var Foo = class { foo() { var x } } }
270+
function _() {
271+
var Foo = class {
272+
foo() {
273+
var e;
274+
}
275+
};
276+
}

napi/minify/index.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,27 @@ export interface MangleOptions {
6565
* @default false
6666
*/
6767
toplevel?: boolean
68+
/** Keep function / class names */
69+
keepNames?: MangleOptionsKeepNames
6870
/** Debug mangled names. */
6971
debug?: boolean
7072
}
7173

74+
export interface MangleOptionsKeepNames {
75+
/**
76+
* Keep function names so that `Function.prototype.name` is preserved.
77+
*
78+
* @default false
79+
*/
80+
function: boolean
81+
/**
82+
* Keep class names so that `Class.prototype.name` is preserved.
83+
*
84+
* @default false
85+
*/
86+
class: boolean
87+
}
88+
7289
/** Minify synchronously. */
7390
export declare function minify(filename: string, sourceText: string, options?: MinifyOptions | undefined | null): MinifyResult
7491

napi/minify/src/options.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ pub struct MangleOptions {
9292
/// @default false
9393
pub toplevel: Option<bool>,
9494

95+
/// Keep function / class names
96+
pub keep_names: Option<MangleOptionsKeepNames>,
97+
9598
/// Debug mangled names.
9699
pub debug: Option<bool>,
97100
}
@@ -101,11 +104,31 @@ impl From<&MangleOptions> for oxc_minifier::MangleOptions {
101104
let default = oxc_minifier::MangleOptions::default();
102105
Self {
103106
top_level: o.toplevel.unwrap_or(default.top_level),
107+
keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(),
104108
debug: o.debug.unwrap_or(default.debug),
105109
}
106110
}
107111
}
108112

113+
#[napi(object)]
114+
pub struct MangleOptionsKeepNames {
115+
/// Keep function names so that `Function.prototype.name` is preserved.
116+
///
117+
/// @default false
118+
pub function: bool,
119+
120+
/// Keep class names so that `Class.prototype.name` is preserved.
121+
///
122+
/// @default false
123+
pub class: bool,
124+
}
125+
126+
impl From<&MangleOptionsKeepNames> for oxc_minifier::MangleOptionsKeepNames {
127+
fn from(o: &MangleOptionsKeepNames) -> Self {
128+
oxc_minifier::MangleOptionsKeepNames { function: o.function, class: o.class }
129+
}
130+
}
131+
109132
#[napi(object)]
110133
pub struct CodegenOptions {
111134
/// Remove whitespace.

0 commit comments

Comments
 (0)