fix(minifier): drop debugger statements in unbraced bodies (#27867)

Replace debugger statements with empty statements during normalization so `dropDebugger` also removes them from unbraced `if`/`else`, loop, and labeled bodies. Add regressions covering these positions, preserved side effects, and `dropDebugger: false`.

Fixes #27866
This commit is contained in:
camc314 committed 2026-10-17 17:39:10 +00:00
1 parent 4661bf0230
commit d92789d0c7
3 files changed
+34 -7

No files matched your search

@@ -101,15 +101,11 @@ impl<'a> Traverse<'a> for Normalize {
fn exit_statements(
&mut self,
stmts: &mut ArenaVec<'a, Statement<'a>>,
ctx: &mut TraverseCtx<'a>,
_ctx: &mut TraverseCtx<'a>,
) {
// No console handling here: `exit_expression` has already rewritten
// every console call (statement position included) to `void 0`.
stmts.retain(|stmt| match stmt {
Statement::EmptyStatement(_) => false,
Statement::DebuggerStatement(_) if ctx.options().drop_debugger => false,
_ => true,
});
stmts.retain(|stmt| !matches!(stmt, Statement::EmptyStatement(_)));
}
fn exit_variable_declaration(
@@ -124,6 +120,10 @@ impl<'a> Traverse<'a> for Normalize {
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
match stmt {
Statement::DebuggerStatement(debugger) if ctx.options().drop_debugger => {
let new_stmt = Statement::new_empty_statement(debugger.span, ctx);
ctx.replace_statement(stmt, new_stmt);
}
Statement::WhileStatement(_) if self.options.convert_while_to_fors => {
Self::convert_while_to_for(stmt, ctx);
}
@@ -78,7 +78,34 @@ fn drop_console_does_not_leak_references() {
#[test]
fn drop_debugger() {
let options = CompressOptions { drop_debugger: true, ..default_options() };
test_options("debugger", "", &options);
test_options(
"export function f(x) { debugger; if (x) debugger; return x }",
"export function f(x) { return x }",
&options,
);
test_options(
"export function f(x) { debugger; if (x) { debugger; } return x }",
"export function f(x) { return x }",
&options,
);
test_options("if (foo()) debugger", "foo()", &options);
test_options("if (x) debugger; else foo()", "x || foo()", &options);
test_options("if (x) foo(); else debugger", "x && foo()", &options);
test_options("while (foo()) debugger", "for (; foo(););", &options);
test_options("do debugger; while (foo())", "do; while (foo());", &options);
test_options("for (foo(); bar(); baz()) debugger", "for (foo(); bar(); baz());", &options);
test_options("for (var x in foo()) debugger", "for (var x in foo());", &options);
test_options("for (var x of foo()) debugger", "for (var x of foo());", &options);
test_options("label: debugger", "", &options);
}
#[test]
fn keep_debugger() {
test_same("export function f(x) { debugger; if (x) debugger; return x }");
test_same("if (x) debugger; else debugger;");
test_same("for (; foo();) debugger;");
}
#[test]
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
sys deallocs: 1854
sys alloc bytes: 5304252 # 5.30 MB
sys peak growth: 3707425 # 3.71 MB
arena allocs: 31111
arena allocs: 31112
arena reallocs: 3012
arena size: 8461040 # 8.46 MB