From 800bd258e4484de24323809ebbf13fc72fcbabac Mon Sep 17 00:00:00 2001 From: Nitin Kumar Date: Fri, 11 Mar 2022 18:29:08 +0530 Subject: [PATCH] feat: add `destructuredArrayIgnorePattern` option in `no-unused-vars` (#15649) * feat: add `destructuredArrayIgnorePattern` option in `no-unused-vars` Fixes #15611 * docs: add `destructuredArrayIgnorePattern` option in `no-unused-vars` * fix: remove false positives and false negatives * docs: update * test: add more cases * docs: update * fix: remove false positives * docs: update Co-authored-by: Milos Djermanovic * feat: improve error message * feat: cover more cases * docs: add more example * refactor: code * fix: check for all references * docs: add more examples * chore: apply suggestions from code review Co-authored-by: Milos Djermanovic Co-authored-by: Milos Djermanovic --- docs/rules/no-unused-vars.md | 37 ++++++ lib/rules/no-unused-vars.js | 29 ++++- tests/lib/rules/no-unused-vars.js | 200 ++++++++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 1 deletion(-) diff --git a/docs/rules/no-unused-vars.md b/docs/rules/no-unused-vars.md index 09761271bdf..ddf74823e7a 100644 --- a/docs/rules/no-unused-vars.md +++ b/docs/rules/no-unused-vars.md @@ -238,6 +238,43 @@ function foo(x, _y) { foo(); ``` +### destructuredArrayIgnorePattern + +The `destructuredArrayIgnorePattern` option specifies exceptions not to check for usage: elements of array destructuring patterns whose names match a regexp pattern. For example, variables whose names begin with an underscore. + +Examples of **correct** code for the `{ "destructuredArrayIgnorePattern": "^_" }` option: + +```js +/*eslint no-unused-vars: ["error", { "destructuredArrayIgnorePattern": "^_" }]*/ + +const [a, _b, c] = ["a", "b", "c"]; +console.log(a+c); + +const { x: [_a, foo] } = bar; +console.log(foo); + +function baz([_c, x]) { + x; +} +baz(); + +function test({p: [_q, r]}) { + r; +} +test(); + +let _m, n; +foo.forEach(item => { + [_m, n] = item; + console.log(n); +}); + +let _o, p; +_o = 1; +[_o, p] = foo; +p; +``` + ### caughtErrors The `caughtErrors` option is used for `catch` block arguments validation. diff --git a/lib/rules/no-unused-vars.js b/lib/rules/no-unused-vars.js index eaf7a8b912e..54e33705f7f 100644 --- a/lib/rules/no-unused-vars.js +++ b/lib/rules/no-unused-vars.js @@ -67,6 +67,9 @@ module.exports = { }, caughtErrorsIgnorePattern: { type: "string" + }, + destructuredArrayIgnorePattern: { + type: "string" } }, additionalProperties: false @@ -114,6 +117,10 @@ module.exports = { if (firstOption.caughtErrorsIgnorePattern) { config.caughtErrorsIgnorePattern = new RegExp(firstOption.caughtErrorsIgnorePattern, "u"); } + + if (firstOption.destructuredArrayIgnorePattern) { + config.destructuredArrayIgnorePattern = new RegExp(firstOption.destructuredArrayIgnorePattern, "u"); + } } } @@ -155,7 +162,14 @@ module.exports = { * @returns {UnusedVarMessageData} The message data to be used with this unused variable. */ function getAssignedMessageData(unusedVar) { - const additional = config.varsIgnorePattern ? `. Allowed unused vars must match ${config.varsIgnorePattern.toString()}` : ""; + const def = unusedVar.defs[0]; + let additional = ""; + + if (config.destructuredArrayIgnorePattern && def && def.name.parent.type === "ArrayPattern") { + additional = `. Allowed unused elements of array destructuring patterns must match ${config.destructuredArrayIgnorePattern.toString()}`; + } else if (config.varsIgnorePattern) { + additional = `. Allowed unused vars must match ${config.varsIgnorePattern.toString()}`; + } return { varName: unusedVar.name, @@ -584,6 +598,19 @@ module.exports = { if (def) { const type = def.type; + const refUsedInArrayPatterns = variable.references.some(ref => ref.identifier.parent.type === "ArrayPattern"); + + // skip elements of array destructuring patterns + if ( + ( + def.name.parent.type === "ArrayPattern" || + refUsedInArrayPatterns + ) && + config.destructuredArrayIgnorePattern && + config.destructuredArrayIgnorePattern.test(def.name.name) + ) { + continue; + } // skip catch variables if (type === "CatchClause") { diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js index 7c5c87102a9..e7276f6d417 100644 --- a/tests/lib/rules/no-unused-vars.js +++ b/tests/lib/rules/no-unused-vars.js @@ -162,6 +162,81 @@ ruleTester.run("no-unused-vars", rule, { { code: "function foo(_a) { } foo();", options: [{ args: "all", argsIgnorePattern: "^_" }] }, { code: "function foo(a, _b) { return a; } foo();", options: [{ args: "after-used", argsIgnorePattern: "^_" }] }, { code: "var [ firstItemIgnored, secondItem ] = items;\nconsole.log(secondItem);", options: [{ vars: "all", varsIgnorePattern: "[iI]gnored" }], parserOptions: { ecmaVersion: 6 } }, + { + code: "const [ a, _b, c ] = items;\nconsole.log(a+c);", + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "const [ [a, _b, c] ] = items;\nconsole.log(a+c);", + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "const { x: [_a, foo] } = bar;\nconsole.log(foo);", + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function baz([_b, foo]) { foo; };\nbaz()", + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function baz({x: [_b, foo]}) {foo};\nbaz()", + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: "function baz([{x: [_b, foo]}]) {foo};\nbaz()", + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + let _a, b; + foo.forEach(item => { + [_a, b] = item; + doSomething(b); + }); + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 6 } + }, + { + code: ` + // doesn't report _x + let _x, y; + _x = 1; + [_x, y] = foo; + y; + + // doesn't report _a + let _a, b; + [_a, b] = foo; + _a = 1; + b; + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 2018 } + }, + { + code: ` + // doesn't report _x + let _x, y; + _x = 1; + [_x, y] = foo; + y; + + // doesn't report _a + let _a, b; + _a = 1; + ({_a, ...b } = foo); + b; + `, + options: [{ destructuredArrayIgnorePattern: "^_", ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 } + }, // for-in loops (see #2342) "(function(obj) { var name; for ( name in obj ) return; })({});", @@ -463,6 +538,131 @@ ruleTester.run("no-unused-vars", rule, { }] }, + // https://github.com/eslint/eslint/issues/15611 + { + code: ` + const array = ['a', 'b', 'c']; + const [a, _b, c] = array; + const newArray = [a, c]; + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + + // should report only `newArray` + { ...assignedError("newArray"), line: 4, column: 19 } + ] + }, + { + code: ` + const array = ['a', 'b', 'c', 'd', 'e']; + const [a, _b, c] = array; + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError("a", ". Allowed unused elements of array destructuring patterns must match /^_/u"), + line: 3, + column: 20 + }, + { + ...assignedError("c", ". Allowed unused elements of array destructuring patterns must match /^_/u"), + line: 3, + column: 27 + } + ] + }, + { + code: ` + const array = ['a', 'b', 'c']; + const [a, _b, c] = array; + const fooArray = ['foo']; + const barArray = ['bar']; + const ignoreArray = ['ignore']; + `, + options: [{ destructuredArrayIgnorePattern: "^_", varsIgnorePattern: "ignore" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError("a", ". Allowed unused elements of array destructuring patterns must match /^_/u"), + line: 3, + column: 20 + }, + { + ...assignedError("c", ". Allowed unused elements of array destructuring patterns must match /^_/u"), + line: 3, + column: 27 + }, + { + ...assignedError("fooArray", ". Allowed unused vars must match /ignore/u"), + line: 4, + column: 19 + }, + { + ...assignedError("barArray", ". Allowed unused vars must match /ignore/u"), + line: 5, + column: 19 + } + ] + }, + { + code: ` + const array = [obj]; + const [{_a, foo}] = array; + console.log(foo); + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError("_a"), + line: 3, + column: 21 + } + ] + }, + { + code: ` + function foo([{_a, bar}]) { + bar; + } + foo(); + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError("_a"), + line: 2, + column: 28 + } + ] + }, + { + code: ` + let _a, b; + + foo.forEach(item => { + [a, b] = item; + }); + `, + options: [{ destructuredArrayIgnorePattern: "^_" }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError("_a"), + line: 2, + column: 17 + }, + { + ...assignedError("b"), + line: 2, + column: 21 + } + ] + }, + // for-in loops (see #2342) { code: "(function(obj) { var name; for ( name in obj ) { i(); return; } })({});",