fix(minifier): avoid merging if to for in sloppy mode when containing function declaration (#25638)

```js
for (var i = 0; i++ < 1;) {
    if (x) break;
    else function f() {}
    f = 0;
}
console.log(typeof f, f);
```
was compressed to
```js
for (var i = 0; i++ < 1 && !x;) {
	function f() {}
	f = 0;
}
console.log(typeof f, f);
```
But this is wrong in sloppy mode. The output changes `number 0` / `undefined undefined` to `function f() {}`.

This PR fixes that.

I used AI to generate the code and then reviewed the code and added tests.
This commit is contained in:
sapphi-red committed 2026-08-23 02:07:08 +00:00
1 parent 30e612dd73
commit 6f0c7cf31b
2 files changed
+70 -1

No files matched your search

@@ -38,6 +38,7 @@ impl<'a> PeepholeOptimizations {
if body_has_function_declaration {
return;
}
let is_sloppy_mode = !ctx.current_scope_flags().is_strict_mode();
// "for (;;) if (x) break;" => "for (; !x;) ;"
// "for (; a;) if (x) break;" => "for (; a && !x;) ;"
@@ -47,6 +48,12 @@ impl<'a> PeepholeOptimizations {
if break_stmt.label.is_some() {
return;
}
// Annex B gives a direct function declaration in an `if` branch an implicit scope.
if is_sloppy_mode
&& if_stmt.alternate.as_ref().is_some_and(Self::statement_has_function_declaration)
{
return;
}
let span = for_stmt.body.span();
let (first, body) = match for_stmt.body.take_in(ctx) {
@@ -85,6 +92,10 @@ impl<'a> PeepholeOptimizations {
if break_stmt.label.is_some() {
return;
}
// Annex B gives a direct function declaration in an `if` branch an implicit scope.
if is_sloppy_mode && Self::statement_has_function_declaration(&if_stmt.consequent) {
return;
}
let span = for_stmt.body.span();
let (first, body) = match for_stmt.body.take_in(ctx) {
@@ -1,6 +1,8 @@
use std::fmt::Write;
use crate::{test, test_same};
use oxc_span::SourceType;
use crate::{default_options, test, test_options_source_type, test_same};
#[test]
fn test_for_variable_declaration() {
@@ -51,6 +53,62 @@ fn test_for_break_preserves_block_function_scope_in_module() {
);
}
#[test]
fn test_for_break_preserves_annex_b_function_scope() {
let options = default_options();
test_options_source_type(
"for (var i = 0; i++ < 1;) {
if (x) break;
else function f() {}
f = 0;
}
console.log(typeof f, f);",
"for (var i = 0; i++ < 1;) {
if (x) break;
else function f() {}
f = 0;
}
console.log(typeof f, f);",
SourceType::script(),
&options,
);
test_options_source_type(
"for (var i = 0; i++ < 1;) {
if (x) function f() {}
else break;
f = 0;
}
console.log(typeof f, f);",
"for (var i = 0; i++ < 1;) {
if (x) function f() {}
else break;
f = 0;
}
console.log(typeof f, f);",
SourceType::script(),
&options,
);
test_options_source_type(
"for (;;) {
if (x) break;
else { function f() {} }
}",
"for (; !x;) { function f() {} }",
SourceType::mjs(),
&options,
);
test_options_source_type(
"for (;;) {
if (x) { function f() {} }
else break;
}",
"for (; x;) { function f() {} }",
SourceType::mjs(),
&options,
);
}
#[test]
fn test_for_continue_in_for() {
test("for( a of b ){ if(c) { continue; } d() }", "for ( a of b ) c || d();");