From 6f8db8b64821d280fff408c1704a9adde682ed69 Mon Sep 17 00:00:00 2001 From: ALOHACREPES345 <51868219+ALOHACREPES345@users.noreply.github.com> Date: Mon, 28 Mar 2022 06:46:54 +0900 Subject: [PATCH] feat(eslint-plugin): [no-unused-vars] add destructuredArrayIgnorePattern options (#4748) * feat(eslint-plugin): [no-unused-vars] add destructuredArrayIgnorePattern options * feat(eslint-plugin): added typing * Update packages/eslint-plugin/src/rules/no-unused-vars.ts Co-authored-by: Josh Goldberg --- .../eslint-plugin/src/rules/no-unused-vars.ts | 73 ++++- .../no-unused-vars-eslint.test.ts | 275 ++++++++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 1 + 3 files changed, 335 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 139089a7448..4fb54dda747 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -14,6 +14,7 @@ export type Options = [ argsIgnorePattern?: string; caughtErrors?: 'all' | 'none'; caughtErrorsIgnorePattern?: string; + destructuredArrayIgnorePattern?: string; }, ]; @@ -25,6 +26,7 @@ interface TranslatedOptions { argsIgnorePattern?: RegExp; caughtErrors: 'all' | 'none'; caughtErrorsIgnorePattern?: RegExp; + destructuredArrayIgnorePattern?: RegExp; } export default util.createRule({ @@ -66,6 +68,9 @@ export default util.createRule({ caughtErrorsIgnorePattern: { type: 'string', }, + destructuredArrayIgnorePattern: { + type: 'string', + }, }, additionalProperties: false, }, @@ -123,12 +128,33 @@ export default util.createRule({ 'u', ); } + + if (firstOption.destructuredArrayIgnorePattern) { + options.destructuredArrayIgnorePattern = new RegExp( + firstOption.destructuredArrayIgnorePattern, + 'u', + ); + } } } return options; })(); function collectUnusedVariables(): TSESLint.Scope.Variable[] { + /** + * Checks whether a node is a sibling of the rest property or not. + * @param {ASTNode} node a node to check + * @returns {boolean} True if the node is a sibling of the rest property, otherwise false. + */ + function hasRestSibling(node: TSESTree.Node): boolean { + return ( + node.type === AST_NODE_TYPES.Property && + node.parent?.type === AST_NODE_TYPES.ObjectPattern && + node.parent.properties[node.parent.properties.length - 1].type === + AST_NODE_TYPES.RestElement + ); + } + /** * Determines if a variable has a sibling rest property * @param variable eslint-scope variable object. @@ -138,17 +164,14 @@ export default util.createRule({ variable: TSESLint.Scope.Variable, ): boolean { if (options.ignoreRestSiblings) { - return variable.defs.some(def => { - const propertyNode = def.name.parent!; - const patternNode = propertyNode.parent!; - - return ( - propertyNode.type === AST_NODE_TYPES.Property && - patternNode.type === AST_NODE_TYPES.ObjectPattern && - patternNode.properties[patternNode.properties.length - 1].type === - AST_NODE_TYPES.RestElement - ); - }); + const hasRestSiblingDefinition = variable.defs.some(def => + hasRestSibling(def.name.parent!), + ); + const hasRestSiblingReference = variable.references.some(ref => + hasRestSibling(ref.identifier.parent!), + ); + + return hasRestSiblingDefinition || hasRestSiblingReference; } return false; @@ -188,6 +211,20 @@ export default util.createRule({ continue; } + const refUsedInArrayPatterns = variable.references.some( + ref => ref.identifier.parent?.type === AST_NODE_TYPES.ArrayPattern, + ); + + // skip elements of array destructuring patterns + if ( + (def.name.parent?.type === AST_NODE_TYPES.ArrayPattern || + refUsedInArrayPatterns) && + 'name' in def.name && + options.destructuredArrayIgnorePattern?.test(def.name.name) + ) { + continue; + } + // skip catch variables if (def.type === TSESLint.Scope.DefinitionType.CatchClause) { if (options.caughtErrors === 'none') { @@ -361,9 +398,17 @@ export default util.createRule({ function getAssignedMessageData( unusedVar: TSESLint.Scope.Variable, ): Record { - const additional = options.varsIgnorePattern - ? `. Allowed unused vars must match ${options.varsIgnorePattern.toString()}` - : ''; + const def = unusedVar.defs[0]; + let additional = ''; + + if ( + options.destructuredArrayIgnorePattern && + def?.name.parent?.type === AST_NODE_TYPES.ArrayPattern + ) { + additional = `. Allowed unused elements of array destructuring patterns must match ${options.destructuredArrayIgnorePattern.toString()}`; + } else if (options.varsIgnorePattern) { + additional = `. Allowed unused vars must match ${options.varsIgnorePattern.toString()}`; + } return { varName: unusedVar.name, diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts index 9ddde4e9f24..6f26085bfd7 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts @@ -701,6 +701,111 @@ console.log(secondItem); options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], parserOptions: { ecmaVersion: 6 }, }, + { + code: ` +const [a, _b, c] = items; +console.log(a + c); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const [[a, _b, c]] = items; +console.log(a + c); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const { + x: [_a, foo], +} = bar; +console.log(foo); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function baz([_b, foo]) { + foo; +} +baz(); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function baz({ x: [_b, foo] }) { + foo; +} +baz(); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function baz([ + { + x: [_b, foo], + }, +]) { + foo; +} +baz(); + `, + 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) ` @@ -1017,6 +1122,13 @@ console.log(Foo); parserOptions: { ecmaVersion: 2018 }, }, + // https://github.com/eslint/eslint/issues/14163 + { + code: 'let foo, rest;\n({ foo, ...rest } = something);\nconsole.log(rest);', + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2020 }, + }, + // https://github.com/eslint/eslint/issues/10952 ` /*eslint use-every-a:1*/ !function (b, a) { @@ -1504,6 +1616,148 @@ foo(); }, ], }, + // 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: 7 }, + ], + }, + { + 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: 8, + }, + { + ...assignedError( + 'c', + '. Allowed unused elements of array destructuring patterns must match /^_/u', + ), + line: 3, + column: 15, + }, + ], + }, + { + 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: 8, + }, + { + ...assignedError( + 'c', + '. Allowed unused elements of array destructuring patterns must match /^_/u', + ), + line: 3, + column: 15, + }, + { + ...assignedError( + 'fooArray', + '. Allowed unused vars must match /ignore/u', + ), + line: 4, + column: 7, + }, + { + ...assignedError( + 'barArray', + '. Allowed unused vars must match /ignore/u', + ), + line: 5, + column: 7, + }, + ], + }, + { + code: ` +const array = [obj]; +const [{ _a, foo }] = array; +console.log(foo); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError('_a'), + line: 3, + column: 10, + }, + ], + }, + { + code: ` +function foo([{ _a, bar }]) { + bar; +} +foo(); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError('_a'), + line: 2, + column: 17, + }, + ], + }, + { + code: ` +let _a, b; +foo.forEach(item => { + [a, b] = item; +}); + `, + options: [{ destructuredArrayIgnorePattern: '^_' }], + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError('_a'), + line: 2, + column: 5, + }, + { + ...assignedError('b'), + line: 4, + column: 7, + }, + ], + }, // for-in loops (see #2342) { @@ -1684,6 +1938,27 @@ console.log(type); }, ], }, + { + code: ` +let type, coords; +({ type, ...coords } = data); +console.log(type); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 13, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, // Unused rest property without ignoreRestSiblings { diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index fa7d5934b26..7e3b9db43e3 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -416,6 +416,7 @@ declare module 'eslint/lib/rules/no-unused-vars' { argsIgnorePattern?: string; caughtErrors?: 'all' | 'none'; caughtErrorsIgnorePattern?: string; + destructuredArrayIgnorePattern?: string; }, ], {