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
21 changes: 14 additions & 7 deletions crates/oxc_mangler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,6 @@ impl<'t> Mangler<'t> {

assert!(scoping.has_scope_child_ids(), "child_id needs to be generated");

// TODO: implement opt-out of direct-eval in a branch of scopes.
if scoping.root_scope_flags().contains_direct_eval() {
return;
}

let (exported_names, exported_symbols) = if self.options.top_level {
Mangler::collect_exported_symbols(program)
} else {
Expand Down Expand Up @@ -333,6 +328,9 @@ impl<'t> Mangler<'t> {
if bindings.is_empty() {
continue;
}
if scoping.scope_flags(scope_id).contains_direct_eval() {
continue;
}

// Sort `bindings` in declaration order.
tmp_bindings.clear();
Expand Down Expand Up @@ -455,7 +453,12 @@ impl<'t> Mangler<'t> {
let name = loop {
let name = generate_name(count);
count += 1;
// Do not mangle keywords and unresolved references
// Do not mangle keywords and unresolved references.
// Note: We don't need to exclude variable names from direct-eval-containing
// scopes because those scopes are skipped entirely during slot assignment
// (their variables keep original names). Mangled names only apply to scopes
// unaffected by eval, so there's no risk of shadowing variables that eval
// needs to access.
let n = name.as_str();
if !oxc_syntax::keyword::is_reserved_keyword(n)
&& !is_special_name(n)
Expand Down Expand Up @@ -539,11 +542,15 @@ impl<'t> Mangler<'t> {

for (symbol_id, slot) in slots.iter().copied().enumerate() {
let symbol_id = SymbolId::from_usize(symbol_id);
if scoping.symbol_scope_id(symbol_id) == root_scope_id
let symbol_scope_id = scoping.symbol_scope_id(symbol_id);
if symbol_scope_id == root_scope_id
&& (!self.options.top_level || exported_symbols.contains(&symbol_id))
{
continue;
}
if scoping.scope_flags(symbol_scope_id).contains_direct_eval() {
continue;
}
if is_special_name(scoping.symbol_name(symbol_id)) {
continue;
}
Expand Down
30 changes: 29 additions & 1 deletion crates/oxc_minifier/tests/mangler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,38 @@ fn mangle(source_text: &str, options: MangleOptions) -> String {

#[test]
fn direct_eval() {
let source_text = "function foo() { let NO_MANGLE; eval('') }";
let options = MangleOptions::default();

// Symbols in scopes with direct eval should NOT be mangled
let source_text = "function foo() { let NO_MANGLE; eval('') }";
let mangled = mangle(source_text, options);
assert_eq!(mangled, "function foo() {\n\tlet NO_MANGLE;\n\teval(\"\");\n}\n");

// Nested direct eval: parent scope also should not mangle
let source_text = "function foo() { let NO_MANGLE; function bar() { eval('') } }";
let mangled = mangle(source_text, options);
assert_eq!(
mangled,
"function foo() {\n\tlet NO_MANGLE;\n\tfunction bar() {\n\t\teval(\"\");\n\t}\n}\n"
);

// Sibling scope without direct eval should be mangled
let source_text =
"function foo() { let NO_MANGLE; eval('') } function bar() { let SHOULD_MANGLE; }";
let mangled = mangle(source_text, options);
// SHOULD_MANGLE gets mangled (to some short name), NO_MANGLE stays as is
assert!(mangled.contains("NO_MANGLE"));
assert!(!mangled.contains("SHOULD_MANGLE"));

// Child function scope without direct eval CAN be mangled (eval in parent cannot access child function locals)
let source_text = "function foo() { eval(''); function bar() { let CAN_MANGLE; } }";
let mangled = mangle(source_text, options);
assert!(!mangled.contains("CAN_MANGLE"));

// Indirect eval should still allow mangling
let source_text = "function foo() { let SHOULD_MANGLE; (0, eval)('') }";
let mangled = mangle(source_text, options);
assert!(!mangled.contains("SHOULD_MANGLE"));
}

#[test]
Expand Down
Loading