eslint logical-assignment-operators
labelled safe
Analysis Nemotron 3 Super
Flags assignments where the LHS matches the left operand of a logical OR, AND, or nullish coalescing operation.
Fix: Rewrites 'LHS = LHS op RHS' into 'LHS op= RHS', preserving whitespace and semicolons.
8 hazards
- Changing 'x = x || y;' to 'x ||= y;' alters evaluation when x is a getter with side effects, as the logical assignment evaluates LHS only once.
- Rewriting 'y || (y = z);' to 'y ||= z;' moves the assignment inside the logical expression, changing when y is assigned if y is falsy but z throws.
- If LHS is a complex expression like 'obj.prop', the rewrite may differ in evaluation count if obj.prop has a getter with side effects.
- When LHS is an array element access like 'arr[i]', the rewrite changes behavior if i has side effects (e.g., function call) because logical assignment evaluates LHS once vs twice.
- In strict mode, if LHS refers to an undeclared variable, both forms throw ReferenceError, but the timing differs slightly due to evaluation count.
- Comments between LHS and '=' or between '=' and RHS may be lost or repositioned during the rewrite, depending on formatter behavior.
- If RHS contains a yield or await, moving it inside the logical assignment shortcircuit could alter async/generator suspension points.
- When LHS is a pattern like '[a]' or '{b}', the rewrite is invalid syntax and will cause a parse error, though the rule should not flag such cases.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | null/undefined distinction, optional chaining short-circuit, nullish vs falsy | Logical assignment with nullish coalescing can mask undefined vs null differences; rewriting may alter evaluation order or short-circuit behavior. |
| r1-p002 | invalid | ASI and semicolon-less code, leading ( or [ on the next line | Automatic semicolon insertion can cause misparsing when logical assignment is followed by a line starting with ( or [, and fixing may disrupt ASI boundaries. |
| r1-p003 | invalid | If RHS contains a yield or await, moving it inside the logical assignment shortcircuit could alter async/generator suspe | Rewriting `x = x || (await y)` to `x ||= await y` changes when await is executed—only if x is falsy—affecting suspension timing in async functions. |
| r1-p004 | fix ok prediction off | function declarations inside blocks, switch cases, labeled statements | Function declarations in blocks are scoped; rewriting logical assignment may interfere with hoisting or TDZ if LHS is a function name in a conditional block. |
| r1-p005 | fired, no fix | Changing 'x = x || y;' to 'x ||= y;' alters evaluation when x is a getter with side effects, as the logical assignment e | Getters with side effects (e.g., logging) are evaluated once in `x ||= y` but twice in `x = x || y`; fixing changes observable behavior. |
| r1-p006 | silent prediction off | When LHS is an array element access like 'arr[i]', the rewrite changes behavior if i has side effects (e.g., function ca | If `i` is a function call with side effects, `arr[i] = arr[i] || y` calls it twice, but `arr[i] ||= y` calls it once—fixing alters call count. |
| r1-p007 | fix ok | regular expressions: escapes, unicode flag, lastIndex with the g flag | Logical assignment with regexps may misbehave due to shared lastIndex state; fixing may alter how many times test/exec is called. |
| r1-p008 | fired, no fix | Comments between LHS and '=' or between '=' and RHS may be lost or repositioned during the rewrite, depending on formatt | Comments attached to the assignment operator may be stripped or moved during autofix, potentially altering readability or tooling expectations. |
| r2-p001 | fired, no fix | Comments around assignment | Comments between LHS and '=' or between '=' and RHS may be lost or repositioned during the rewrite, depending on formatter behavior. |
| r2-p002 | fired, no fix | __proto__ and prototype mutation | __proto__ keys, prototype access, Object.assign vs spread |
| r2-p003 | fix ok | Function declaration in block | function declarations inside blocks, switch cases, labeled statements |
| r2-p004 | fix ok | Var hoisting and redeclaration | var hoisting and re-declaration inside loops or after use |
| r2-p005 | fix ok prediction off | Short-circuit with throw in RHS | Rewriting 'y || (y = z);' to 'y ||= z;' moves the assignment inside the logical expression, changing when y is assigned if y is falsy but z throws. |
| r2-p006 | silent prediction off | Temporal dead zone interaction | temporal dead zone after code is hoisted or reordered |
| r2-p007 | invalid | Pattern LHS invalid syntax | When LHS is a pattern like '[a]' or '{b}', the rewrite is invalid syntax and will cause a parse error, though the rule should not flag such cases. |
| r2-p008 | fix ok | This binding in extracted methods | this binding: arrow vs function, methods extracted from objects, call/apply/bind |
Sources
r1-p001 · null/undefined distinction, optional chaining short-circuit, nullish vs falsy
1let x = undefined;
2let y = null;
3console.log(x ??= y);
4console.log(x);
null null
r1-p002 · ASI and semicolon-less code, leading ( or [ on the next line (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-13-3)
1let a
2= 5
3a = a || [1, 2].pop()
4console.log(a)
5[].push(0)
r1-p003 · If RHS contains a yield or await, moving it inside the logical assignment shortcircuit could alter async/generator suspe (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-13-3)
1async function f() {
2 let x = false;
3 console.log(await x ||= Promise.resolve(1));
4 console.log(x);
5}
6f().then(() => console.log('done'));
r1-p004 · function declarations inside blocks, switch cases, labeled statements
1if (true) {
2 function foo() { return 1; }
3 let x = typeof foo;
4 foo = foo || function() { return 2; };
5 console.log(typeof foo, x);
6}
--- orig.js 2026-10-22 14:13:40.896672850 -0400 +++ fixed.js 2026-10-22 14:13:43.749723024 -0400 @@ -1,6 +1,6 @@ if (true) { function foo() { return 1; } let x = typeof foo; - foo = foo || function() { return 2; }; + foo ||= function() { return 2; }; console.log(typeof foo, x); }
function function
r1-p005 · Changing 'x = x || y;' to 'x ||= y;' alters evaluation when x is a getter with side effects, as the logical assignment e
1const obj = {
2 get x() {
3 console.log('getter');
4 return this._x;
5 },
6 _x: 0
7};
8obj.x = obj.x || 5;
9console.log(obj._x);
getter 0
r1-p006 · When LHS is an array element access like 'arr[i]', the rewrite changes behavior if i has side effects (e.g., function ca
1let i = 0;
2function next() { return i++; }
3const arr = [0, 0];
4arr[next()] = arr[next()] || 42;
5console.log('i:', i, 'arr:', arr);
i: 2 arr: [ 42, 0 ]
r1-p007 · regular expressions: escapes, unicode flag, lastIndex with the g flag
1let regex = /a/g;
2let str = 'ab';
3console.log(regex.test(str));
4regex = regex || /b/g;
5console.log(regex.test(str));
6console.log(regex.lastIndex);
--- orig.js 2026-10-22 14:13:40.939673598 -0400 +++ fixed.js 2026-10-22 14:13:45.503754323 -0400 @@ -1,6 +1,6 @@ let regex = /a/g; let str = 'ab'; console.log(regex.test(str)); -regex = regex || /b/g; +regex ||= /b/g; console.log(regex.test(str)); console.log(regex.lastIndex);
true false 0
r1-p008 · Comments between LHS and '=' or between '=' and RHS may be lost or repositioned during the rewrite, depending on formatt
1let x = 0; // comment before
2x /* inline */ = x /* middle */ || 1 /* after */;
3console.log(x);
1
r2-p001 · Comments around assignment
1// comment before
2let x = 0;
3// x = x /* mid */ || 1; // after
4x = x /* LHS comment */ || /* RHS comment */ 1;
5console.log(x);
1
r2-p002 · __proto__ and prototype mutation
1let obj = {};
2obj.__proto__ = obj.__proto__ || { toString() { return 'proto'; } };
3console.log(obj.toString());
4obj.__proto__ = obj.__proto__ && { valueOf() { return 42; } };
5console.log(obj.valueOf());
[object Object] 42
r2-p003 · Function declaration in block
1{ let f = 0; if (true) { function g() { return 1; } f = f || g(); } console.log(f); }
2{ let h = 0; switch (1) { case 1: function k() { return 2; } h = h && k(); break; } console.log(h); }
--- orig.js 2026-10-22 14:13:52.337879502 -0400 +++ fixed.js 2026-10-22 14:13:55.200933453 -0400 @@ -1,2 +1,2 @@ -{ let f = 0; if (true) { function g() { return 1; } f = f || g(); } console.log(f); } -{ let h = 0; switch (1) { case 1: function k() { return 2; } h = h && k(); break; } console.log(h); } +{ let f = 0; if (true) { function g() { return 1; } f ||= g(); } console.log(f); } +{ let h = 0; switch (1) { case 1: function k() { return 2; } h &&= k(); break; } console.log(h); }
1 0
r2-p004 · Var hoisting and redeclaration
1for (var i = 0; i < 2; i++) {
2 var x = 0;
3 x = x || i;
4 console.log('loop:', x);
5}
6console.log('after:', x);
7var x = 99;
8console.log('redeclared:', x);
--- orig.js 2026-10-22 14:13:52.338879521 -0400 +++ fixed.js 2026-10-22 14:13:55.215933738 -0400 @@ -1,6 +1,6 @@ for (var i = 0; i < 2; i++) { var x = 0; - x = x || i; + x ||= i; console.log('loop:', x); } console.log('after:', x);
loop: 0 loop: 1 after: 1 redeclared: 99
r2-p005 · Short-circuit with throw in RHS
1let y = 0;
2try {
3 y = y || (() => { throw new Error('side effect'); })();
4 console.log('assigned:', y);
5} catch (e) {
6 console.log('caught:', e.name, e.message);
7}
8y = 0;
9try {
10 y ||= (() => { throw new Error('side effect'); })();
11 console.log('assigned:', y);
12} catch (e) {
13 console.log('caught:', e.name, e.message);
14}
--- orig.js 2026-10-22 14:13:52.338879521 -0400 +++ fixed.js 2026-10-22 14:13:56.475957768 -0400 @@ -1,6 +1,6 @@ let y = 0; try { - y = y || (() => { throw new Error('side effect'); })(); + y ||= (() => { throw new Error('side effect'); })(); console.log('assigned:', y); } catch (e) { console.log('caught:', e.name, e.message);
caught: Error side effect caught: Error side effect
r2-p006 · Temporal dead zone interaction
1if (true) {
2 console.log(typeof tmp);
3 let tmp = tmp || 'initialized';
4 console.log(tmp);
5}
r2-p007 · Pattern LHS invalid syntax (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-13-3)
1let arr = [1];
2let obj = { a: 2 };
3try {
4 [arr[0]] = [arr[0]] || [3];
5 console.log(arr);
6} catch (e) {
7 console.log('catch pattern:', e.name);
8}
9try {
10 { obj.a } = { obj.a } || { a: 4 };
11 console.log(obj);
12} catch (e) {
13 console.log('catch pattern:', e.name);
14}
r2-p008 · This binding in extracted methods
1const obj = { value: 1, getValue() { return this.value; } };
2let f = obj.getValue;
3let result = f || (f = obj.getValue.bind(obj));
4console.log('bound result:', result());
5f = obj.getValue;
6f && (f = obj.getValue.bind(obj));
7console.log('bound result2:', f());
--- orig.js 2026-10-22 14:13:55.040930407 -0400 +++ fixed.js 2026-10-22 14:13:58.564997973 -0400 @@ -1,7 +1,7 @@ const obj = { value: 1, getValue() { return this.value; } }; let f = obj.getValue; -let result = f || (f = obj.getValue.bind(obj)); +let result = (f ||= obj.getValue.bind(obj)); console.log('bound result:', result()); f = obj.getValue; -f && (f = obj.getValue.bind(obj)); +f &&= obj.getValue.bind(obj); console.log('bound result2:', f());
bound result: undefined bound result2: 1