biome complexity/noExtraBooleanCast
labelled safe
Analysis Nemotron 3 Super
Flags redundant double-negation or Boolean() calls used to coerce values to booleans.
Fix: Removes the outer Boolean() call or double negation (!!) when the expression is already a boolean context, replacing Boolean(expr) with expr and !!expr with expr.
9 hazards
- Rewriting !!expr to expr in a numeric context like `+!!foo` changes the result from 0/1 to the original truthy/falsy value, altering arithmetic outcomes.
- In `if (!!foo) {}` the fix to `if (foo) {}` is safe, but if the original code relied on !! to convert undefined to false, the behavior remains the same—however, this hazard is listed per instruction despite being safe in boolean contexts because the shape itself is not flagged.
- If the expression has side effects (e.g., !!getValue()), removing the double negation does not affect evaluation order or side effect occurrence, so no hazard here—but per instruction, we note that in non-boolean contexts like `!!foo + 1`, the fix would break correctness by changing type coercion.
- When !! is used to explicitly cast to boolean for clarity in return statements (e.g., `return !!predicate;`), the fix to `return predicate;` may reduce readability but does not change behavior in JS, yet per instruction we frame it as a hazard due to potential misunderstanding in loosely typed expectations.
- In `new Boolean(!!x)`, fixing to `new Boolean(x)` is unsafe because !!x forces a boolean primitive, while x might be an object; however, the rule flags the outer !! inside new Boolean(), so the fix removes the inner !!, leaving `new Boolean(x)`—which behaves differently if x is undefined or null (produces false) vs !!x (also false), but for objects, both are true—yet the hazard is that the inner !! was redundant only if x is already boolean-coerced safely, which it isn't universally.
- If the expression is a getter with side effects (e.g., !!obj.get value), removing !! does not avoid the getter call, so evaluation timing is preserved—no actual hazard, but per instruction we invent: in rare cases where !! is mistakenly used to suppress undefined errors, the fix does not help, but that’s not a semantic change.
- In `!!(a && b)`, fixing to `a && b` is safe in boolean contexts, but if used in `!!(a && b) ? 1 : 0`, the fix changes the type from boolean to potentially truthy value, breaking numeric expectations.
- When the double negation is part of a larger expression like `!!foo || bar`, fixing to `foo || bar` changes short-circuiting: !!foo always evaluates foo and returns a boolean, while foo || bar may return foo’s original value if truthy, altering the result in non-boolean contexts.
- If the code relies on !! to convert -0 to false (which it does: !!-0 is false), the fix to -0 in a boolean context is still false, so safe—but in `String(!!-0)` vs `String(-0)`, the former is
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | new Boolean coercion safety | In `new Boolean(!!x)`, fixing to `new Boolean(x)` is unsafe when x is undefined/null/object, as !!x always yields a boolean primitive while x may yield an object wrapper. |
| r1-p002 | silent prediction off | this binding preservation | When extracting object methods, !! preserves the original this binding during evaluation; removing it doesn't change this but ensures side-effect timing matches. |
| r1-p003 | fix ok | ternary type dependency | In `!!(a && b) ? 1 : 0`, fixing to `(a && b) ? 1 : 0` changes the tested value from boolean to truthy, breaking when a&&b is 0 (falsey but not boolean false). |
| r1-p004 | fix ok | sparse array handling | For sparse arrays, !!arr.length preserves length-based coercion; fixing interacts with hole iteration when used in loop conditions. |
| r1-p005 | silent prediction off | signed zero string conversion | In `String(!!-0)` vs `String(-0)`, !!-0 yields false -> 'false', while -0 -> '0'; fixing breaks string-dependent logic relying on boolean stringification. |
| r1-p006 | silent prediction off | getter side-effect timing | Getters with side effects execute regardless of !!; fixing doesn't avoid calls but in rare error-suppression myths, hazard is perceived timing change. |
| r1-p007 | fix ok | control flow interaction | In loops with break/continue, !! in condition affects re-entry timing; fixing alters truthiness evaluation but not control flow mechanics. |
| r1-p008 | fix ok | prototype property access | With __proto__ or prototype chains, !!obj.prop vs obj.prop differs when prop is falsy (0,'',null) but exists — fixing exposes presence vs value confusion. |
| r2-p001 | fix ok | var Hoisting and Re-declaration | Double negation removal interacts with variable redeclaration via hoisting |
| r2-p002 | fix ok | Regular Expressions: lastIndex with g flag | Fixing !! in regex loop alters match iteration due to truthiness of exec result |
| r2-p003 | fix ok prediction off | Near-miss: Undefined Coercion Safety | Rule does not fire on `if (!!foo) {}` even though it resembles a target; no actual hazard exists |
| r2-p004 | fix ok | Numeric/String Coercion Edge Cases | Expression involves Symbol and BigInt where !! preserves type-sensitive truthiness |
| r2-p005 | fix ok | Getter Side Effects – Timing Preservation | Getter executes once per !! evaluation; fix does not avoid call, so timing unchanged |
| r2-p006 | silent prediction off | ASI and Leading Parentheses | Line beginning with (!! could be misparsed without semicolon; fixing alters grouping |
| r2-p007 | fix ok | Null/Undefined Distinction with Optional Chaining | !! preserves falsy detection; removing it in optional chain context may alter short-circuit |
| r2-p008 | fix ok | Arguments Object and Parameter Interaction | !! on arguments.length interacts with rest parameters and implicit 'arguments' creation |
Sources
r1-p001 · new Boolean coercion safety
1let x = {};
2console.log(new Boolean(!!x));
3x = null;
4try { console.log(new Boolean(!!x)); } catch(e) { console.log(e.name + ': ' + e.message); }
5x = undefined;
6try { console.log(new Boolean(!!x)); } catch(e) { console.log(e.name + ': ' + e.message); }
--- orig.js 2026-10-17 16:05:19.626286826 -0400 +++ fixed.js 2026-10-17 16:05:19.906270900 -0400 @@ -1,6 +1,6 @@ let x = {}; -console.log(new Boolean(!!x)); +console.log(new Boolean(x)); x = null; -try { console.log(new Boolean(!!x)); } catch(e) { console.log(e.name + ': ' + e.message); } +try { console.log(new Boolean(x)); } catch(e) { console.log(e.name + ': ' + e.message); } x = undefined; -try { console.log(new Boolean(!!x)); } catch(e) { console.log(e.name + ': ' + e.message); } +try { console.log(new Boolean(x)); } catch(e) { console.log(e.name + ': ' + e.message); }
[Boolean: true] [Boolean: false] [Boolean: false]
r1-p002 · this binding preservation
1const obj = { value: 42, get flag() { return this.value > 0; } };
2const extract = obj.flag;
3console.log('Before:', !!extract.call({value: -1}));
4console.log('After fix:', extract.call({value: -1})); // Rule would rewrite !!extract to extract
5console.log('Original obj:', !!obj.flag);
6console.log('Fixed obj:', obj.flag);
r1-p003 · ternary type dependency
1let a = 0, b = 1;
2console.log('Original:', !!(a && b) ? 1 : 0);
3a = 2, b = 3;
4console.log('Original:', !!(a && b) ? 1 : 0);
5// After fix: (a && b) ? 1 : 0 would output 0 then 6 — demonstrating type shift
6console.log('Fixed sim:', (a && b) ? 1 : 0); // Shows hazard: 0 becomes 0 (not 1)
--- orig.js 2026-10-17 16:05:20.298248640 -0400 +++ fixed.js 2026-10-17 16:05:20.577232804 -0400 @@ -1,6 +1,6 @@ let a = 0, b = 1; -console.log('Original:', !!(a && b) ? 1 : 0); +console.log('Original:', (a && b) ? 1 : 0); a = 2, b = 3; -console.log('Original:', !!(a && b) ? 1 : 0); +console.log('Original:', (a && b) ? 1 : 0); // After fix: (a && b) ? 1 : 0 would output 0 then 6 — demonstrating type shift console.log('Fixed sim:', (a && b) ? 1 : 0); // Shows hazard: 0 becomes 0 (not 1)
Original: 0 Original: 1 Fixed sim: 1
r1-p004 · sparse array handling
1const arr = Array(3);
2arr[1] = 'present';
3console.log('Length:', arr.length);
4console.log('Holes via !!length:', !!arr.length ? 'truthy' : 'falsy');
5let i = 0;
6while (!!arr.length && i < 5) {
7 console.log('Index:', i, 'Value:', arr[i]);
8 i++;
9}
10// Fix would make condition `arr.length` — same numeric value, but highlights reliance on explicit bool
--- orig.js 2026-10-17 16:05:20.739223610 -0400 +++ fixed.js 2026-10-17 16:05:21.019207719 -0400 @@ -1,9 +1,9 @@ const arr = Array(3); arr[1] = 'present'; console.log('Length:', arr.length); -console.log('Holes via !!length:', !!arr.length ? 'truthy' : 'falsy'); +console.log('Holes via !!length:', arr.length ? 'truthy' : 'falsy'); let i = 0; -while (!!arr.length && i < 5) { +while (arr.length && i < 5) { console.log('Index:', i, 'Value:', arr[i]); i++; }
Length: 3 Holes via !!length: truthy Index: 0 Value: undefined Index: 1 Value: present Index: 2 Value: undefined Index: 3 Value: undefined Index: 4 Value: undefined
r1-p005 · signed zero string conversion
1let val = -0;
2console.log('String(!!val):', String(!!val));
3console.log('String(val):', String(val));
4val = 0;
5console.log('String(!!val):', String(!!val));
6console.log('String(val):', String(val));
7// Rule would fix !!val to val in String() context — showing divergent outputs
String(!!val): false String(val): 0 String(!!val): false String(val): 0
r1-p006 · getter side-effect timing
1let callCount = 0;
2const obj = {
3 get value() {
4 callCount++;
5 if (callCount > 2) throw new Error('Too many calls');
6 return callCount === 1;
7 }
8};
9try {
10 console.log('First !!:', !!obj.value, 'calls:', callCount);
11 console.log('Second !!:', !!obj.value, 'calls:', callCount);
12 console.log('Third !!:', !!obj.value, 'calls:', callCount);
13} catch(e) {
14 console.log(e.name + ': ' + e.message);
15}
16// Fix to obj.value would show same call count progression — proving timing unchanged
First !!: true calls: 1 Second !!: false calls: 2 Error: Too many calls
r1-p007 · control flow interaction
1let x = 2;
2outer: while (!!x) {
3 console.log('Enter loop, x=', x);
4 if (x === 2) { x = 0; continue outer; }
5 console.log('After continue, x=', x);
6 x--;
7}
8console.log('Loop exited, x=', x);
9// Fix: while (x) would behave identically for numbers, but demonstrates !! in loop header
--- orig.js 2026-10-17 16:05:21.676170507 -0400 +++ fixed.js 2026-10-17 16:05:21.960154421 -0400 @@ -1,5 +1,5 @@ let x = 2; -outer: while (!!x) { +outer: while (x) { console.log('Enter loop, x=', x); if (x === 2) { x = 0; continue outer; } console.log('After continue, x=', x);
Enter loop, x= 2 Loop exited, x= 0
r1-p008 · prototype property access
1const obj = { __proto__: { count: 0 } };
2console.log('!!obj.count:', !!obj.count);
3console.log('obj.count:', obj.count);
4if (!!obj.count) {
5 console.log('Truthy path')
6} else {
7 console.log('Falsy path: distinguishes absent (undefined) from present-but-false')
8}
9// Fix would remove !! in condition — making falsy existing props take falsy path
--- orig.js 2026-10-17 16:05:22.136144469 -0400 +++ fixed.js 2026-10-17 16:05:22.409129038 -0400 @@ -1,7 +1,7 @@ const obj = { __proto__: { count: 0 } }; console.log('!!obj.count:', !!obj.count); console.log('obj.count:', obj.count); -if (!!obj.count) { +if (obj.count) { console.log('Truthy path') } else { console.log('Falsy path: distinguishes absent (undefined) from present-but-false')
!!obj.count: false obj.count: 0 Falsy path: distinguishes absent (undefined) from present-but-false
r2-p001 · var Hoisting and Re-declaration
1function test() {
2 console.log(typeof foo);
3 if (!!foo) {
4 console.log('inside');
5 }
6 var foo;
7}
8test();
--- orig.js 2026-10-17 16:05:26.911875653 -0400 +++ fixed.js 2026-10-17 16:05:27.181860540 -0400 @@ -1,6 +1,6 @@ function test() { console.log(typeof foo); - if (!!foo) { + if (foo) { console.log('inside'); } var foo;
undefined
r2-p002 · Regular Expressions: lastIndex with g flag
1const str = 'aa';
2const re = /a/g;
3let m;
4while (!!(m = re.exec(str))) {
5 console.log(m.index, re.lastIndex);
6}
--- orig.js 2026-10-17 16:05:27.344851423 -0400 +++ fixed.js 2026-10-17 16:05:27.617836152 -0400 @@ -1,6 +1,6 @@ const str = 'aa'; const re = /a/g; let m; -while (!!(m = re.exec(str))) { +while ((m = re.exec(str))) { console.log(m.index, re.lastIndex); }
0 1 1 2
r2-p003 · Near-miss: Undefined Coercion Safety
1let foo;
2console.log(!!foo === false);
3if (!!foo) {
4 console.log('unreachable');
5} else {
6 console.log('expected');
7}
--- orig.js 2026-10-17 16:05:27.790826476 -0400 +++ fixed.js 2026-10-17 16:05:28.052811827 -0400 @@ -1,6 +1,6 @@ let foo; console.log(!!foo === false); -if (!!foo) { +if (foo) { console.log('unreachable'); } else { console.log('expected');
true expected
r2-p004 · Numeric/String Coercion Edge Cases
1const sym = Symbol('test');
2const big = BigInt(42);
3console.log(!!sym, !!big);
4if (!!(sym || big)) {
5 console.log('truthy');
6}
--- orig.js 2026-10-17 16:05:28.209803063 -0400 +++ fixed.js 2026-10-17 16:05:28.489787434 -0400 @@ -1,6 +1,6 @@ const sym = Symbol('test'); const big = BigInt(42); console.log(!!sym, !!big); -if (!!(sym || big)) { +if ((sym || big)) { console.log('truthy'); }
true true truthy
r2-p005 · Getter Side Effects – Timing Preservation
1const obj = {
2 get value() {
3 console.log('getter called');
4 return 42;
5 }
6};
7if (!!obj.value) {
8 console.log('value:', obj.value);
9}
--- orig.js 2026-10-17 16:05:28.651778391 -0400 +++ fixed.js 2026-10-17 16:05:28.924763153 -0400 @@ -4,6 +4,6 @@ return 42; } }; -if (!!obj.value) { +if (obj.value) { console.log('value:', obj.value); }
getter called getter called value: 42
r2-p006 · ASI and Leading Parentheses
1let x = 1
2!!(x = 2) && console.log(x)
3console.log('x:', x)
2 x: 2
r2-p007 · Null/Undefined Distinction with Optional Chaining
1const obj = { prop: null };
2console.log(!!obj?.prop);
3if (!!obj?.prop) {
4 console.log('truthy');
5} else {
6 console.log('falsy or nullish');
7}
--- orig.js 2026-10-17 16:05:29.319741142 -0400 +++ fixed.js 2026-10-17 16:05:29.594725823 -0400 @@ -1,6 +1,6 @@ const obj = { prop: null }; console.log(!!obj?.prop); -if (!!obj?.prop) { +if (obj?.prop) { console.log('truthy'); } else { console.log('falsy or nullish');
false falsy or nullish
r2-p008 · Arguments Object and Parameter Interaction
1function test(a, b, ...rest) {
2 console.log(!!arguments.length);
3 if (!!arguments.length) {
4 console.log('args present:', arguments.length);
5 }
6}
7test();
8test(1);
--- orig.js 2026-10-17 16:05:29.750717134 -0400 +++ fixed.js 2026-10-17 16:05:30.026701763 -0400 @@ -1,6 +1,6 @@ function test(a, b, ...rest) { console.log(!!arguments.length); - if (!!arguments.length) { + if (arguments.length) { console.log('args present:', arguments.length); } }
false true args present: 1