Skip to content

Commit

Permalink
feat: add options to check destructuring in no-underscore-dangle (#16006
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
Kaltoft committed Dec 21, 2022
1 parent b68440f commit b401cde
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 0 deletions.
47 changes: 47 additions & 0 deletions docs/src/rules/no-underscore-dangle.md
Expand Up @@ -28,6 +28,7 @@ Examples of **incorrect** code for this rule:
var foo_;
var __proto__ = {};
foo._bar();
const [_foo, ..._bar] = list;
```

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

### allow
Expand Down Expand Up @@ -182,6 +185,50 @@ class Foo {

:::

### allowInArrayDestructuring

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

::: incorrect

```js
/*eslint no-underscore-dangle: ["error", { "allowInArrayDestructuring": false }]*/

const [_foo, _bar] = list;
const [foo_, ..._bar] = list;
const [foo, [bar, _baz]] = list;
```

:::

### allowInObjectDestructuring

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

::: incorrect

```js
/*eslint no-underscore-dangle: ["error", { "allowInObjectDestructuring": false }]*/

const { foo, bar: _bar } = collection;
const { foo, bar, _baz } = collection;
```

:::

Examples of **correct** code for this rule with the `{ "allowInObjectDestructuring": false }` option:

::: correct

```js
/*eslint no-underscore-dangle: ["error", { "allowInObjectDestructuring": false }]*/

const { foo, bar, _baz: { a, b } } = collection;
const { foo, bar, _baz: baz } = collection;
```

:::

### allowFunctionParams

Examples of **incorrect** code for this rule with the `{ "allowFunctionParams": false }` option:
Expand Down
77 changes: 77 additions & 0 deletions lib/rules/no-underscore-dangle.js
Expand Up @@ -53,6 +53,14 @@ module.exports = {
enforceInClassFields: {
type: "boolean",
default: false
},
allowInArrayDestructuring: {
type: "boolean",
default: true
},
allowInObjectDestructuring: {
type: "boolean",
default: true
}
},
additionalProperties: false
Expand All @@ -74,6 +82,8 @@ module.exports = {
const enforceInMethodNames = typeof options.enforceInMethodNames !== "undefined" ? options.enforceInMethodNames : false;
const enforceInClassFields = typeof options.enforceInClassFields !== "undefined" ? options.enforceInClassFields : false;
const allowFunctionParams = typeof options.allowFunctionParams !== "undefined" ? options.allowFunctionParams : true;
const allowInArrayDestructuring = typeof options.allowInArrayDestructuring !== "undefined" ? options.allowInArrayDestructuring : true;
const allowInObjectDestructuring = typeof options.allowInObjectDestructuring !== "undefined" ? options.allowInObjectDestructuring : true;

//-------------------------------------------------------------------------
// Helpers
Expand Down Expand Up @@ -195,13 +205,80 @@ module.exports = {
checkForDanglingUnderscoreInFunctionParameters(node);
}

/**
* Check if node has dangling underscore or if node is type of ArrayPattern check its elements recursively
* @param {ASTNode} node node to evaluate
* @param {string} parentNodeType the ASTNode['type'] of the node parent
* @returns {void}
* @private
*/
function deepCheckDestructured(node, parentNodeType) {
let identifier;

if (!node || !node.type) {
return;
}

switch (node.type) {
case "ArrayPattern":
node.elements.forEach(element => deepCheckDestructured(element, "ArrayPattern"));
break;
case "ObjectPattern":
node.properties.forEach(property => deepCheckDestructured(property, "ObjectPattern"));
break;
case "RestElement":
deepCheckDestructured(node.argument, parentNodeType);
break;
case "Property":
deepCheckDestructured(node.value, "ObjectPattern");
break;
case "Identifier":
identifier = node.name;
break;
default:
break;
}

const isFromDestructuredObject = parentNodeType === "ObjectPattern" && !allowInObjectDestructuring;
const isFromDestructuredArray = parentNodeType === "ArrayPattern" && !allowInArrayDestructuring;
const hasDisallowedDestructuring = isFromDestructuredObject || isFromDestructuredArray;

if (
identifier &&
hasDisallowedDestructuring &&
hasDanglingUnderscore(identifier) &&
!isSpecialCaseIdentifierInVariableExpression(identifier) &&
!isAllowed(identifier)
) {
context.report({
node,
messageId: "unexpectedUnderscore",
data: {
identifier
}
});
}
}

/**
* Check if variable expression has a dangling underscore
* @param {ASTNode} node node to evaluate
* @returns {void}
* @private
*/
function checkForDanglingUnderscoreInVariableExpression(node) {
if (node.id.type === "ArrayPattern") {
node.id.elements.forEach(element => {
deepCheckDestructured(element, node.id.type);
});
}

if (node.id.type === "ObjectPattern") {
node.id.properties.forEach(element => {
deepCheckDestructured(element, node.id.type);
});
}

const identifier = node.id.name;

if (typeof identifier !== "undefined" && hasDanglingUnderscore(identifier) &&
Expand Down
61 changes: 61 additions & 0 deletions tests/lib/rules/no-underscore-dangle.js
Expand Up @@ -70,6 +70,11 @@ ruleTester.run("no-underscore-dangle", rule, {
{ code: "function foo( { _bar }) {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 } },
{ code: "function foo( { _bar = 0 } = {}) {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 } },
{ code: "function foo(...[_bar]) {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 2016 } },
{ code: "const [foo, ...rest] = [1, 2, 3]", options: [{ allowInArrayDestructuring: false }], parserOptions: { ecmaVersion: 2022 } },
{ code: "const [foo, _bar] = [1, 2, 3]", options: [{ allowInArrayDestructuring: false, allow: ["_bar"] }], parserOptions: { ecmaVersion: 2022 } },
{ code: "const { foo, bar: _bar } = { foo: 1, bar: 2 }", options: [{ allowInObjectDestructuring: false, allow: ["_bar"] }], parserOptions: { ecmaVersion: 2022 } },
{ code: "const { foo, _bar } = { foo: 1, _bar: 2 }", options: [{ allowInObjectDestructuring: false, allow: ["_bar"] }], parserOptions: { ecmaVersion: 2022 } },
{ code: "const { foo, _bar: bar } = { foo: 1, _bar: 2 }", options: [{ allowInObjectDestructuring: false }], parserOptions: { ecmaVersion: 2022 } },
{ code: "class foo { _field; }", parserOptions: { ecmaVersion: 2022 } },
{ code: "class foo { _field; }", options: [{ enforceInClassFields: false }], parserOptions: { ecmaVersion: 2022 } },
{ code: "class foo { #_field; }", parserOptions: { ecmaVersion: 2022 } },
Expand Down Expand Up @@ -103,6 +108,62 @@ ruleTester.run("no-underscore-dangle", rule, {
{ code: "const foo = { onClick(..._bar) { } }", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" }, type: "RestElement" }] },
{ code: "const foo = (..._bar) => {}", options: [{ allowFunctionParams: false }], parserOptions: { ecmaVersion: 6 }, errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" }, type: "RestElement" }] },
{
code: "const [foo, _bar] = [1, 2]",
options: [{ allowInArrayDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" } }]
}, {
code: "const [foo, ..._rest] = [1, 2, 3]",
options: [{ allowInArrayDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_rest" } }]
}, {
code: "const [foo, [bar_, baz]] = [1, [2, 3]]",
options: [{ allowInArrayDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "bar_" } }]
}, {
code: "const { _foo, bar } = { _foo: 1, bar: 2 }",
options: [{ allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_foo" } }]
}, {
code: "const { foo: _foo, bar } = { foo: 1, bar: 2 }",
options: [{ allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_foo" } }]
}, {
code: "const { foo, ..._rest} = { foo: 1, bar: 2, baz: 3 }",
options: [{ allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_rest" } }]
}, {
code: "const { foo: [_bar, { a: _a, b } ] } = { foo: [1, { a: 'a', b: 'b' }] }",
options: [{ allowInArrayDestructuring: false, allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [
{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" } },
{ messageId: "unexpectedUnderscore", data: { identifier: "_a" } }
]
}, {
code: "const { foo: [_bar, { a: _a, b } ] } = { foo: [1, { a: 'a', b: 'b' }] }",
options: [{ allowInArrayDestructuring: true, allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_a" } }]
}, {
code: "const [{ foo: [_bar, _, { bar: _baz }] }] = [{ foo: [1, 2, { bar: 'a' }] }]",
options: [{ allowInArrayDestructuring: false, allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [
{ messageId: "unexpectedUnderscore", data: { identifier: "_bar" } },
{ messageId: "unexpectedUnderscore", data: { identifier: "_baz" } }
]
}, {
code: "const { foo, bar: { baz, _qux } } = { foo: 1, bar: { baz: 3, _qux: 4 } }",
options: [{ allowInObjectDestructuring: false }],
parserOptions: { ecmaVersion: 2022 },
errors: [{ messageId: "unexpectedUnderscore", data: { identifier: "_qux" } }]
}, {
code: "class foo { #_bar() {} }",
options: [{ enforceInMethodNames: true }],
parserOptions: { ecmaVersion: 2022 },
Expand Down

0 comments on commit b401cde

Please sign in to comment.