mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 18:43:44 +00:00
fix(minifier): preserve var inside catch with same-named parameter (#21366)
## Summary - Block inlining of `var` declarators whose symbol has the `CatchVariable` flag, preventing loss of function-scoped hoisting - Check `symbol_redeclarations` before removing catch parameters, preventing semantic changes when the catch body contains a `var` redeclaration - Uses existing semantic data (`CatchVariable` flag and `symbol_redeclarations`) instead of manual AST walks Closes #17307 ## Test plan - [x] `cargo test -p oxc_minifier` — 476 tests pass - [x] Verified minified output matches original JS semantics via `node` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
1 parent
b5cb8d1fc4
commit
b0e8f13889
4 files changed
+51
-12
No files matched your search
@@ -1250,6 +1250,13 @@ impl<'a> PeepholeOptimizations {
|
||||
let BindingPattern::BindingIdentifier(prev_decl_id) = &prev_decl.id else {
|
||||
return true;
|
||||
};
|
||||
// Don't inline `var e` inside `catch (e) { ... }`. Removing the var declarator
|
||||
// would lose the function-scoped hoisting that `var` provides. The catch parameter
|
||||
// and the var share one symbol (with CatchVariable flag) due to the redeclaration
|
||||
// semantics in https://tc39.es/ecma262/#sec-variablestatements-in-catch-blocks
|
||||
if ctx.scoping().symbol_flags(prev_decl_id.symbol_id()).is_catch_variable() {
|
||||
return true;
|
||||
}
|
||||
if ctx.is_expression_whose_name_needs_to_be_kept(prev_decl_init) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1581,6 +1581,10 @@ impl<'a> PeepholeOptimizations {
|
||||
&& let Some(param) = &catch.param
|
||||
&& let BindingPattern::BindingIdentifier(ident) = ¶m.pattern
|
||||
&& (catch.body.body.is_empty() || ctx.scoping().symbol_is_unused(ident.symbol_id()))
|
||||
// Don't remove catch parameter when the body has a `var` with the same name.
|
||||
// In `catch (e) { var e = x }`, `var e` hoists to function scope but the assignment
|
||||
// targets the catch parameter. Removing the catch param changes semantics.
|
||||
&& ctx.scoping().symbol_redeclarations(ident.symbol_id()).is_empty()
|
||||
{
|
||||
catch.param = None;
|
||||
}
|
||||
|
||||
@@ -2343,7 +2343,11 @@ fn test_remove_dead_expr_other() {
|
||||
"try { throw 1 } catch (x) { y(x); var x = 2; y(x) }",
|
||||
"try { throw 1;} catch (x) { y(x); var x = 2; y(x);}",
|
||||
);
|
||||
test("try { throw 1 } catch (x) { var x = 2; y(x) }", "try { throw 1;} catch { y(2);}");
|
||||
// `var x` inside `catch (x)` must be kept, because removing it loses hoisting
|
||||
test(
|
||||
"try { throw 1 } catch (x) { var x = 2; y(x) }",
|
||||
"try { throw 1;} catch (x) { var x = 2; y(x);}",
|
||||
);
|
||||
test(
|
||||
"try { throw 1 } catch (x) { var x = 2; y(x) } console.log(x)",
|
||||
"try { throw 1;} catch (x) { var x = 2; y(x);} console.log(x)",
|
||||
|
||||
@@ -740,19 +740,43 @@ fn optional_catch_binding() {
|
||||
test_same("try { foo } catch([e]) {}");
|
||||
test_same("try { foo } catch({e}) {}");
|
||||
test_same("try { foo } catch(e) { var e = baz; bar(e) }");
|
||||
test("try { foo } catch(e) { var e = 2 }", "try { foo } catch { var e = 2 }");
|
||||
// catch param must be kept when body has a same-named var. Removing
|
||||
// the param would change which binding the assignment targets.
|
||||
test_same("try { foo } catch(e) { var e = 2 }");
|
||||
test_same("try { foo } catch(e) { var e = 2 } bar(e)");
|
||||
test_same("try { foo } catch(e) { var {e} = obj }");
|
||||
test_same("try { foo } catch(e) { var [e] = arr }");
|
||||
|
||||
// FIXME catch(a) has no references but it cannot be removed.
|
||||
// test_same(
|
||||
// r#"var a = "PASS";
|
||||
// try {
|
||||
// throw "FAIL1";
|
||||
// } catch (a) {
|
||||
// var a = "FAIL2";
|
||||
// }
|
||||
// console.log(a);"#,
|
||||
// );
|
||||
// var inside a function does NOT interact with the catch parameter;
|
||||
// var doesn't hoist out of functions, so the catch param can be removed.
|
||||
test(
|
||||
"try { foo } catch(e) { (function() { var e = 2 })() }",
|
||||
"try { foo } catch { (function() { var e = 2;})();}",
|
||||
);
|
||||
test(
|
||||
"try { foo } catch(e) { function f() { var e = 2 } }",
|
||||
"try { foo } catch { function f() { var e = 2 } }",
|
||||
);
|
||||
|
||||
test_same(
|
||||
r#"var a = "PASS";
|
||||
try {
|
||||
throw "FAIL1";
|
||||
} catch (a) {
|
||||
var a = "FAIL2";
|
||||
}
|
||||
console.log(a);"#,
|
||||
);
|
||||
|
||||
// Regression tests for https://github.com/oxc-project/oxc/issues/17307
|
||||
test(
|
||||
"try {} catch (e) { try {} catch (e) { var e = 'e'; console.log(e === 'e') } } console.log(e === undefined)",
|
||||
"try {} catch (e) { var e } console.log(e === void 0)",
|
||||
);
|
||||
test(
|
||||
"try { throw 1 } catch (e) { try { throw 2 } catch (e) { var e = 'e'; console.log(e === 'e') } } console.log(e === undefined)",
|
||||
"try { throw 1 } catch (e) { try { throw 2 } catch (e) { var e = 'e'; console.log(e === 'e') } } console.log(e === void 0)",
|
||||
);
|
||||
|
||||
test_target_same("try { foo } catch(e) {}", "chrome65");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user