Skip to content
Closed
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
74 changes: 68 additions & 6 deletions crates/oxc_mangler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,45 @@ 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.
///
/// Uses base54 if false.
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<bool> 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
Expand Down Expand Up @@ -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();

Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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())
{
Expand Down Expand Up @@ -282,6 +322,7 @@ impl Mangler {
let frequencies = self.tally_slot_frequencies(
&scoping,
&exported_symbols,
&keep_name_symbols,
total_number_of_slots,
&slots,
&allocator,
Expand All @@ -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;
}
Expand Down Expand Up @@ -368,6 +411,7 @@ impl Mangler {
&'a self,
scoping: &Scoping,
exported_symbols: &FxHashSet<SymbolId>,
keep_name_symbols: &FxHashSet<SymbolId>,
total_number_of_slots: usize,
slots: &[Slot],
allocator: &'a Allocator,
Expand All @@ -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();
Expand Down Expand Up @@ -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<SymbolId>) {
let ids: FxHashSet<SymbolId> = 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 {
Expand Down
13 changes: 9 additions & 4 deletions crates/oxc_minifier/examples/mangler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -23,23 +24,27 @@ 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);
}

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
}
2 changes: 1 addition & 1 deletion crates/oxc_minifier/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 18 additions & 6 deletions crates/oxc_minifier/tests/mangler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

Expand Down Expand Up @@ -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
});

Expand Down
39 changes: 39 additions & 0 deletions crates/oxc_minifier/tests/mangler/snapshots/mangler.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
};
}
17 changes: 17 additions & 0 deletions napi/minify/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions napi/minify/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ pub struct MangleOptions {
/// @default false
pub toplevel: Option<bool>,

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

/// Debug mangled names.
pub debug: Option<bool>,
}
Expand All @@ -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.
Expand Down
Loading