eslint prefer-destructuring
labelled safe
Analysis Nemotron 3 Super
Flags variable declarators where the value is a simple member expression (property/index access) and suggests using destructuring instead.
Fix: Rewrites `const v = obj.prop;` (or similar with let/var) to `const { prop: v } = obj;` when the variable name matches the property name and the property access is a simple identifier or string literal; similarly rewrites array index accesses `const v = arr[i];` to `[v] = arr;` only when i is a numeric literal and v matches the expected destructuring variable name (which it never does per rule limitations, so array fixes are not applied).
9 hazards
- Rewriting `const x = obj.a;` to `const { a: x } = obj;` throws if `obj` is null or undefined, whereas the original would throw only when accessing `.a` on null/undefined—same timing but different error path; however, if `obj` is falsy but not null/undefined (e.g., 0), the original accesses property `a` on the coerced object (which may exist via wrapper) while destructuring attempts to coerce `obj` to an object and then get `a`, which may differ for exotic objects.
- If `obj` is a function with a getter for `a` that has side effects, the original evaluates the getter once; the rewrite also evaluates it once, so no difference—but if the getter is defined on the prototype and `obj` is undefined, the original throws immediately while destructuring first attempts coercion (still throws), so observable behavior is identical.
- The fix removes any comments placed between the variable and the equals token or after the initializer on the same line, as the entire initializer is replaced.
- If the variable is used later in a way that depends on the temporal dead zone (e.g., referenced before declaration in a scope where hoisting matters), moving the destructuring form does not change hoisting behavior because both forms are variable declarations with identical hoisting rules.
- In strict mode, if `obj` is the global object and the property is a configurable global variable, destructuring creates a binding that is not a global variable, whereas member access refers to the global property—this could break code that later deletes the property or relies on it being configurable.
- If the member access is part of a larger expression (not flagged), the fix does not apply, so no hazard there; but if mistakenly applied due to a bug, it would break parsing by creating invalid left-hand side in assignment contexts.
- For array indexing like `const x = arr[0];`, the rule does not autofix even if `x` matches because the fix only applies to object destructuring per documentation; thus no rewrite occurs, eliminating hazards for array cases.
- If the object has a getter that throws when accessed, both forms throw on access—no behavioral difference.
- If the property name is a string literal that is not an identifier (e.g., `obj['foo-bar']`), the fix would produce `{ 'foo-bar': x }` which is invalid syntax, so the rule does not apply in such cases (property must be a simple identifier for the fix to be considered).
Programs
| id | result | title | hazard |
|---|---|---|---|
| r1-p001 | silent prediction off | Temporal Dead Zone Interaction | Variable accessed before declaration within block scope triggers TDZ error regardless of declaration form. |
| r1-p002 | silent prediction off | Order of Evaluation Side Effects | Destructuring evaluates the source expression once, same as member access—no duplication, but interaction with comma operator reveals evaluation timing. |
| r1-p003 | invalid | Arguments Object and Parameters | Function parameters shadow outer bindings; destructuring inside function body does not affect arguments or length. |
| r1-p004 | fired, no fix prediction off | Sparse Arrays and Holes | Accessing arr[0] on sparse array returns undefined if hole; destructuring [0] = arr behaves identically for numeric literals. |
| r1-p005 | silent prediction off | Getter on Prototype with Undefined Receiver | Both forms throw TypeError when obj is undefined; destructuring coerces first, member access throws immediately—same outcome. |
| r1-p006 | silent prediction off | Falsy but Not Null/Undefined Object Coercion | For obj=0, member access coerces to Number object (no 'a' property → undefined); destructuring coerces 0 to Object → same result. |
| r1-p007 | silent prediction off | Class Fields and Static Blocks | Class field initialization order is unaffected by destructuring rewrite; instance property access behaves same. |
| r1-p008 | silent prediction off | Exceptions in Removed Code | If obj.a throws, both original and rewritten form propagate the exception identically. |
| r2-p001 | silent prediction off | __proto__ key interaction | __proto__ keys, prototype access, Object.assign vs spread |
| r2-p002 | fix ok | Comment removal during fix | The fix removes any comments placed between the variable and the equals token or after the initializer on the same line |
| r2-p003 | silent | Invalid LHS in assignment context | If mistakenly applied due to a bug, it would break parsing by creating invalid left-hand side in assignment contexts |
| r2-p004 | invalid | Property access coercion differences | numeric/string coercion: + vs template literal, objects with toString/valueOf, Symbol, BigInt |
| r2-p005 | silent | Non-identifier property name | If the property name is a string literal that is not an identifier (e.g., obj['foo-bar']), the fix would produce { 'foo-bar': x } which is invalid syntax |
| r2-p006 | fired, no fix prediction off | Array indexing no fix | For array indexing like const x = arr[0], the rule does not autofix even if x matches because the fix only applies to object destructuring |
| r2-p007 | silent prediction off | TDZ after hoisting/reordering | temporal dead zone after code is hoisted or reordered |
| r2-p008 | fix ok | Strict vs sloppy mode | strict vs sloppy mode semantics |
Sources
r1-p001 · Temporal Dead Zone Interaction
1function f() {
2 console.log(x); // ReferenceError due to TDZ
3 const x = obj.a;
4 return x;
5}
6const obj = { a: 42 };
7try { f(); } catch (e) { console.log('TDZ:', e.name); }
TDZ: ReferenceError
r1-p002 · Order of Evaluation Side Effects
1let obj = { a: (console.log('eval'), 5) };
2let result = [];
3result.push(obj.a); // logs 'eval', pushes 5
4obj = { a: (console.log('eval'), 10) };
5const x = obj.a; // logs 'eval', x=10
6result.push(x);
7console.log('result:', result.join(',')); // eval,eval -> 5,10
eval eval result: 5,10
r1-p003 · Arguments Object and Parameters (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-26-0)
1function g(a) {
2 console.log('args:', arguments.length, arguments[0]);
3 const a = { prop: 99 }.prop; // rule triggers: reassigns param
4 console.log('after:', arguments.length, arguments[0]);
5 return a;
6}
7console.log(g(1)); // args: 1 1 -> after: 1 1 -> 99
r1-p004 · Sparse Arrays and Holes
1const arr = [];
2arr[2] = 3;
3console.log('arr[0]:', arr[0]); // undefined
4let x = arr[0]; // rule does NOT trigger (index is not numeric literal)
5console.log('x:', x); // undefined
6console.log('arr:', JSON.stringify(arr)); // [,,3]
arr[0]: undefined x: undefined arr: [null,null,3]
r1-p005 · Getter on Prototype with Undefined Receiver
1const proto = { get a() { return 1; } };
2let obj = { __proto__: proto, a: 2 };
3console.log(obj.a); // 2
4obj = undefined;
5try { obj.a; } catch (e) { console.log('member access:', e.name); }
6try { const x = obj.a; } catch (e) { console.log('destructuring would throw:', e.name); }
2 member access: TypeError destructuring would throw: TypeError
r1-p006 · Falsy but Not Null/Undefined Object Coercion
1let obj = 0;
2console.log(obj.a); // undefined (Number.prototype.a doesn't exist)
3try {
4 const x = obj.a; // rule triggers
5 console.log('x:', x); // undefined
6} catch (e) { console.log('unexpected:', e); }
7obj = {};
8obj.a = 5;
9console.log('after reassignment:', obj.a); // 5
undefined x: undefined after reassignment: 5
r1-p007 · Class Fields and Static Blocks
1class C {
2 static s = { prop: 'static' };
3 inst = { prop: 'instance' };
4 getProp() {
5 const x = this.inst.prop; // rule triggers
6 return x;
7 }
8}
9const c = new C();
10console.log(c.getProp()); // instance
11console.log(C.s.prop); // static
instance static
r1-p008 · Exceptions in Removed Code
1const obj = {
2 get a() {
3 if (!obj.seen) {
4 obj.seen = true;
5 throw new Error('lazy error');
6 }
7 return 42;
8 }
9};
10let count = 0;
11try {
12 const x = obj.a; // rule triggers
13} catch (e) {
14 console.log('first:', e.message);
15 count++;
16}
17try {
18 const x = obj.a; // second access
19} catch (e) {
20 console.log('second:', e.message);
21 count++;
22}
23console.log('count:', count); // 2
first: lazy error count: 1
r2-p001 · __proto__ key interaction
1// Hazard 1: __proto__ key
2function test() {
3 const obj = { __proto__: null, a: 1 };
4 // Rule should fire on next line: const a = obj.__proto__;
5 const a = obj.__proto__; /* keep */
6 console.log(a === null ? 'null-proto' : 'has-proto');
7}
8test();
has-proto
r2-p002 · Comment removal during fix
1// Hazard 2: Comment preservation
2let obj = { x: 42 };
3// Before fix, this line has a trailing comment
4let x = obj.x; // value is 42
5console.log('x=', x, 'comment survived?');
--- orig.js 2026-10-22 14:26:31.188166630 -0400 +++ fixed.js 2026-10-22 14:26:33.847110954 -0400 @@ -1,5 +1,5 @@ // Hazard 2: Comment preservation let obj = { x: 42 }; // Before fix, this line has a trailing comment -let x = obj.x; // value is 42 +let {x} = obj; // value is 42 console.log('x=', x, 'comment survived?');
x= 42 comment survived?
r2-p003 · Invalid LHS in assignment context
1// Hazard 3: Near-miss - not flagged, so fix should not apply
2let obj = { a: 1 };
3if (true) (obj.a = 2); // AssignmentExpression, not VariableDeclarator
4console.log(obj.a);
2
r2-p004 · Property access coercion differences (generated program does not parse: /home/sun/dev/diaghunt/runs/2026-10-22T18-26-0)
1// Hazard 4: Coercion in property access
2const o = { '0': 'zero', '1': 'one', 2: 'two' };
3let idx = 0;
4// Rule fires on next line: const v = o[idx]; (idx is identifier, not literal)
5// BUT we need a literal index for rule to fire - adjust:
6const v = o[0]; // literal 0, v matches? no -> rule does NOT fire on v=o[0]
7// Instead trigger on string literal match:
8const k = '1';
9const val = o[k]; // k is identifier -> no fire
10// Final: use obj['0'] style with literal string that matches var name? impossible.
11// Redesign: use obj.a where a is var name
12const a = 5;
13const obj = { a: 99 };
14const a_val = obj.a; // YES: var name 'a_val' != prop 'a' -> NO FIRE
15// Need var name == prop name:
16const prop = 'a';
17const obj2 = { a: 88 };
18// const a = obj2.a; // var name 'a' == prop 'a' -> SHOULD FIRE
19const a = obj2.a;
20console.log('val:', a);
r2-p005 · Non-identifier property name
1// Hazard 5: Non-identifier property name
2const obj = { 'foo-bar': 123 };
3// Rule does NOT fire because prop name is not simple identifier
4const x = obj['foo-bar'];
5console.log(x);
123
r2-p006 · Array indexing no fix
1// Hazard 6: Array indexing - no autofix
2const arr = [10, 20];
3// Rule does not autofix array indexing per documentation
4const x = arr[0];
5console.log(x);
10
r2-p007 · TDZ after hoisting/reordering
1// Hazard 7: TDZ in block
2(function() {
3 if (true) {
4 console.log(typeof x); // TDZ if let/const
5 // Rule fires on next line:
6 let x = {}.prop; // assume {}.prop is undefined
7 }
8 // hoisted? no - block scoped
9})();
r2-p008 · Strict vs sloppy mode
1"use strict"; // Hazard 8: strict mode
2const obj = {};
3try {
4 // Rule fires: const x = obj.x;
5 let x = obj.x; // in strict, ok (undefined)
6 console.log('x:', x);
7} catch (e) {
8 console.log(e.name);
9}
--- orig.js 2026-10-22 14:26:33.717113661 -0400 +++ fixed.js 2026-10-22 14:26:35.784070915 -0400 @@ -2,7 +2,7 @@ const obj = {}; try { // Rule fires: const x = obj.x; - let x = obj.x; // in strict, ok (undefined) + let {x} = obj; // in strict, ok (undefined) console.log('x:', x); } catch (e) { console.log(e.name);
x: undefined