diff --git a/package.json b/package.json index dea370eded..fd51fde919 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "eslint-utils": "^2.0.0", "import-modules": "^2.0.0", "lodash": "^4.17.15", + "pluralize": "^8.0.0", "read-pkg-up": "^7.0.1", "regexp-tree": "^0.1.21", "reserved-words": "^0.1.2", diff --git a/rules/no-for-loop.js b/rules/no-for-loop.js index e2f40885de..cb83b6a233 100644 --- a/rules/no-for-loop.js +++ b/rules/no-for-loop.js @@ -3,6 +3,7 @@ const getDocumentationUrl = require('./utils/get-documentation-url'); const isLiteralValue = require('./utils/is-literal-value'); const {flatten} = require('lodash'); const avoidCapture = require('./utils/avoid-capture'); +const {singular} = require('pluralize'); const defaultElementName = 'element'; const isLiteralZero = node => isLiteralValue(node, 0); @@ -267,6 +268,19 @@ const getChildScopesRecursive = scope => [ ...flatten(scope.childScopes.map(scope => getChildScopesRecursive(scope))) ]; +const getSingularName = originalName => { + const singularName = singular(originalName); + if ( + !singularName || + singularName === originalName + ) { + // Bail if we can't produce a singular name that differs from the original name. + return; + } + + return singularName; +}; + const create = context => { const sourceCode = context.getSourceCode(); const {scopeManager} = sourceCode; @@ -342,7 +356,7 @@ const create = context => { const index = indexIdentifierName; const element = elementIdentifierName || - avoidCapture(defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); + avoidCapture(getSingularName(arrayIdentifierName) || defaultElementName, getChildScopesRecursive(bodyScope), context.parserOptions.ecmaVersion); const array = arrayIdentifierName; let declarationElement = element; diff --git a/test/no-for-loop.js b/test/no-for-loop.js index 43fbf888d6..e0f48530ff 100644 --- a/test/no-for-loop.js +++ b/test/no-for-loop.js @@ -649,6 +649,57 @@ ruleTester.run('no-for-loop', rule, { console.log(element); console.log(element_); } + `), + + // Singularization (simple case): + testCase(outdent` + for (let i = 0; i < plugins.length; i++) { + console.log(plugins[i]); + } + `, outdent` + for (const plugin of plugins) { + console.log(plugin); + } + `), + // Singularization (irregular case): + testCase(outdent` + for (let i = 0; i < people.length; i++) { + console.log(people[i]); + } + `, outdent` + for (const person of people) { + console.log(person); + } + `), + // Singularization (avoid using reserved JavaScript keywords): + testCase(outdent` + for (let i = 0; i < cases.length; i++) { + console.log(cases[i]); + } + `, outdent` + for (const case_ of cases) { + console.log(case_); + } + `), + // Singularization (camelCase): + testCase(outdent` + for (let i = 0; i < largeCities.length; i++) { + console.log(largeCities[i]); + } + `, outdent` + for (const largeCity of largeCities) { + console.log(largeCity); + } + `), + // Singularization (capital letters, multiple words): + testCase(outdent` + for (let i = 0; i < LARGE_CITIES.length; i++) { + console.log(LARGE_CITIES[i]); + } + `, outdent` + for (const LARGE_CITY of LARGE_CITIES) { + console.log(LARGE_CITY); + } `) ] });