eslint operator-assignment
labelled safe
Analysis Nemotron 3 Super
Flags assignments like 'x = x op y' where 'op' is an arithmetic or bitwise operator and suggests using the compound assignment shorthand.
Fix: Rewrites 'x = x op y' into 'x op= y', preserving the left-hand side expression and the right-hand side expression exactly as they appear.
10 hazards
- If the left-hand side has side effects (e.g., a function call or increment), evaluating it twice in the original vs once in the fix changes behavior (e.g., x[foo()] = x[foo()] + 1).
- If the left-hand side is a complex expression with operators, rewriting may alter evaluation order or precedence (e.g., (x[y]) = (x[y]) + z vs x[y] += z, though generally safe, relies on LHS being stable).
- If the left-hand side references a variable that is redeclared or changed between evaluations due to hoisting or block scoping, the fix may use a different value (e.g., in a loop with let).
- If the right-hand side has side effects, the original evaluates LHS then RHS; the fix evaluates LHS once then RHS, but if LHS evaluation has side effects, it now happens only once instead of potentially twice.
- If the left-hand side is a property access on an object that has a getter with side effects, accessing it twice in the original vs once in the fix changes behavior.
- If the left-hand side is an array access and the expression used as index has side effects (e.g., x[a++]), evaluating the index twice vs once alters the index value.
- If the code relies on the specific spacing or formatting for parsing by other tools (e.g., templating engines), removing whitespace could break processing.
- If there are comments between the tokens of the assignment (e.g., x = /* comment */ x + y;), the fix may discard those comments.
- If the left-hand side expression is not actually identical due to type coercion or implicit conversion in the original context, the shorthand may not preserve semantics (though rare for these operators).
- If the assignment is part of a larger expression and the rewrite changes the structure (though it's a statement, unlikely), but if used in an expression context, the shorthand returns a value, same as the original, so generally safe.
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | fix ok | async await generator yield interaction | async/await ordering, generators and yield position |
| r1-p002 | fix ok | coercion plus vs template literal | numeric/string coercion: + vs template literal, objects with toString/valueOf, Symbol, BigInt |
| r1-p004 | fired, no fix | comments discarded in fix | If there are comments between the tokens of the assignment (e.g., x = /* comment */ x + y;), the fix may discard those comments. |
| r1-p005 | silent prediction off | array index side effect double eval | If the left-hand side is an array access and the expression used as index has side effects (e.g., x[a++]), evaluating the index twice vs once alters the index value. |
| r1-p006 | fix ok | arguments length default rest | arguments object, function.length, default parameters, rest parameters |
| r1-p007 | invalid | ASI semicolon-less leading paren | ASI and semicolon-less code, leading ( or [ on the next line |
| r1-p008 | fix ok | class fields static block private super | class fields, static blocks, private names, super calls |
| r2-p001 | fix ok | Sparse Arrays Holes | sparse arrays, holes, Array(n) vs [n], array-likes |
| r2-p002 | fix ok | Var Hoisting Redeclaration Loop | var hoisting and re-declaration inside loops or after use |
| r2-p003 | fix ok | Getter Side Effect Property Access | If the left-hand side is a property access on an object that has a getter with side effects, accessing it twice in the original vs once in the fix changes behavior. |
| r2-p004 | fix ok | Arguments Object Default Rest | arguments object, function.length, default parameters, rest parameters |
| r2-p005 | fix ok | Whitespace Dependent Templating | If the code relies on the specific spacing or formatting for parsing by other tools (e.g., templating engines), removing whitespace could break processing. |
| r2-p006 | fix ok | This Binding Arrow Function | this binding: arrow vs function, methods extracted from objects, call/apply/bind |
| r2-p007 | silent prediction off | LHS Side Effect Double Eval | If the left-hand side has side effects (e.g., a function call or increment), evaluating it twice in the original vs once in the fix changes behavior (e.g., x[foo()] = x[foo()] + 1). |
| r2-p008 | silent prediction off | Order of Evaluation Merged Expressions | order of evaluation when expressions are merged, duplicated or deleted (side effects run twice or never) |
Sources
r1-p001 · async await generator yield interaction
1async function* g() {
2 let i = 0;
3 while (i < 3) {
4 const v = await Promise.resolve(i);
5 if ((yield i++) === undefined) continue;
6 i = i + v; // TRIGGER
7 console.log('state:', i);
8 }
9}
10(async () => {
11 const iter = g();
12 for await (const x of iter) console.log('yielded:', x);
13})().catch(e => console.error(e));
--- orig.js 2026-10-22 14:24:49.429813897 -0400 +++ fixed.js 2026-10-22 14:24:52.737960322 -0400 @@ -3,7 +3,7 @@ while (i < 3) { const v = await Promise.resolve(i); if ((yield i++) === undefined) continue; - i = i + v; // TRIGGER + i += v; // TRIGGER console.log('state:', i); } }
yielded: 0 yielded: 1 yielded: 2
r1-p002 · coercion plus vs template literal
1const o = { valueOf: () => 2, toString: () => 'x' };
2let acc = '';
3acc = acc + o; // TRIGGER
4console.log(typeof acc, acc);
5try {
6 acc = acc + Symbol('foo'); // TRIGGER? No, Symbol + string throws
7} catch (e) {
8 console.log(e.name, e.message);
9}
10console.log('final:', acc);
--- orig.js 2026-10-22 14:24:49.430813942 -0400 +++ fixed.js 2026-10-22 14:24:52.636955850 -0400 @@ -1,9 +1,9 @@ const o = { valueOf: () => 2, toString: () => 'x' }; let acc = ''; -acc = acc + o; // TRIGGER +acc += o; // TRIGGER console.log(typeof acc, acc); try { - acc = acc + Symbol('foo'); // TRIGGER? No, Symbol + string throws + acc += Symbol('foo'); // TRIGGER? No, Symbol + string throws } catch (e) { console.log(e.name, e.message); }
string 2 TypeError Cannot convert a Symbol value to a string final: 2
r1-p004 · comments discarded in fix
1let counter = 0;
2counter = counter /* step */ + 1; // TRIGGER
3console.log('after first:', counter);
4counter = counter /* inline */ + counter; // TRIGGER
5console.log('after second:', counter);
after first: 1 after second: 2
r1-p005 · array index side effect double eval
1const arr = [10, 20, 30];
2let idx = 0;
3arr[idx++] = arr[idx++] + 5; // TRIGGER: idx read twice
4console.log('idx after:', idx, 'arr:', arr);
5// Reset
6idx = 0;
7arr[idx++] = arr[idx++] + 5; // Same line again
8console.log('idx after2:', idx, 'arr2:', arr);
idx after: 2 arr: [ 25, 20, 30 ] idx after2: 2 arr2: [ 25, 20, 30 ]
r1-p006 · arguments length default rest
1function sum(a, b = 0, ...rest) {
2 let total = a;
3 total = total + b; // TRIGGER
4 for (const v of rest) total = total + v; // TRIGGER
5 console.log('args length:', arguments.length, 'total:', total);
6 return total;
7}
8console.log(sum(1, 2, 3, 4));
--- orig.js 2026-10-22 14:24:49.432814030 -0400 +++ fixed.js 2026-10-22 14:24:52.801963156 -0400 @@ -1,7 +1,7 @@ function sum(a, b = 0, ...rest) { let total = a; - total = total + b; // TRIGGER - for (const v of rest) total = total + v; // TRIGGER + total += b; // TRIGGER + for (const v of rest) total += v; // TRIGGER console.log('args length:', arguments.length, 'total:', total); return total; }
args length: 4 total: 10 10
r1-p007 · ASI semicolon-less leading paren (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-24-3)
1let x = 1
2x = x
3+ 2 // TRIGGER: ASI would break without fix? Actually x = x
4+2 is invalid; but we rely on the fix changing behavior if applied incorrectly
5// Instead, make a case where ASI affects
6let y = 1
7y = y
8+ 3
9console.log('x:', x, 'y:', y)
r1-p008 · class fields static block private super
1class Base {
2 constructor() { this.x = 0; }
3 method() { return this.x; }
4}
5class Der extends Base {
6 #y = 5;
7 static {
8 Der.prototype.z = Der.prototype.z + 1; // TRIGGER
9 }
10 constructor() {
11 super();
12 this.#y = this.#y + 2; // TRIGGER
13 }
14 getY() { return this.#y; }
15}
16const d = new Der();
17console.log('instance y:', d.getY(), 'static z:', Object.getPrototypeOf(d).z);
--- orig.js 2026-10-22 14:24:49.454815004 -0400 +++ fixed.js 2026-10-22 14:24:52.689958197 -0400 @@ -9,7 +9,7 @@ } constructor() { super(); - this.#y = this.#y + 2; // TRIGGER + this.#y += 2; // TRIGGER } getY() { return this.#y; } }
instance y: 7 static z: NaN
r2-p001 · Sparse Arrays Holes
1let arr = new Array(3);
2arr[0] = 1;
3arr[2] = arr[2] + 1;
4console.log(arr[1]); // hole -> undefined
--- orig.js 2026-10-22 14:24:59.382254681 -0400 +++ fixed.js 2026-10-22 14:25:03.121420489 -0400 @@ -1,4 +1,4 @@ let arr = new Array(3); arr[0] = 1; -arr[2] = arr[2] + 1; +arr[2] += 1; console.log(arr[1]); // hole -> undefined
undefined
r2-p002 · Var Hoisting Redeclaration Loop
1for (var i = 0; i < 2; i++) {
2 var x = 1;
3 x = x + i;
4}
5console.log(x); // hoisted, accessible outside loop
--- orig.js 2026-10-22 14:24:59.383254725 -0400 +++ fixed.js 2026-10-22 14:25:03.144421510 -0400 @@ -1,5 +1,5 @@ for (var i = 0; i < 2; i++) { var x = 1; - x = x + i; + x += i; } console.log(x); // hoisted, accessible outside loop
2
r2-p003 · Getter Side Effect Property Access
1const obj = {
2 get prop() {
3 console.log('get');
4 return 5;
5 }
6};
7obj.prop = obj.prop + 1; // getter called twice originally
--- orig.js 2026-10-22 14:24:59.384254769 -0400 +++ fixed.js 2026-10-22 14:25:03.143421466 -0400 @@ -4,4 +4,4 @@ return 5; } }; -obj.prop = obj.prop + 1; // getter called twice originally +obj.prop += 1; // getter called twice originally
get
r2-p004 · Arguments Object Default Rest
1function f(a = 1, b = 2, ...rest) {
2 arguments[0] = arguments[0] + 1;
3 console.log(arguments.length, rest.length);
4}
5f(); // args: [1,2] -> after: [2,2]; length unchanged but values changed
--- orig.js 2026-10-22 14:24:59.384254769 -0400 +++ fixed.js 2026-10-22 14:25:01.995370544 -0400 @@ -1,5 +1,5 @@ function f(a = 1, b = 2, ...rest) { - arguments[0] = arguments[0] + 1; + arguments[0] += 1; console.log(arguments.length, rest.length); } f(); // args: [1,2] -> after: [2,2]; length unchanged but values changed
0 0
r2-p005 · Whitespace Dependent Templating
1let template = `value: ${ x = x + 1 }`;
2x = 0;
3console.log(template); // depends on spacing inside ${}
--- orig.js 2026-10-22 14:24:59.385254814 -0400 +++ fixed.js 2026-10-22 14:25:03.119420401 -0400 @@ -1,3 +1,3 @@ -let template = `value: ${ x = x + 1 }`; +let template = `value: ${ x += 1 }`; x = 0; console.log(template); // depends on spacing inside ${}
r2-p006 · This Binding Arrow Function
1const obj = {
2 x: 0,
3 update: function() {
4 this.x = this.x + 1;
5 }
6};
7const fn = obj.update;
8fn.call(obj);
9console.log(obj.x);
--- orig.js 2026-10-22 14:24:59.385254814 -0400 +++ fixed.js 2026-10-22 14:25:03.138421244 -0400 @@ -1,7 +1,7 @@ const obj = { x: 0, update: function() { - this.x = this.x + 1; + this.x += 1; } }; const fn = obj.update;
1
r2-p007 · LHS Side Effect Double Eval
1let i = 0;
2const arr = [0, 0];
3arr[i++] = arr[i++] + 1;
4console.log(arr[0], arr[1], i); // i incremented twice vs once
1 0 2
r2-p008 · Order of Evaluation Merged Expressions
1let a = { val: 0 };
2let b = { val: 0 };
3a.val = (b.val = 1) + a.val;
4console.log(a.val, b.val); // depends on order: original: a.val used after b.val assigned
1 1