From b401cde47d44746ff91b8feced3fb3a4e32c0e12 Mon Sep 17 00:00:00 2001 From: Morten Kaltoft Date: Wed, 21 Dec 2022 21:15:05 +0100 Subject: [PATCH] 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 --- docs/src/rules/no-underscore-dangle.md | 47 +++++++++++++++ lib/rules/no-underscore-dangle.js | 77 +++++++++++++++++++++++++ tests/lib/rules/no-underscore-dangle.js | 61 ++++++++++++++++++++ 3 files changed, 185 insertions(+) diff --git a/docs/src/rules/no-underscore-dangle.md b/docs/src/rules/no-underscore-dangle.md index 2d6637bbd29..54114d88021 100644 --- a/docs/src/rules/no-underscore-dangle.md +++ b/docs/src/rules/no-underscore-dangle.md @@ -28,6 +28,7 @@ Examples of **incorrect** code for this rule: var foo_; var __proto__ = {}; foo._bar(); +const [_foo, ..._bar] = list; ``` ::: @@ -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 @@ -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: diff --git a/lib/rules/no-underscore-dangle.js b/lib/rules/no-underscore-dangle.js index eb3e404a66d..844c4fb3a0f 100644 --- a/lib/rules/no-underscore-dangle.js +++ b/lib/rules/no-underscore-dangle.js @@ -53,6 +53,14 @@ module.exports = { enforceInClassFields: { type: "boolean", default: false + }, + allowInArrayDestructuring: { + type: "boolean", + default: true + }, + allowInObjectDestructuring: { + type: "boolean", + default: true } }, additionalProperties: false @@ -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 @@ -195,6 +205,61 @@ 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 @@ -202,6 +267,18 @@ module.exports = { * @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) && diff --git a/tests/lib/rules/no-underscore-dangle.js b/tests/lib/rules/no-underscore-dangle.js index f3e11cc3e76..2ae34f1a88f 100644 --- a/tests/lib/rules/no-underscore-dangle.js +++ b/tests/lib/rules/no-underscore-dangle.js @@ -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 } }, @@ -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 },