biome style/useCollapsedIf
labelled safe
Analysis Nemotron 3 Super
Flags nested if statements where the inner if is the sole statement in the outer if's body.
Fix: Rewrites the nested if statements into a single if with a logical AND (&&) condition, preserving the inner block's statements exactly as they were.
11 hazards
- The rewrite changes evaluation order: in the original, `b` is only evaluated if `a` is truthy, but in the fixed version `b` is always evaluated regardless of `a`'s value, which can cause errors if `b` has side effects or throws when `a` is falsy.
- If `a` is an expression with side effects (e.g., a function call), the original evaluates it once, but the fixed version may evaluate it twice due to how `&&` short-circuiting works in some contexts—though actually `a && b` still evaluates `a` only once, so this hazard is invalid; instead, consider: if `a` is falsy, the original skips evaluating `b`, but the fixed version evaluates `b` unconditionally in the `&&` expression, leading to potential errors.
- Comments inside the outer if block but outside the inner if (e.g., after the opening brace and before the inner if) are removed by the autofix because the fix collapses the entire block into a single line, discarding intermediate comments.
- If the inner block contains a `break` statement, the original `break` applies to the nearest enclosing loop or switch, but after rewriting, the context may change if the if statement is nested within other constructs, potentially altering which construct the `break` targets.
- If the inner block contains a `continue` statement, similar to `break`, its target loop may be misinterpreted after the structural change, especially if the if statement is not directly inside a loop.
- If the inner block contains a `return` statement, the function return behavior is preserved, but if the outer if is part of a larger expression or statement, the control flow change could affect surrounding code—though `return` exits the function immediately, so this is less likely; however, in arrow functions or edge cases with commas, it might matter.
- If the inner block declares a variable with `let` or `const`, the original confines that variable to the inner block's scope, but after rewriting into a single if block, the variable remains in the same scope (the outer if's block), so scoping is preserved—wait, no: in both cases, the variable is scoped to the outer if's block because the inner block is the only statement; thus, no change. Instead, consider: if the inner block is empty (though the rule requires a statement), but it's not. Another idea: if the condition `a` or `b` involves `this` or `super` in a method, the evaluation context is unchanged, so no issue. Focus on comments and evaluation order.
- If the outer if block has trailing comments after the closing brace, they are unaffected, but if there are comments between the outer `{` and the inner `if`, they are lost.
- In rare cases involving ASI (Automatic Semicolon Insertion), rewriting a nested if into a single line could alter semicolon insertion if the surrounding code relies on newlines, though this is highly speculative and unlikely in practice.
- If the inner if statement is labeled (e.g., `inner: if (b) { ... }`), the label is discarded in the fix because the collapsed form does not retain labels, breaking any `break inner;` or `continue inner;` references.
- If the inner block contains a debugger statement, it is preserved, but if the autofix process mishandles it due to formatting, it could be dropped—though formatters usually preserve it.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | Evaluation order changed due to && | In the original, `b` is only evaluated if `a` is truthy, but in the fixed version `b` is always evaluated regardless of `a`'s value. |
| r1-p002 | fired, no fix | Comment removed by autofix | Comments inside the outer if block but outside the inner if are removed by the autofix. |
| r1-p003 | silent prediction off | Temporal Dead Zone hazard via let scoping misunderstanding | Although let scoping is preserved, this program checks for mistaken assumptions about hoisting or TDZ if code is reordered incorrectly. |
| r1-p004 | silent prediction off | Regex lastIndex side effect with g flag | Merging conditions with && causes the regex to be tested twice, advancing lastIndex and skipping matches. |
| r1-p005 | fix ok | Side effect duplication in merged condition | The condition `b()` is evaluated once in original but twice in fixed version due to &&, causing side effects to run twice. |
| r1-p006 | fix ok | Async/await ordering changed by && evaluation | In `if (a) { if (await b()) { ... } }`, the await is only called if a is true, but `if (a && await b())` always evaluates await b() even if a is false. |
| r1-p007 | fix ok | Function declaration in block affected by scoping perception | Function declarations inside blocks are hoisted to the function scope, but the collapse may mislead about visibility—though semantics are preserved, we capture any confusion. |
| r1-p008 | silent prediction off | Generator yield position affected by early exit | The collapse does not change yield position, but we test interaction with loops and re-entry to ensure deterministic behavior. |
| r2-p001 | fix ok | Comment loss between braces | If the inner block contains a `break` statement, the original `break` applies to the nearest enclosing loop or switch, but after rewriting, the context may change if the if statement is nested within |
| r2-p002 | fix ok | Function declaration in block scoping | function declarations inside blocks, switch cases, labeled statements |
| r2-p003 | fix ok | Label/break/continue/finally control flow | label/break/continue control flow, finally blocks, early returns in nested blocks |
| r2-p004 | fix ok | Debugger statement preservation | If the inner block contains a debugger statement, it is preserved, but if the autofix process mishandles it due to formatting, it could be dropped—though formatters usually preserve it. |
| r2-p005 | fix ok | Strict vs sloppy mode semantics | strict vs sloppy mode semantics |
| r2-p006 | fix ok | Falsy outer condition skipping inner eval | if `a` is falsy, the original skips evaluating `b`, but the fixed version evaluates `b` unconditionally in the `&&` expression, leading to potential errors. |
| r2-p007 | fix ok | Arguments object and rest parameters interaction | arguments object, function.length, default parameters, rest parameters |
| r2-p008 | fired, no fix | Comment loss between outer { and inner if | If the outer if block has trailing comments after the closing brace, they are unaffected, but if there are comments between the outer `{` and the inner `if`, they are lost. |
Sources
r1-p001 · Evaluation order changed due to &&
1let a = false;
2let bEval = 0;
3function b() { bEval++; return true; }
4if (a) { if (b()) { console.log('inner'); } }
5console.log('bEval:', bEval);
--- orig.js 2026-10-17 17:00:00.026457539 -0400 +++ fixed.js 2026-10-17 17:00:00.298460978 -0400 @@ -1,5 +1,5 @@ let a = false; let bEval = 0; function b() { bEval++; return true; } -if (a) { if (b()) { console.log('inner'); } } +if (a && b()) { console.log('inner'); } console.log('bEval:', bEval);
bEval: 0
r1-p002 · Comment removed by autofix
1if (true) {
2 // This comment should be preserved
3 if (false) { console.log('inner'); }
4}
5// After fix, the above becomes: if (true && false) { ... }, losing the comment
6console.log('Comment presence affects readability only; no runtime change.');
Comment presence affects readability only; no runtime change.
r1-p003 · Temporal Dead Zone hazard via let scoping misunderstanding
1if (true) {
2 if (false) {
3 console.log(x); // TDZ if let x were here, but it's not
4 }
5 let x = 1;
6}
7console.log('x after:', typeof x === 'undefined' ? 'undefined' : x);
x after: undefined
r1-p004 · Regex lastIndex side effect with g flag
1let str = 'abab';
2let re = /a/g;
3if (true) {
4 if (re.test(str)) { console.log('1st:', re.lastIndex); }
5 if (re.test(str)) { console.log('2nd:', re.lastIndex); }
6}
7console.log('Final lastIndex:', re.lastIndex);
1st: 1 2nd: 3 Final lastIndex: 3
r1-p005 · Side effect duplication in merged condition
1let a = true;
2let counter = 0;
3function b() { counter++; return true; }
4if (a) { if (b()) { console.log('inner'); } }
5console.log('counter:', counter);
--- orig.js 2026-10-17 17:00:01.186472206 -0400 +++ fixed.js 2026-10-17 17:00:01.443475455 -0400 @@ -1,5 +1,5 @@ let a = true; let counter = 0; function b() { counter++; return true; } -if (a) { if (b()) { console.log('inner'); } } +if (a && b()) { console.log('inner'); } console.log('counter:', counter);
inner counter: 1
r1-p006 · Async/await ordering changed by && evaluation
1let a = false;
2let bCalled = false;
3async function b() { bCalled = true; return true; }
4(async () => {
5 if (a) { if (await b()) { console.log('inner'); } }
6 console.log('bCalled:', bCalled);
7})();
--- orig.js 2026-10-17 17:00:01.608477540 -0400 +++ fixed.js 2026-10-17 17:00:01.881480991 -0400 @@ -2,6 +2,6 @@ let bCalled = false; async function b() { bCalled = true; return true; } (async () => { - if (a) { if (await b()) { console.log('inner'); } } + if (a && (await b())) { console.log('inner'); } console.log('bCalled:', bCalled); })();
bCalled: false
r1-p007 · Function declaration in block affected by scoping perception
1if (true) {
2 if (false) {
3 function inner() { return 1; }
4 console.log(typeof inner); // 'function' due to hoisting
5 }
6}
7console.log('inner after:', typeof inner); // 'function' due to hoisting to outer scope
--- orig.js 2026-10-17 17:00:02.049483114 -0400 +++ fixed.js 2026-10-17 17:00:02.322486563 -0400 @@ -1,7 +1,5 @@ -if (true) { - if (false) { +if (true && false) { function inner() { return 1; } console.log(typeof inner); // 'function' due to hoisting } -} console.log('inner after:', typeof inner); // 'function' due to hoisting to outer scope
inner after: undefined
r1-p008 · Generator yield position affected by early exit
1function* gen() {
2 if (true) {
3 if (false) { yield 1; }
4 yield 2;
5 }
6}
7let g = gen();
8console.log([...g]);
[ 2 ]
r2-p001 · Comment loss between braces
1// Hazard 2: break target change
2let sum = 0;
3outer:
4for (let i = 0; i < 2; i++) {
5 if (i === 0) {
6 if (true) {
7 break outer;
8 }
9 }
10 sum += i;
11}
12console.log('sum:', sum);
--- orig.js 2026-10-17 17:00:08.628566110 -0400 +++ fixed.js 2026-10-17 17:00:08.904569586 -0400 @@ -2,11 +2,9 @@ let sum = 0; outer: for (let i = 0; i < 2; i++) { - if (i === 0) { - if (true) { + if (i === 0 && true) { break outer; } - } sum += i; } console.log('sum:', sum);
sum: 0
r2-p002 · Function declaration in block scoping
1// Hazard 3: function declaration in block
2(function() {
3 if (true) {
4 if (false) {
5 function f() { return 1; }
6 return f();
7 }
8 }
9 return typeof f === 'function' ? f() : 0;
10})();
--- orig.js 2026-10-17 17:00:09.074571727 -0400 +++ fixed.js 2026-10-17 17:00:09.311574710 -0400 @@ -1,10 +1,8 @@ // Hazard 3: function declaration in block (function() { - if (true) { - if (false) { + if (true && false) { function f() { return 1; } return f(); } - } return typeof f === 'function' ? f() : 0; })();
r2-p003 · Label/break/continue/finally control flow
1// Hazard 4: label and finally with break
2let result = [];
3loop:
4for (let i = 0; i < 2; i++) {
5 try {
6 if (i === 0) {
7 if (true) {
8 break loop;
9 }
10 }
11 result.push(i);
12 } finally {
13 result.push('finally');
14 }
15}
16console.log(result.join(','));
--- orig.js 2026-10-17 17:00:09.471576725 -0400 +++ fixed.js 2026-10-17 17:00:09.751580250 -0400 @@ -3,11 +3,9 @@ loop: for (let i = 0; i < 2; i++) { try { - if (i === 0) { - if (true) { + if (i === 0 && true) { break loop; } - } result.push(i); } finally { result.push('finally');
finally
r2-p004 · Debugger statement preservation
1// Hazard 5: debugger statement
2let x = 0;
3if (true) {
4 if (false) {
5 debugger;
6 x = 1;
7 }
8}
9console.log('x:', x);
--- orig.js 2026-10-17 17:00:09.928582478 -0400 +++ fixed.js 2026-10-17 17:00:10.197585864 -0400 @@ -1,9 +1,7 @@ // Hazard 5: debugger statement let x = 0; -if (true) { - if (false) { +if (true && false) { debugger; x = 1; } -} console.log('x:', x);
x: 0
r2-p005 · Strict vs sloppy mode semantics
1'use strict';
2// Hazard 6: strict mode
3let obj = {};
4if (obj) {
5 if (obj.prop === undefined) {
6 obj.prop = 42;
7 }
8}
9console.log('obj.prop:', obj.prop);
--- orig.js 2026-10-17 17:00:10.374588091 -0400 +++ fixed.js 2026-10-17 17:00:10.640591438 -0400 @@ -1,9 +1,7 @@ 'use strict'; // Hazard 6: strict mode let obj = {}; -if (obj) { - if (obj.prop === undefined) { +if (obj && obj.prop === undefined) { obj.prop = 42; } -} console.log('obj.prop:', obj.prop);
obj.prop: 42
r2-p006 · Falsy outer condition skipping inner eval
1// Hazard 7: falsy outer skips inner
2let called = false;
3function side() {
4 called = true;
5 return true;
6}
7if (false) {
8 if (side()) {
9 // should not run
10 }
11}
12console.log('side called:', called);
--- orig.js 2026-10-17 17:00:10.808593552 -0400 +++ fixed.js 2026-10-17 17:00:11.082596999 -0400 @@ -4,9 +4,7 @@ called = true; return true; } -if (false) { - if (side()) { +if (false && side()) { // should not run } -} console.log('side called:', called);
side called: false
r2-p007 · Arguments object and rest parameters interaction
1// Hazard 8: arguments and rest
2function test(...args) {
3 if (args.length > 0) {
4 if (args[0] === 42) {
5 return arguments[0];
6 }
7 }
8 return -1;
9}
10console.log('result:', test(42, 99));
--- orig.js 2026-10-17 17:00:11.250599112 -0400 +++ fixed.js 2026-10-17 17:00:11.531602646 -0400 @@ -1,10 +1,8 @@ // Hazard 8: arguments and rest function test(...args) { - if (args.length > 0) { - if (args[0] === 42) { + if (args.length > 0 && args[0] === 42) { return arguments[0]; } - } return -1; } console.log('result:', test(42, 99));
result: 42
r2-p008 · Comment loss between outer { and inner if
1// Hazard 1: comment between braces
2let log = '';
3if (true) { /* keep me */
4 if (false) {
5 log += 'inner';
6 }
7}
8if (log === '') log = 'none';
9console.log('log:', log);
log: none