diff --git a/rules/no-for-loop.js b/rules/no-for-loop.js index 001bd9c6d1..13e91b5579 100644 --- a/rules/no-for-loop.js +++ b/rules/no-for-loop.js @@ -1,9 +1,10 @@ 'use strict'; -const {singular} = require('pluralize'); const {isClosingParenToken} = require('eslint-utils'); const getDocumentationUrl = require('./utils/get-documentation-url'); const isLiteralValue = require('./utils/is-literal-value'); const avoidCapture = require('./utils/avoid-capture'); +const getChildScopesRecursive = require('./utils/get-child-scopes-recursive'); +const singular = require('./utils/singular'); const MESSAGE_ID = 'no-for-loop'; const messages = { @@ -268,18 +269,6 @@ const getReferencesInChildScopes = (scope, name) => { ]; }; -const getChildScopesRecursive = scope => [ - scope, - ...scope.childScopes.flatMap(scope => getChildScopesRecursive(scope)) -]; - -const getSingularName = originalName => { - const singularName = singular(originalName); - if (singularName !== originalName) { - return singularName; - } -}; - const create = context => { const sourceCode = context.getSourceCode(); const {scopeManager, text: sourceCodeText} = sourceCode; @@ -361,7 +350,7 @@ const create = context => { const index = indexIdentifierName; const element = elementIdentifierName || - avoidCapture(getSingularName(arrayIdentifierName) || defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); + avoidCapture(singular(arrayIdentifierName) || defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); const array = arrayIdentifierName; let declarationElement = element; diff --git a/rules/prefer-array-find.js b/rules/prefer-array-find.js index 59a48e10a4..a30f05304b 100644 --- a/rules/prefer-array-find.js +++ b/rules/prefer-array-find.js @@ -3,6 +3,10 @@ const {isParenthesized, findVariable} = require('eslint-utils'); const getDocumentationUrl = require('./utils/get-documentation-url'); const methodSelector = require('./utils/method-selector'); const getVariableIdentifiers = require('./utils/get-variable-identifiers'); +const renameVariable = require('./utils/rename-variable'); +const avoidCapture = require('./utils/avoid-capture'); +const getChildScopesRecursive = require('./utils/get-child-scopes-recursive'); +const singular = require('./utils/singular'); const ERROR_ZERO_INDEX = 'error-zero-index'; const ERROR_SHIFT = 'error-shift'; @@ -288,6 +292,14 @@ const create = context => { problem.fix = function * (fixer) { yield fixer.replaceText(node.init.callee.property, 'find'); + const singularName = singular(node.id.name); + if (singularName) { + // Rename variable to be singularized now that it refers to a single item in the array instead of the entire array. + const singularizedName = avoidCapture(singularName, getChildScopesRecursive(context.getScope()), context.parserOptions.ecmaVersion); + const scope = context.getScope(); + yield * renameVariable(findVariable(scope, node.id), singularizedName, fixer); + } + for (const node of zeroIndexNodes) { yield fixer.removeRange([node.object.range[1], node.range[1]]); } diff --git a/rules/utils/get-child-scopes-recursive.js b/rules/utils/get-child-scopes-recursive.js new file mode 100644 index 0000000000..00a537bc9f --- /dev/null +++ b/rules/utils/get-child-scopes-recursive.js @@ -0,0 +1,14 @@ +'use strict'; + +/** +Gather a list of all Scopes starting recursively from the input Scope. + +@param {Scope} scope - The Scope to start checking from. +@returns {Scope[]} - The resulting Scopes. +*/ +const getChildScopesRecursive = scope => [ + scope, + ...scope.childScopes.flatMap(scope => getChildScopesRecursive(scope)) +]; + +module.exports = getChildScopesRecursive; diff --git a/rules/utils/singular.js b/rules/utils/singular.js new file mode 100644 index 0000000000..f198061e21 --- /dev/null +++ b/rules/utils/singular.js @@ -0,0 +1,18 @@ +'use strict'; + +const {singular: pluralizeSingular} = require('pluralize'); + +/** +Singularizes a word/name, i.e. `items` to `item`. + +@param {string} original - The word/name to singularize. +@returns {string|undefined} - The singularized result, or `undefined` if attempting singularization resulted in no change. +*/ +const singular = original => { + const singularized = pluralizeSingular(original); + if (singularized !== original) { + return singularized; + } +}; + +module.exports = singular; diff --git a/test/prefer-array-find.mjs b/test/prefer-array-find.mjs index 07c63899bb..ecd13cf3ca 100644 --- a/test/prefer-array-find.mjs +++ b/test/prefer-array-find.mjs @@ -145,6 +145,7 @@ ruleTester.run('prefer-array-find', rule, { 'function a([foo] = array.filter(bar)) {}', // Not `ArrayPattern` 'const foo = array.filter(bar)', + 'const items = array.filter(bar)', // Plural variable name. 'const {0: foo} = array.filter(bar)', // `elements` 'const [] = array.filter(bar)', @@ -175,6 +176,12 @@ ruleTester.run('prefer-array-find', rule, { output: 'const foo = array.find(bar)', errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] }, + { + // Plural variable name. + code: 'const [items] = array.filter(bar)', + output: 'const items = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, { code: 'const [foo] = array.filter(bar, thisArgument)', output: 'const foo = array.find(bar, thisArgument)', @@ -376,6 +383,7 @@ ruleTester.run('prefer-array-find', rule, { 'function a([foo] = array.filter(bar)) {}', // Not `ArrayPattern` 'foo = array.filter(bar)', + 'items = array.filter(bar)', // Plural variable name. '({foo} = array.filter(bar))', // `elements` '[] = array.filter(bar)', @@ -626,7 +634,11 @@ ruleTester.run('prefer-array-find', rule, { // More or less argument(s) 'const foo = array.filter(); const first = foo[0]', 'const foo = array.filter(bar, thisArgument, extraArgument); const first = foo[0]', - 'const foo = array.filter(...bar); const first = foo[0]' + 'const foo = array.filter(...bar); const first = foo[0]', + + // Singularization + 'const item = array.find(bar), first = item;', // Already singular variable name. + 'let items = array.filter(bar); console.log(items[0]); items = [1,2,3]; console.log(items[0]);' // Reassigning array variable. ], invalid: [ { @@ -669,6 +681,37 @@ ruleTester.run('prefer-array-find', rule, { output: 'const foo = array.find(bar); ({propOfFirst = unicorn} = foo);', errors: [{messageId: ERROR_DECLARATION}] }, + + // Singularization + { + // Multiple usages and child scope. + code: outdent` + const items = array.filter(bar); + const first = items[0]; + console.log(items[0]); + function foo() { return items[0]; } + `, + output: outdent` + const item = array.find(bar); + const first = item; + console.log(item); + function foo() { return item; } + `, + errors: [{messageId: ERROR_DECLARATION}] + }, + { + // Variable name collision. + code: 'const item = {}; const items = array.filter(bar); console.log(items[0]);', + output: 'const item = {}; const item_ = array.find(bar); console.log(item_);', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + // Variable defined with `let`. + code: 'let items = array.filter(bar); console.log(items[0]);', + output: 'let item = array.find(bar); console.log(item);', + errors: [{messageId: ERROR_DECLARATION}] + }, + // Not fixable { code: 'const foo = array.filter(bar); const [first = bar] = foo;',