diff --git a/crates/oxc_mangler/src/lib.rs b/crates/oxc_mangler/src/lib.rs index e32f345e40e79..90e9a0ecd4924 100644 --- a/crates/oxc_mangler/src/lib.rs +++ b/crates/oxc_mangler/src/lib.rs @@ -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 { @@ -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(); @@ -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) @@ -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; } diff --git a/crates/oxc_minifier/tests/mangler/mod.rs b/crates/oxc_minifier/tests/mangler/mod.rs index 6378ffb392cd3..8ca260812c90d 100644 --- a/crates/oxc_minifier/tests/mangler/mod.rs +++ b/crates/oxc_minifier/tests/mangler/mod.rs @@ -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]