diff --git a/crates/oxc_mangler/src/lib.rs b/crates/oxc_mangler/src/lib.rs index d43bea1b8cbf6..f3eac0243ad3a 100644 --- a/crates/oxc_mangler/src/lib.rs +++ b/crates/oxc_mangler/src/lib.rs @@ -21,6 +21,9 @@ pub struct MangleOptions { /// Default: `false` pub top_level: bool, + /// Keep function / class names + pub keep_names: MangleOptionsKeepNames, + /// Use more readable mangled names /// (e.g. `slot_0`, `slot_1`, `slot_2`, ...) for debugging. /// @@ -28,6 +31,35 @@ pub struct MangleOptions { pub debug: bool, } +#[derive(Debug, Clone, Copy, Default)] +pub struct MangleOptionsKeepNames { + /// Keep function names so that `Function.prototype.name` is preserved. + /// + /// Default `false` + pub function: bool, + + /// Keep class names so that `Class.prototype.name` is preserved. + /// + /// Default `false` + pub class: bool, +} + +impl MangleOptionsKeepNames { + pub fn all_false() -> Self { + Self { function: false, class: false } + } + + pub fn all_true() -> Self { + Self { function: true, class: true } + } +} + +impl From for MangleOptionsKeepNames { + fn from(keep_names: bool) -> Self { + if keep_names { Self::all_true() } else { Self::all_false() } + } +} + type Slot = usize; /// # Name Mangler / Symbol Minification @@ -206,6 +238,8 @@ impl Mangler { } else { Default::default() }; + let (keep_name_names, keep_name_symbols) = + Mangler::collect_keep_name_symbols(self.options.keep_names, &scoping); let allocator = Allocator::default(); @@ -225,6 +259,16 @@ impl Mangler { continue; } + // Sort `bindings` in declaration order. + tmp_bindings.clear(); + tmp_bindings.extend( + bindings.values().copied().filter(|binding| !keep_name_symbols.contains(binding)), + ); + tmp_bindings.sort_unstable(); + if tmp_bindings.is_empty() { + continue; + } + let mut slot = slot_liveness.len(); reusable_slots.clear(); @@ -235,11 +279,11 @@ impl Mangler { .enumerate() .filter(|(_, slot_liveness)| !slot_liveness.contains(scope_id.index())) .map(|(slot, _)| slot) - .take(bindings.len()), + .take(tmp_bindings.len()), ); // The number of new slots that needs to be allocated. - let remaining_count = bindings.len() - reusable_slots.len(); + let remaining_count = tmp_bindings.len() - reusable_slots.len(); reusable_slots.extend(slot..slot + remaining_count); slot += remaining_count; @@ -248,10 +292,6 @@ impl Mangler { .resize_with(slot, || FixedBitSet::with_capacity(scoping.scopes_len())); } - // Sort `bindings` in declaration order. - tmp_bindings.clear(); - tmp_bindings.extend(bindings.values().copied()); - tmp_bindings.sort_unstable(); for (&symbol_id, assigned_slot) in tmp_bindings.iter().zip(reusable_slots.iter().copied()) { @@ -282,6 +322,7 @@ impl Mangler { let frequencies = self.tally_slot_frequencies( &scoping, &exported_symbols, + &keep_name_symbols, total_number_of_slots, &slots, &allocator, @@ -304,6 +345,8 @@ impl Mangler { && !root_unresolved_references.contains_key(n) && !(root_bindings.contains_key(n) && (!self.options.top_level || exported_names.contains(n))) + // TODO: only skip the names that are kept in the current scope + && !keep_name_names.contains(n) { break name; } @@ -368,6 +411,7 @@ impl Mangler { &'a self, scoping: &Scoping, exported_symbols: &FxHashSet, + keep_name_symbols: &FxHashSet, total_number_of_slots: usize, slots: &[Slot], allocator: &'a Allocator, @@ -388,6 +432,9 @@ impl Mangler { if is_special_name(scoping.symbol_name(symbol_id)) { continue; } + if keep_name_symbols.contains(&symbol_id) { + continue; + } let index = slot; frequencies[index].slot = slot; frequencies[index].frequency += scoping.get_resolved_reference_ids(symbol_id).len(); @@ -421,6 +468,21 @@ impl Mangler { .map(|id| (id.name, id.symbol_id())) .collect() } + + fn collect_keep_name_symbols( + keep_names: MangleOptionsKeepNames, + scoping: &Scoping, + ) -> (FxHashSet<&str>, FxHashSet) { + let ids: FxHashSet = keep_names + .function + .then(|| scoping.function_name_symbols()) + .into_iter() + .flatten() + .chain(keep_names.class.then(|| scoping.class_name_symbols()).into_iter().flatten()) + .copied() + .collect(); + (ids.iter().map(|id| scoping.symbol_name(*id)).collect(), ids) + } } fn is_special_name(name: &str) -> bool { diff --git a/crates/oxc_minifier/examples/mangler.rs b/crates/oxc_minifier/examples/mangler.rs index d8b6fd764490f..594e30e823e1c 100644 --- a/crates/oxc_minifier/examples/mangler.rs +++ b/crates/oxc_minifier/examples/mangler.rs @@ -15,6 +15,7 @@ use pico_args::Arguments; fn main() -> std::io::Result<()> { let mut args = Arguments::from_env(); + let keep_names = args.contains("--keep-names"); let debug = args.contains("--debug"); let twice = args.contains("--twice"); let name = args.free_from_str().unwrap_or_else(|_| "test.js".to_string()); @@ -23,11 +24,11 @@ fn main() -> std::io::Result<()> { let source_text = std::fs::read_to_string(path)?; let source_type = SourceType::from_path(path).unwrap(); - let printed = mangler(&source_text, source_type, debug); + let printed = mangler(&source_text, source_type, keep_names, debug); println!("{printed}"); if twice { - let printed2 = mangler(&printed, source_type, debug); + let printed2 = mangler(&printed, source_type, keep_names, debug); println!("{printed2}"); println!("same = {}", printed == printed2); } @@ -35,11 +36,15 @@ fn main() -> std::io::Result<()> { Ok(()) } -fn mangler(source_text: &str, source_type: SourceType, debug: bool) -> String { +fn mangler(source_text: &str, source_type: SourceType, keep_names: bool, debug: bool) -> String { let allocator = Allocator::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); let symbol_table = Mangler::new() - .with_options(MangleOptions { debug, top_level: source_type.is_module() }) + .with_options(MangleOptions { + keep_names: keep_names.into(), + debug, + top_level: source_type.is_module(), + }) .build(&ret.program); CodeGenerator::new().with_scoping(Some(symbol_table)).build(&ret.program).code } diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index 0837a7d908cdf..0451ba0ab49f7 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -16,7 +16,7 @@ use oxc_ast::ast::Program; use oxc_mangler::Mangler; use oxc_semantic::{Scoping, SemanticBuilder, Stats}; -pub use oxc_mangler::MangleOptions; +pub use oxc_mangler::{MangleOptions, MangleOptionsKeepNames}; pub use crate::{ compressor::Compressor, options::CompressOptions, options::CompressOptionsKeepNames, diff --git a/crates/oxc_minifier/tests/mangler/mod.rs b/crates/oxc_minifier/tests/mangler/mod.rs index be3f3846aaf65..252c04cb163fd 100644 --- a/crates/oxc_minifier/tests/mangler/mod.rs +++ b/crates/oxc_minifier/tests/mangler/mod.rs @@ -6,20 +6,21 @@ use oxc_mangler::{MangleOptions, Mangler}; use oxc_parser::Parser; use oxc_span::SourceType; -fn mangle(source_text: &str, top_level: bool) -> String { +fn mangle(source_text: &str, top_level: bool, keep_names: bool) -> String { let allocator = Allocator::default(); let source_type = SourceType::mjs(); let ret = Parser::new(&allocator, source_text, source_type).parse(); let program = ret.program; - let symbol_table = - Mangler::new().with_options(MangleOptions { debug: false, top_level }).build(&program); + let symbol_table = Mangler::new() + .with_options(MangleOptions { keep_names: keep_names.into(), debug: false, top_level }) + .build(&program); CodeGenerator::new().with_scoping(Some(symbol_table)).build(&program).code } #[test] fn direct_eval() { let source_text = "function foo() { let NO_MANGLE; eval('') }"; - let mangled = mangle(source_text, false); + let mangled = mangle(source_text, false, false); assert_eq!(mangled, "function foo() {\n\tlet NO_MANGLE;\n\teval(\"\");\n}\n"); } @@ -61,14 +62,25 @@ fn mangler() { "export const foo = 1; foo", "const foo = 1; foo; export { foo }", ]; + let keep_name_cases = [ + "function _() { function foo() { var x } }", + "function _() { var foo = function() { var x } }", + "function _() { var foo = () => { var x } }", + "function _() { class Foo { foo() { var x } } }", + "function _() { var Foo = class { foo() { var x } } }", + ]; let mut snapshot = String::new(); cases.into_iter().fold(&mut snapshot, |w, case| { - write!(w, "{case}\n{}\n", mangle(case, false)).unwrap(); + write!(w, "{case}\n{}\n", mangle(case, false, false)).unwrap(); w }); top_level_cases.into_iter().fold(&mut snapshot, |w, case| { - write!(w, "{case}\n{}\n", mangle(case, true)).unwrap(); + write!(w, "{case}\n{}\n", mangle(case, true, false)).unwrap(); + w + }); + keep_name_cases.into_iter().fold(&mut snapshot, |w, case| { + write!(w, "{case}\n{}\n", mangle(case, false, true)).unwrap(); w }); diff --git a/crates/oxc_minifier/tests/mangler/snapshots/mangler.snap b/crates/oxc_minifier/tests/mangler/snapshots/mangler.snap index 85ab853cc83aa..0aebcf67ff29d 100644 --- a/crates/oxc_minifier/tests/mangler/snapshots/mangler.snap +++ b/crates/oxc_minifier/tests/mangler/snapshots/mangler.snap @@ -235,3 +235,42 @@ const foo = 1; foo; export { foo } const e = 1; e; export { e as foo }; + +function _() { function foo() { var x } } +function _() { + function foo() { + var e; + } +} + +function _() { var foo = function() { var x } } +function _() { + var foo = function() { + var e; + }; +} + +function _() { var foo = () => { var x } } +function _() { + var foo = () => { + var e; + }; +} + +function _() { class Foo { foo() { var x } } } +function _() { + class Foo { + foo() { + var e; + } + } +} + +function _() { var Foo = class { foo() { var x } } } +function _() { + var Foo = class { + foo() { + var e; + } + }; +} diff --git a/napi/minify/index.d.ts b/napi/minify/index.d.ts index 8daf831a49737..b3764cd385d4f 100644 --- a/napi/minify/index.d.ts +++ b/napi/minify/index.d.ts @@ -65,10 +65,27 @@ export interface MangleOptions { * @default false */ toplevel?: boolean + /** Keep function / class names */ + keepNames?: MangleOptionsKeepNames /** Debug mangled names. */ debug?: boolean } +export interface MangleOptionsKeepNames { + /** + * Keep function names so that `Function.prototype.name` is preserved. + * + * @default false + */ + function: boolean + /** + * Keep class names so that `Class.prototype.name` is preserved. + * + * @default false + */ + class: boolean +} + /** Minify synchronously. */ export declare function minify(filename: string, sourceText: string, options?: MinifyOptions | undefined | null): MinifyResult diff --git a/napi/minify/src/options.rs b/napi/minify/src/options.rs index ff2ddb3fe753f..feff6996495fa 100644 --- a/napi/minify/src/options.rs +++ b/napi/minify/src/options.rs @@ -92,6 +92,9 @@ pub struct MangleOptions { /// @default false pub toplevel: Option, + /// Keep function / class names + pub keep_names: Option, + /// Debug mangled names. pub debug: Option, } @@ -101,11 +104,31 @@ impl From<&MangleOptions> for oxc_minifier::MangleOptions { let default = oxc_minifier::MangleOptions::default(); Self { top_level: o.toplevel.unwrap_or(default.top_level), + keep_names: o.keep_names.as_ref().map(Into::into).unwrap_or_default(), debug: o.debug.unwrap_or(default.debug), } } } +#[napi(object)] +pub struct MangleOptionsKeepNames { + /// Keep function names so that `Function.prototype.name` is preserved. + /// + /// @default false + pub function: bool, + + /// Keep class names so that `Class.prototype.name` is preserved. + /// + /// @default false + pub class: bool, +} + +impl From<&MangleOptionsKeepNames> for oxc_minifier::MangleOptionsKeepNames { + fn from(o: &MangleOptionsKeepNames) -> Self { + oxc_minifier::MangleOptionsKeepNames { function: o.function, class: o.class } + } +} + #[napi(object)] pub struct CodegenOptions { /// Remove whitespace.