Skip to content

Commit b401cde

Browse files
authored
feat: add options to check destructuring in no-underscore-dangle (#16006)
* feat: disallow dangling `_` in destructured variable names * doc: describe dangling `_` examples * add: test dangling `_` in destructured variables * refactor: rename to `allowInArrayDestructuring` * fix: change default value * docs: update `allowInArrayDestructuring` examples * feat: disallow dangling `_` in variable names from object destructuring * style: formatting * feat: tests for nested destructured objects and arrays * fix: handle varying nested destructuring * style: identation * fix: remove optional chaining * fix: remove unnecessary check
1 parent b68440f commit b401cde

3 files changed

Lines changed: 185 additions & 0 deletions

File tree

docs/src/rules/no-underscore-dangle.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Examples of **incorrect** code for this rule:
2828
var foo_;
2929
var __proto__ = {};
3030
foo._bar();
31+
const [_foo, ..._bar] = list;
3132
```
3233

3334
:::
@@ -60,6 +61,8 @@ This rule has an object option:
6061
* `"allowAfterThisConstructor": false` (default) disallows dangling underscores in members of the `this.constructor` object
6162
* `"enforceInMethodNames": false` (default) allows dangling underscores in method names
6263
* `"enforceInClassFields": false` (default) allows dangling underscores in es2022 class fields names
64+
* `"allowInArrayDestructuring": true` (default) allows dangling underscores in variable names assigned by array destructuring
65+
* `"allowInObjectDestructuring": true` (default) allows dangling underscores in variable names assigned by object destructuring
6366
* `"allowFunctionParams": true` (default) allows dangling underscores in function parameter names
6467

6568
### allow
@@ -182,6 +185,50 @@ class Foo {
182185

183186
:::
184187

188+
### allowInArrayDestructuring
189+
190+
Examples of **incorrect** code for this rule with the `{ "allowInArrayDestructuring": false }` option:
191+
192+
::: incorrect
193+
194+
```js
195+
/*eslint no-underscore-dangle: ["error", { "allowInArrayDestructuring": false }]*/
196+
197+
const [_foo, _bar] = list;
198+
const [foo_, ..._bar] = list;
199+
const [foo, [bar, _baz]] = list;
200+
```
201+
202+
:::
203+
204+
### allowInObjectDestructuring
205+
206+
Examples of **incorrect** code for this rule with the `{ "allowInObjectDestructuring": false }` option:
207+
208+
::: incorrect
209+
210+
```js
211+
/*eslint no-underscore-dangle: ["error", { "allowInObjectDestructuring": false }]*/
212+
213+
const { foo, bar: _bar } = collection;
214+
const { foo, bar, _baz } = collection;
215+
```
216+
217+
:::
218+
219+
Examples of **correct** code for this rule with the `{ "allowInObjectDestructuring": false }` option:
220+
221+
::: correct
222+
223+
```js
224+
/*eslint no-underscore-dangle: ["error", { "allowInObjectDestructuring": false }]*/
225+
226+
const { foo, bar, _baz: { a, b } } = collection;
227+
const { foo, bar, _baz: baz } = collection;
228+
```
229+
230+
:::
231+
185232
### allowFunctionParams
186233

187234
Examples of **incorrect** code for this rule with the `{ "allowFunctionParams": false }` option:

lib/rules/no-underscore-dangle.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ module.exports = {
5353
enforceInClassFields: {
5454
type: "boolean",
5555
default: false
56+
},
57+
allowInArrayDestructuring: {
58+
type: "boolean",
59+
default: true
60+
},
61+
allowInObjectDestructuring: {
62+
type: "boolean",
63+
default: true
5664
}
5765
},
5866
additionalProperties: false
@@ -74,6 +82,8 @@ module.exports = {
7482
const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false;
7583
const enforceInClassFields = typeof options.enforceInClassFields !== "undefined" ? options.enforceInClassFields : false;
7684
const allowFunctionParams = typeof options.allowFunctionParams !== "undefined" ? options.allowFunctionParams : true;
85+
const allowInArrayDestructuring = typeof options.allowInArrayDestructuring !== "undefined" ? options.allowInArrayDestructuring : true;
86+
const allowInObjectDestructuring = typeof options.allowInObjectDestructuring !== "undefined" ? options.allowInObjectDestructuring : true;
7787

7888
//-------------------------------------------------------------------------
7989
// Helpers
@@ -195,13 +205,80 @@ module.exports = {
195205
checkForDanglingUnderscoreInFunctionParameters(node);
196206
}
197207

208+
/**
209+
* Check if node has dangling underscore or if node is type of ArrayPattern check its elements recursively
210+
* @param {ASTNode} node node to evaluate
211+
* @param {string} parentNodeType the ASTNode['type'] of the node parent
212+
* @returns {void}
213+
* @private
214+
*/
215+
function deepCheckDestructured(node, parentNodeType) {
216+
let identifier;
217+
218+
if (!node || !node.type) {
219+
return;
220+
}
221+
222+
switch (node.type) {
223+
case "ArrayPattern":
224+
node.elements.forEach(element => deepCheckDestructured(element, "ArrayPattern"));
225+
break;
226+
case "ObjectPattern":
227+
node.properties.forEach(property => deepCheckDestructured(property, "ObjectPattern"));
228+
break;
229+
case "RestElement":
230+
deepCheckDestructured(node.argument, parentNodeType);
231+
break;
232+
case "Property":
233+
deepCheckDestructured(node.value, "ObjectPattern");
234+
break;
235+
case "Identifier":
236+
identifier = node.name;
237+
break;
238+
default:
239+
break;
240+
}
241+
242+
const isFromDestructuredObject = parentNodeType === "ObjectPattern" && !allowInObjectDestructuring;
243+
const isFromDestructuredArray = parentNodeType === "ArrayPattern" && !allowInArrayDestructuring;
244+
const hasDisallowedDestructuring = isFromDestructuredObject || isFromDestructuredArray;
245+
246+
if (
247+
identifier &&
248+
hasDisallowedDestructuring &&
249+
hasDanglingUnderscore(identifier) &&
250+
!isSpecialCaseIdentifierInVariableExpression(identifier) &&
251+
!isAllowed(identifier)
252+
) {
253+
context.report({
254+
node,
255+
messageId: "unexpectedUnderscore",
256+
data: {
257+
identifier
258+
}
259+
});
260+
}
261+
}
262+
198263
/**
199264
* Check if variable expression has a dangling underscore
200265
* @param {ASTNode} node node to evaluate
201266
* @returns {void}
202267
* @private
203268
*/
204269
function checkForDanglingUnderscoreInVariableExpression(node) {
270+
if (node.id.type === "ArrayPattern") {
271+
node.id.elements.forEach(element => {
272+
deepCheckDestructured(element, node.id.type);
273+
});
274+
}
275+
276+
if (node.id.type === "ObjectPattern") {
277+
node.id.properties.forEach(element => {
278+
deepCheckDestructured(element, node.id.type);
279+
});
280+
}
281+
205282
const identifier = node.id.name;
206283

207284
if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) &&

tests/lib/rules/no-underscore-dangle.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ ruleTester.run("no-underscore-dangle", rule, {
7070
{ code: "function foo( { _bar }) {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 } },
7171
{ code: "function foo( { _bar = 0 } = {}) {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 } },
7272
{ code: "function foo(...[_bar]) {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 2016 } },
73+
{ code: "const [foo, ...rest] = [1, 2, 3]", options: [{ allowInArrayDestructuring: false }], parserOptions: { ecmaVersion: 2022 } },
74+
{ code: "const [foo, _bar] = [1, 2, 3]", options: [{ allowInArrayDestructuring: false, allow: ["_bar"] }], parserOptions: { ecmaVersion: 2022 } },
75+
{ code: "const { foo, bar: _bar } = { foo: 1, bar: 2 }", options: [{ allowInObjectDestructuring: false, allow: ["_bar"] }], parserOptions: { ecmaVersion: 2022 } },
76+
{ code: "const { foo, _bar } = { foo: 1, _bar: 2 }", options: [{ allowInObjectDestructuring: false, allow: ["_bar"] }], parserOptions: { ecmaVersion: 2022 } },
77+
{ code: "const { foo, _bar: bar } = { foo: 1, _bar: 2 }", options: [{ allowInObjectDestructuring: false }], parserOptions: { ecmaVersion: 2022 } },
7378
{ code: "class foo { _field; }", parserOptions: { ecmaVersion: 2022 } },
7479
{ code: "class foo { _field; }", options: [{ enforceInClassFields: false }], parserOptions: { ecmaVersion: 2022 } },
7580
{ code: "class foo { #_field; }", parserOptions: { ecmaVersion: 2022 } },
@@ -103,6 +108,62 @@ ruleTester.run("no-underscore-dangle", rule, {
103108
{ code: "const foo = { onClick(..._bar) { } }", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" }, type: "RestElement" }] },
104109
{ code: "const foo = (..._bar) => {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" }, type: "RestElement" }] },
105110
{
111+
code: "const [foo, _bar] = [1, 2]",
112+
options: [{ allowInArrayDestructuring: false }],
113+
parserOptions: { ecmaVersion: 2022 },
114+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" } }]
115+
}, {
116+
code: "const [foo, ..._rest] = [1, 2, 3]",
117+
options: [{ allowInArrayDestructuring: false }],
118+
parserOptions: { ecmaVersion: 2022 },
119+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_rest" } }]
120+
}, {
121+
code: "const [foo, [bar_, baz]] = [1, [2, 3]]",
122+
options: [{ allowInArrayDestructuring: false }],
123+
parserOptions: { ecmaVersion: 2022 },
124+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "bar_" } }]
125+
}, {
126+
code: "const { _foo, bar } = { _foo: 1, bar: 2 }",
127+
options: [{ allowInObjectDestructuring: false }],
128+
parserOptions: { ecmaVersion: 2022 },
129+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_foo" } }]
130+
}, {
131+
code: "const { foo: _foo, bar } = { foo: 1, bar: 2 }",
132+
options: [{ allowInObjectDestructuring: false }],
133+
parserOptions: { ecmaVersion: 2022 },
134+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_foo" } }]
135+
}, {
136+
code: "const { foo, ..._rest} = { foo: 1, bar: 2, baz: 3 }",
137+
options: [{ allowInObjectDestructuring: false }],
138+
parserOptions: { ecmaVersion: 2022 },
139+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_rest" } }]
140+
}, {
141+
code: "const { foo: [_bar, { a: _a, b } ] } = { foo: [1, { a: 'a', b: 'b' }] }",
142+
options: [{ allowInArrayDestructuring: false, allowInObjectDestructuring: false }],
143+
parserOptions: { ecmaVersion: 2022 },
144+
errors: [
145+
{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" } },
146+
{ messageId: "unexpectedUnderscore", data: { identifier: "_a" } }
147+
]
148+
}, {
149+
code: "const { foo: [_bar, { a: _a, b } ] } = { foo: [1, { a: 'a', b: 'b' }] }",
150+
options: [{ allowInArrayDestructuring: true, allowInObjectDestructuring: false }],
151+
parserOptions: { ecmaVersion: 2022 },
152+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_a" } }]
153+
}, {
154+
code: "const [{ foo: [_bar, _, { bar: _baz }] }] = [{ foo: [1, 2, { bar: 'a' }] }]",
155+
options: [{ allowInArrayDestructuring: false, allowInObjectDestructuring: false }],
156+
parserOptions: { ecmaVersion: 2022 },
157+
errors: [
158+
{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" } },
159+
{ messageId: "unexpectedUnderscore", data: { identifier: "_baz" } }
160+
]
161+
}, {
162+
code: "const { foo, bar: { baz, _qux } } = { foo: 1, bar: { baz: 3, _qux: 4 } }",
163+
options: [{ allowInObjectDestructuring: false }],
164+
parserOptions: { ecmaVersion: 2022 },
165+
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_qux" } }]
166+
}, {
106167
code: "class foo { #_bar() {} }",
107168
options: [{ enforceInMethodNames: true }],
108169
parserOptions: { ecmaVersion: 2022 },

0 commit comments

Comments
 (0)