Skip to content

Commit

Permalink
prefer-array-find: Singularize variable name in autofix (#1243)
Browse files Browse the repository at this point in the history
Co-authored-by: fisker Cheung <lionkay@gmail.com>
  • Loading branch information
bmish and fisker committed May 8, 2021
1 parent 3205419 commit 6b340a3
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 32 deletions.
17 changes: 3 additions & 14 deletions 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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
50 changes: 33 additions & 17 deletions rules/prefer-array-find.js
Expand Up @@ -3,6 +3,11 @@ 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 extendFixRange = require('./utils/extend-fix-range');

const ERROR_ZERO_INDEX = 'error-zero-index';
const ERROR_SHIFT = 'error-shift';
Expand Down Expand Up @@ -94,10 +99,10 @@ const destructuringAssignmentSelector = [
// Need add `()` to the `AssignmentExpression`
// - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)`
// - `ObjectPattern`: `[{foo = baz}] = array.filter(bar)`
const assignmentNeedParenthesize = (node, source) => {
const assignmentNeedParenthesize = (node, sourceCode) => {
const isAssign = node.type === 'AssignmentExpression';

if (!isAssign || isParenthesized(node, source)) {
if (!isAssign || isParenthesized(node, sourceCode)) {
return false;
}

Expand Down Expand Up @@ -143,36 +148,36 @@ const getDestructuringLeftAndRight = node => {
return {};
};

function * fixDestructuring(node, source, fixer) {
function * fixDestructuring(node, sourceCode, fixer) {
const {left} = getDestructuringLeftAndRight(node);
const [element] = left.elements;

const leftText = source.getText(element.type === 'AssignmentPattern' ? element.left : element);
const leftText = sourceCode.getText(element.type === 'AssignmentPattern' ? element.left : element);
yield fixer.replaceText(left, leftText);

// `AssignmentExpression` always starts with `[` or `(`, so we don't need check ASI
if (assignmentNeedParenthesize(node, source)) {
if (assignmentNeedParenthesize(node, sourceCode)) {
yield fixer.insertTextBefore(node, '(');
yield fixer.insertTextAfter(node, ')');
}
}

const hasDefaultValue = node => getDestructuringLeftAndRight(node).left.elements[0].type === 'AssignmentPattern';

const fixDestructuringDefaultValue = (node, source, fixer, operator) => {
const fixDestructuringDefaultValue = (node, sourceCode, fixer, operator) => {
const {left, right} = getDestructuringLeftAndRight(node);
const [element] = left.elements;
const defaultValue = element.right;
let defaultValueText = source.getText(defaultValue);
let defaultValueText = sourceCode.getText(defaultValue);

if (isParenthesized(defaultValue, source) || hasLowerPrecedence(defaultValue, operator)) {
if (isParenthesized(defaultValue, sourceCode) || hasLowerPrecedence(defaultValue, operator)) {
defaultValueText = `(${defaultValueText})`;
}

return fixer.insertTextAfter(right, ` ${operator} ${defaultValueText}`);
};

const fixDestructuringAndReplaceFilter = (source, node) => {
const fixDestructuringAndReplaceFilter = (sourceCode, node) => {
const {property} = getDestructuringLeftAndRight(node).right.callee;

let suggest;
Expand All @@ -186,14 +191,14 @@ const fixDestructuringAndReplaceFilter = (source, node) => {
messageId,
* fix(fixer) {
yield fixer.replaceText(property, 'find');
yield fixDestructuringDefaultValue(node, source, fixer, operator);
yield * fixDestructuring(node, source, fixer);
yield fixDestructuringDefaultValue(node, sourceCode, fixer, operator);
yield * fixDestructuring(node, sourceCode, fixer);
}
}));
} else {
fix = function * (fixer) {
yield fixer.replaceText(property, 'find');
yield * fixDestructuring(node, source, fixer);
yield * fixDestructuring(node, sourceCode, fixer);
};
}

Expand Down Expand Up @@ -221,7 +226,7 @@ const isDestructuringFirstElement = node => {
};

const create = context => {
const source = context.getSourceCode();
const sourceCode = context.getSourceCode();

return {
[zeroIndexSelector](node) {
Expand All @@ -248,18 +253,19 @@ const create = context => {
context.report({
node: node.init.callee.property,
messageId: ERROR_DESTRUCTURING_DECLARATION,
...fixDestructuringAndReplaceFilter(source, node)
...fixDestructuringAndReplaceFilter(sourceCode, node)
});
},
[destructuringAssignmentSelector](node) {
context.report({
node: node.right.callee.property,
messageId: ERROR_DESTRUCTURING_ASSIGNMENT,
...fixDestructuringAndReplaceFilter(source, node)
...fixDestructuringAndReplaceFilter(sourceCode, node)
});
},
[filterVariableSelector](node) {
const variable = findVariable(context.getScope(), node.id);
const scope = context.getScope();
const variable = findVariable(scope, node.id);
const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id);

if (identifiers.length === 0) {
Expand Down Expand Up @@ -288,12 +294,22 @@ 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(scope), context.parserOptions.ecmaVersion);
yield * renameVariable(variable, singularizedName, fixer);

// Prevent possible variable conflicts
yield * extendFixRange(fixer, sourceCode.ast.range);
}

for (const node of zeroIndexNodes) {
yield fixer.removeRange([node.object.range[1], node.range[1]]);
}

for (const node of destructuringNodes) {
yield * fixDestructuring(node, source, fixer);
yield * fixDestructuring(node, sourceCode, fixer);
}
};
}
Expand Down
14 changes: 14 additions & 0 deletions 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;
18 changes: 18 additions & 0 deletions 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;
134 changes: 133 additions & 1 deletion test/prefer-array-find.mjs
Expand Up @@ -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)',
Expand Down Expand Up @@ -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)',
Expand Down Expand Up @@ -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)',
Expand Down Expand Up @@ -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: [
{
Expand Down Expand Up @@ -669,6 +681,126 @@ 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}]
},
{
code: outdent`
const item = 1;
function f() {
const items = array.filter(bar);
console.log(items[0]);
}
`,
output: outdent`
const item = 1;
function f() {
const item_ = array.find(bar);
console.log(item_);
}
`,
errors: [{messageId: ERROR_DECLARATION}]
},
{
code: outdent`
const items = array.filter(bar);
function f() {
const item = 1;
const item_ = 2;
console.log(items[0]);
}
`,
output: outdent`
const item__ = array.find(bar);
function f() {
const item = 1;
const item_ = 2;
console.log(item__);
}
`,
errors: [{messageId: ERROR_DECLARATION}]
},
{
code: outdent`
const items = array.filter(bar);
function f() {
console.log(items[0], item);
}
`,
output: outdent`
const item_ = array.find(bar);
function f() {
console.log(item_, item);
}
`,
errors: [{messageId: ERROR_DECLARATION}]
},
{
code: outdent`
const items = array.filter(bar);
console.log(items[0]);
function f(item) {
return item;
}
`,
output: outdent`
const item_ = array.find(bar);
console.log(item_);
function f(item) {
return item;
}
`,
errors: [{messageId: ERROR_DECLARATION}]
},
{
code: outdent`
function f() {
const items = array.filter(bar);
console.log(items[0]);
}
function f2(item) {
return item;
}
`,
output: outdent`
function f() {
const item = array.find(bar);
console.log(item);
}
function f2(item) {
return item;
}
`,
errors: [{messageId: ERROR_DECLARATION}]
},

// Not fixable
{
code: 'const foo = array.filter(bar); const [first = bar] = foo;',
Expand Down

0 comments on commit 6b340a3

Please sign in to comment.