From 12b46dacb56622914213390130685b1046fbc63f Mon Sep 17 00:00:00 2001 From: fisker Cheung Date: Sun, 31 May 2020 21:39:04 +0800 Subject: [PATCH] Add `prefer-array-find` rule (#735) Co-authored-by: Sindre Sorhus --- docs/rules/prefer-array-find.md | 33 ++ index.js | 1 + readme.md | 2 + rules/prefer-array-find.js | 322 +++++++++++++ rules/utils/method-selector.js | 2 +- test/prefer-array-find.js | 779 ++++++++++++++++++++++++++++++++ 6 files changed, 1138 insertions(+), 1 deletion(-) create mode 100644 docs/rules/prefer-array-find.md create mode 100644 rules/prefer-array-find.js create mode 100644 test/prefer-array-find.js diff --git a/docs/rules/prefer-array-find.md b/docs/rules/prefer-array-find.md new file mode 100644 index 0000000000..1025b9a683 --- /dev/null +++ b/docs/rules/prefer-array-find.md @@ -0,0 +1,33 @@ +# Prefer `.find(…)` over the first element from `.filter(…)` + +[`Array#find()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) breaks the loop as soon as it finds a match and doesn't create a new array. + +This rule is fixable unless default values are used in declaration or assignment. + +## Fail + +```js +const item = array.filter(x => x === '🦄')[0]; +``` + +```js +const item = array.filter(x => x === '🦄').shift(); +``` + +```js +const [item] = array.filter(x => x === '🦄'); +``` + +```js +[item] = array.filter(x => x === '🦄'); +``` + +## Pass + +```js +const item = array.find(x => x === '🦄'); +``` + +```js +item = array.find(x => x === '🦄'); +``` diff --git a/index.js b/index.js index 7a9355c12f..7242a47191 100644 --- a/index.js +++ b/index.js @@ -48,6 +48,7 @@ module.exports = { 'unicorn/no-zero-fractions': 'error', 'unicorn/number-literal-case': 'error', 'unicorn/prefer-add-event-listener': 'error', + 'unicorn/prefer-array-find': 'error', 'unicorn/prefer-dataset': 'error', 'unicorn/prefer-event-key': 'error', 'unicorn/prefer-flat-map': 'error', diff --git a/readme.md b/readme.md index de9484b9d4..2a0503dd0d 100644 --- a/readme.md +++ b/readme.md @@ -64,6 +64,7 @@ Configure it in `package.json`. "unicorn/no-zero-fractions": "error", "unicorn/number-literal-case": "error", "unicorn/prefer-add-event-listener": "error", + "unicorn/prefer-array-find": "error", "unicorn/prefer-dataset": "error", "unicorn/prefer-event-key": "error", "unicorn/prefer-flat-map": "error", @@ -124,6 +125,7 @@ Configure it in `package.json`. - [no-zero-fractions](docs/rules/no-zero-fractions.md) - Disallow number literals with zero fractions or dangling dots. *(fixable)* - [number-literal-case](docs/rules/number-literal-case.md) - Enforce proper case for numeric literals. *(fixable)* - [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)* +- [prefer-array-find](docs/rules/prefer-array-find.md) - Prefer `.find(…)` over the first element from `.filter(…)`. *(partly fixable)* - [prefer-dataset](docs/rules/prefer-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)* - [prefer-event-key](docs/rules/prefer-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)* - [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)* diff --git a/rules/prefer-array-find.js b/rules/prefer-array-find.js new file mode 100644 index 0000000000..909da0cc3e --- /dev/null +++ b/rules/prefer-array-find.js @@ -0,0 +1,322 @@ +'use strict'; +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 ERROR_ZERO_INDEX = 'error-zero-index'; +const ERROR_SHIFT = 'error-shift'; +const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration'; +const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment'; +const ERROR_DECLARATION = 'error-variable'; + +const SUGGESTION_NULLISH_COALESCING_OPERATOR = 'suggest-nullish-coalescing-operator'; +const SUGGESTION_LOGICAL_OR_OPERATOR = 'suggest-logical-or-operator'; + +const filterMethodSelectorOptions = { + name: 'filter', + min: 1, + max: 2 +}; + +const filterVariableSelector = [ + 'VariableDeclaration', + // Exclude `export const foo = [];` + `:not(${ + [ + 'ExportNamedDeclaration', + '>', + 'VariableDeclaration.declaration' + ].join('') + })`, + '>', + 'VariableDeclarator.declarations', + '[id.type="Identifier"]', + methodSelector({ + ...filterMethodSelectorOptions, + property: 'init' + }) +].join(''); + +const zeroIndexSelector = [ + 'MemberExpression', + '[computed=true]', + '[property.type="Literal"]', + '[property.raw="0"]', + methodSelector({ + ...filterMethodSelectorOptions, + property: 'object' + }) +].join(''); + +const shiftSelector = [ + methodSelector({ + name: 'shift', + length: 0 + }), + methodSelector({ + ...filterMethodSelectorOptions, + property: 'callee.object' + }) +].join(''); + +const destructuringDeclaratorSelector = [ + 'VariableDeclarator', + '[id.type="ArrayPattern"]', + '[id.elements.length=1]', + '[id.elements.0.type!="RestElement"]', + methodSelector({ + ...filterMethodSelectorOptions, + property: 'init' + }) +].join(''); + +const destructuringAssignmentSelector = [ + 'AssignmentExpression', + '[left.type="ArrayPattern"]', + '[left.elements.length=1]', + '[left.elements.0.type!="RestElement"]', + methodSelector({ + ...filterMethodSelectorOptions, + property: 'right' + }) +].join(''); + +// 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 isAssign = node.type === 'AssignmentExpression'; + + if (!isAssign || isParenthesized(node, source)) { + return false; + } + + const {left} = getDestructuringLeftAndRight(node); + const [element] = left.elements; + const {type} = element.type === 'AssignmentPattern' ? element.left : element; + return type === 'ObjectExpression' || type === 'ObjectPattern'; +}; + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table +const hasLowerPrecedence = (node, operator) => ( + (node.type === 'LogicalExpression' && ( + node.operator === operator || + // https://tc39.es/proposal-nullish-coalescing/ says + // `??` has lower precedence than `||` + // But MDN says + // `??` has higher precedence than `||` + (operator === '||' && node.operator === '??') || + (operator === '??' && (node.operator === '||' || node.operator === '&&')) + )) || + node.type === 'ConditionalExpression' || + // Lower than `assignment`, should already parenthesized + /* istanbul ignore next */ + node.type === 'AssignmentExpression' || + node.type === 'YieldExpression' || + node.type === 'SequenceExpression' +); + +const getDestructuringLeftAndRight = node => { + /* istanbul ignore next */ + if (!node) { + return {}; + } + + if (node.type === 'AssignmentExpression') { + return node; + } + + if (node.type === 'VariableDeclarator') { + return {left: node.id, right: node.init}; + } + + return {}; +}; + +const fixDestructuring = (node, source, fixer) => { + const {left} = getDestructuringLeftAndRight(node); + const [element] = left.elements; + + const leftText = source.getText(element.type === 'AssignmentPattern' ? element.left : element); + const fixes = [fixer.replaceText(left, leftText)]; + + // `AssignmentExpression` always starts with `[` or `(`, so we don't need check ASI + if (assignmentNeedParenthesize(node, source)) { + fixes.push(fixer.insertTextBefore(node, '(')); + fixes.push(fixer.insertTextAfter(node, ')')); + } + + return fixes; +}; + +const hasDefaultValue = node => getDestructuringLeftAndRight(node).left.elements[0].type === 'AssignmentPattern'; + +const fixDestructuringDefaultValue = (node, source, fixer, operator) => { + const {left, right} = getDestructuringLeftAndRight(node); + const [element] = left.elements; + const defaultValue = element.right; + let defaultValueText = source.getText(defaultValue); + + if (isParenthesized(defaultValue, source) || hasLowerPrecedence(defaultValue, operator)) { + defaultValueText = `(${defaultValueText})`; + } + + return fixer.insertTextAfter(right, ` ${operator} ${defaultValueText}`); +}; + +const fixDestructuringAndReplaceFilter = (source, node) => { + const {property} = getDestructuringLeftAndRight(node).right.callee; + + let suggest; + let fix; + + if (hasDefaultValue(node)) { + suggest = [ + {operator: '??', messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR}, + {operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR} + ].map(({messageId, operator}) => ({ + messageId, + fix: fixer => [ + fixer.replaceText(property, 'find'), + fixDestructuringDefaultValue(node, source, fixer, operator), + ...fixDestructuring(node, source, fixer) + ] + })); + } else { + fix = fixer => [ + fixer.replaceText(property, 'find'), + ...fixDestructuring(node, source, fixer) + ]; + } + + return {fix, suggest}; +}; + +const isAccessingZeroIndex = node => + node.parent && + node.parent.type === 'MemberExpression' && + node.parent.computed === true && + node.parent.object === node && + node.parent.property && + node.parent.property.type === 'Literal' && + node.parent.property.raw === '0'; + +const isDestructuringFirstElement = node => { + const {left, right} = getDestructuringLeftAndRight(node.parent); + return left && + right && + right === node && + left.type === 'ArrayPattern' && + left.elements && + left.elements.length === 1 && + left.elements[0].type !== 'RestElement'; +}; + +const create = context => { + const source = context.getSourceCode(); + + return { + [zeroIndexSelector](node) { + context.report({ + node: node.object.callee.property, + messageId: ERROR_ZERO_INDEX, + fix: fixer => [ + fixer.replaceText(node.object.callee.property, 'find'), + fixer.removeRange([node.object.range[1], node.range[1]]) + ] + }); + }, + [shiftSelector](node) { + context.report({ + node: node.callee.object.callee.property, + messageId: ERROR_SHIFT, + fix: fixer => [ + fixer.replaceText(node.callee.object.callee.property, 'find'), + fixer.removeRange([node.callee.object.range[1], node.range[1]]) + ] + }); + }, + [destructuringDeclaratorSelector](node) { + context.report({ + node: node.init.callee.property, + messageId: ERROR_DESTRUCTURING_DECLARATION, + ...fixDestructuringAndReplaceFilter(source, node) + }); + }, + [destructuringAssignmentSelector](node) { + context.report({ + node: node.right.callee.property, + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + ...fixDestructuringAndReplaceFilter(source, node) + }); + }, + [filterVariableSelector](node) { + const variable = findVariable(context.getScope(), node.id); + const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id); + + if (identifiers.length === 0) { + return; + } + + const zeroIndexNodes = []; + const destructuringNodes = []; + for (const identifier of identifiers) { + if (isAccessingZeroIndex(identifier)) { + zeroIndexNodes.push(identifier.parent); + } else if (isDestructuringFirstElement(identifier)) { + destructuringNodes.push(identifier.parent); + } else { + return; + } + } + + const problem = { + node: node.init.callee.property, + messageId: ERROR_DECLARATION + }; + + // `const [foo = bar] = baz` is not fixable + if (!destructuringNodes.some(node => hasDefaultValue(node))) { + problem.fix = fixer => { + const fixes = [ + fixer.replaceText(node.init.callee.property, 'find') + ]; + + for (const node of zeroIndexNodes) { + fixes.push(fixer.removeRange([node.object.range[1], node.range[1]])); + } + + for (const node of destructuringNodes) { + fixes.push(...fixDestructuring(node, source, fixer)); + } + + return fixes; + }; + } + + context.report(problem); + } + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + url: getDocumentationUrl(__filename) + }, + fixable: 'code', + messages: { + [ERROR_DECLARATION]: 'Prefer `.find(…)` over `.filter(…)`.', + [ERROR_ZERO_INDEX]: 'Prefer `.find(…)` over `.filter(…)[0]`.', + [ERROR_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.', + [ERROR_DESTRUCTURING_DECLARATION]: 'Prefer `.find(…)` over destructuring `.filter(…)`.', + // Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case + [ERROR_DESTRUCTURING_ASSIGNMENT]: 'Prefer `.find(…)` over destructuring `.filter(…)`.', + [SUGGESTION_NULLISH_COALESCING_OPERATOR]: 'Replace `.filter(…)` with `.find(…) ?? …`.', + [SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.' + } + } +}; diff --git a/rules/utils/method-selector.js b/rules/utils/method-selector.js index a996ae75e0..569be8eed4 100644 --- a/rules/utils/method-selector.js +++ b/rules/utils/method-selector.js @@ -25,7 +25,7 @@ module.exports = options => { ]; if (name) { - selector.push(`[callee.property.name="${name}"]`); + selector.push(`[${prefix}callee.property.name="${name}"]`); } if (Array.isArray(names) && names.length !== 0) { diff --git a/test/prefer-array-find.js b/test/prefer-array-find.js new file mode 100644 index 0000000000..8f6a211611 --- /dev/null +++ b/test/prefer-array-find.js @@ -0,0 +1,779 @@ +import test from 'ava'; +import {outdent} from 'outdent'; +import avaRuleTester from 'eslint-ava-rule-tester'; +import rule from '../rules/prefer-array-find'; + +const ERROR_ZERO_INDEX = 'error-zero-index'; +const ERROR_SHIFT = 'error-shift'; +const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration'; +const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment'; +const ERROR_DECLARATION = 'error-variable'; + +const SUGGESTION_NULLISH_COALESCING_OPERATOR = 'suggest-nullish-coalescing-operator'; +const SUGGESTION_LOGICAL_OR_OPERATOR = 'suggest-logical-or-operator'; + +const ruleTester = avaRuleTester(test, { + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}); + +// `[0]` +ruleTester.run('prefer-array-find', rule, { + valid: [ + 'array.find(foo)', + + // Test `[0]` + 'array.filter(foo)', + 'array.filter(foo)[+0]', + 'array.filter(foo)[-0]', + 'array.filter(foo)[1-1]', + 'array.filter(foo)["0"]', + 'array.filter(foo).first', + + // Test `.filter()` + // Not `CallExpression` + 'array.filter[0]', + // Not `MemberExpression` + 'filter(foo)[0]', + // `callee.property` is not a `Identifier` + 'array["filter"](foo)[0]', + // Computed + 'array[filter](foo)[0]', + // Not `filter` + 'array.notFilter(foo)[0]', + // More or less argument(s) + 'array.filter()[0]', + 'array.filter(foo, thisArgument, extraArgument)[0]', + 'array.filter(...foo)[0]' + ], + invalid: [ + { + code: 'array.filter(foo)[0]', + output: 'array.find(foo)', + errors: [{messageId: ERROR_ZERO_INDEX}] + }, + { + code: 'array.filter(foo, thisArgument)[0]', + output: 'array.find(foo, thisArgument)', + errors: [{messageId: ERROR_ZERO_INDEX}] + } + ] +}); + +// `.shift()` +ruleTester.run('prefer-array-find', rule, { + valid: [ + // Test `.shift()` + // Not `CallExpression` + 'array.filter(foo).shift', + // Not `MemberExpression` + 'shift(array.filter(foo))', + // `callee.property` is not a `Identifier` + 'array.filter(foo)["shift"]()', + // Computed + 'array.filter(foo)[shift]()', + // Not `shift` + 'array.filter(foo).notShift()', + // More or less argument(s) + 'array.filter(foo).shift(extraArgument)', + 'array.filter(foo).shift(...[])', + + // Test `.filter()` + // Not `CallExpression` + 'array.filter.shift()', + // Not `MemberExpression` + 'filter(foo).shift()', + // `callee.property` is not a `Identifier` + 'array["filter"](foo).shift()', + // Computed + 'array[filter](foo).shift()', + // Not `filter` + 'array.notFilter(foo).shift()', + // More or less argument(s) + 'array.filter().shift()', + 'array.filter(foo, thisArgument, extraArgument).shift()', + 'array.filter(...foo).shift()' + ], + invalid: [ + { + code: 'array.filter(foo).shift()', + output: 'array.find(foo)', + errors: [{messageId: ERROR_SHIFT}] + }, + { + code: 'array.filter(foo, thisArgument).shift()', + output: 'array.find(foo, thisArgument)', + errors: [{messageId: ERROR_SHIFT}] + }, + { + code: outdent` + const item = array + // comment 1 + .filter( + // comment 2 + x => x === '🦄' + ) + // comment 3 + .shift() + // comment 4 + ; + `, + output: outdent` + const item = array + // comment 1 + .find( + // comment 2 + x => x === '🦄' + ) + // comment 4 + ; + `, + errors: [{messageId: ERROR_SHIFT}] + } + ] +}); + +// `const [foo] =` +ruleTester.run('prefer-array-find', rule, { + valid: [ + // Test `const [item] = …` + // Not `VariableDeclarator` + 'function a([foo] = array.filter(bar)) {}', + // Not `ArrayPattern` + 'const foo = array.filter(bar)', + 'const {0: foo} = array.filter(bar)', + // `elements` + 'const [] = array.filter(bar)', + 'const [foo, another] = array.filter(bar)', + 'const [, foo] = array.filter(bar)', + // `RestElement` + 'const [...foo] = array.filter(bar)', + + // Test `.filter()` + // Not `CallExpression` + 'const [foo] = array.filter', + // Not `MemberExpression` + 'const [foo] = filter(bar)', + // `callee.property` is not a `Identifier` + 'const [foo] = array["filter"](bar)', + // Computed + 'const [foo] = array[filter](bar)', + // Not `filter` + 'const [foo] = array.notFilter(bar)', + // More or less argument(s) + 'const [foo] = array.filter()', + 'const [foo] = array.filter(bar, thisArgument, extraArgument)', + 'const [foo] = array.filter(...bar)' + ], + invalid: [ + { + code: 'const [foo] = array.filter(bar)', + output: 'const foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'const [foo] = array.filter(bar, thisArgument)', + output: 'const foo = array.find(bar, thisArgument)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'const [{foo}] = array.filter(fn);', + output: 'const {foo} = array.find(fn);', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'const [{foo = bar}] = array.filter(fn);', + output: 'const {foo = bar} = array.find(fn);', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'const [[foo]] = array.filter(fn);', + output: 'const [foo] = array.find(fn);', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'const [[foo = bar]] = array.filter(fn);', + output: 'const [foo = bar] = array.find(fn);', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'const [foo, ] = array.filter(bar)', + output: 'const foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'var [foo, ] = array.filter(bar)', + output: 'var foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'let [foo, ] = array.filter(bar)', + output: 'let foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'let a = 1, [foo, ] = array.filter(bar)', + output: 'let a = 1, foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'let a = 1, [{foo}] = array.filter(bar)', + output: 'let a = 1, {foo} = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: 'for (let [i] = array.filter(bar); i< 10; i++) {}', + output: 'for (let i = array.find(bar); i< 10; i++) {}', + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + { + code: outdent` + const [ + // comment 1 + item + ] + // comment 2 + = array + // comment 3 + .filter( + // comment 4 + x => x === '🦄' + ) + // comment 5 + ; + `, + output: outdent` + const item + // comment 2 + = array + // comment 3 + .find( + // comment 4 + x => x === '🦄' + ) + // comment 5 + ; + `, + errors: [{messageId: ERROR_DESTRUCTURING_DECLARATION}] + }, + // Suggestions + { + code: 'const [foo = baz] = array.filter(bar)', + output: 'const [foo = baz] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_DECLARATION, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'const foo = array.find(bar) ?? baz' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'const foo = array.find(bar) || baz' + } + ] + }] + }, + // Default value is parenthesized + { + code: 'const [foo = (bar)] = array.filter(bar)', + output: 'const [foo = (bar)] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_DECLARATION, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'const foo = array.find(bar) ?? (bar)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'const foo = array.find(bar) || (bar)' + } + ] + }] + }, + // Default value has lower precedence + { + code: 'const [foo = a ? b : c] = array.filter(bar)', + output: 'const [foo = a ? b : c] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_DECLARATION, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'const foo = array.find(bar) ?? (a ? b : c)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'const foo = array.find(bar) || (a ? b : c)' + } + ] + }] + }, + // // TODO: enable this test when ESLint support `nullish coalescing operator` + // { + // code: 'const [foo = a ?? b] = array.filter(bar)', + // output: 'const [foo = a ?? b] = array.filter(bar)', + // errors: [{ + // messageId: ERROR_DESTRUCTURING_DECLARATION, + // suggestions: [ + // { + // messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + // output: 'const foo = array.find(bar) ?? (a ?? b)' + // }, + // { + // messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + // output: 'const foo = array.find(bar) || (a ?? b)' + // } + // ] + // }] + // }, + { + code: 'const [foo = a || b] = array.filter(bar)', + output: 'const [foo = a || b] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_DECLARATION, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'const foo = array.find(bar) ?? (a || b)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'const foo = array.find(bar) || (a || b)' + } + ] + }] + }, + { + code: 'const [foo = a && b] = array.filter(bar)', + output: 'const [foo = a && b] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_DECLARATION, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'const foo = array.find(bar) ?? (a && b)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'const foo = array.find(bar) || a && b' + } + ] + }] + } + ] +}); + +// `[foo] =` +ruleTester.run('prefer-array-find', rule, { + valid: [ + // Test `[item] = …` + // Not `AssignmentExpression` + 'function a([foo] = array.filter(bar)) {}', + // Not `ArrayPattern` + 'foo = array.filter(bar)', + '({foo} = array.filter(bar))', + // `elements` + '[] = array.filter(bar)', + '[foo, another] = array.filter(bar)', + '[, foo] = array.filter(bar)', + // `RestElement` + '[...foo] = array.filter(bar)', + + // Test `.filter()` + // Not `CallExpression` + '[foo] = array.filter', + // Not `MemberExpression` + '[foo] = filter(bar)', + // `callee.property` is not a `Identifier` + '[foo] = array["filter"](bar)', + // Computed + '[foo] = array[filter](bar)', + // Not `filter` + '[foo] = array.notFilter(bar)', + // More or less argument(s) + '[foo] = array.filter()', + '[foo] = array.filter(bar, thisArgument, extraArgument)', + '[foo] = array.filter(...bar)' + ], + invalid: [ + { + code: '[foo] = array.filter(bar)', + output: 'foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: '[foo] = array.filter(bar, thisArgument)', + output: 'foo = array.find(bar, thisArgument)', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: '[foo.bar().baz] = array.filter(fn)', + output: 'foo.bar().baz = array.find(fn)', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: '[{foo}] = array.filter(fn);', + output: '({foo} = array.find(fn));', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: '[[foo]] = array.filter(fn);', + output: '[foo] = array.find(fn);', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: '[{foo = baz}] = array.filter(fn);', + output: '({foo = baz} = array.find(fn));', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: '[foo, ] = array.filter(bar)', + output: 'foo = array.find(bar)', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + { + code: 'for ([i] = array.filter(bar); i< 10; i++) {}', + output: 'for (i = array.find(bar); i< 10; i++) {}', + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + // `no-semi` style + { + code: outdent` + let foo + const bar = [] + ;[foo] = array.filter(bar) + `, + output: outdent` + let foo + const bar = [] + ;foo = array.find(bar) + `, + errors: [{messageId: ERROR_DESTRUCTURING_ASSIGNMENT}] + }, + // Suggestions + { + code: '[foo = baz] = array.filter(bar)', + output: '[foo = baz] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'foo = array.find(bar) ?? baz' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'foo = array.find(bar) || baz' + } + ] + }] + }, + { + code: '[{foo} = baz] = array.filter(bar)', + output: '[{foo} = baz] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: '({foo} = array.find(bar) ?? baz)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: '({foo} = array.find(bar) || baz)' + } + ] + }] + }, + { + code: ';([{foo} = baz] = array.filter(bar))', + output: ';([{foo} = baz] = array.filter(bar))', + errors: [{ + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: ';({foo} = array.find(bar) ?? baz)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: ';({foo} = array.find(bar) || baz)' + } + ] + }] + }, + // Default value is parenthesized + { + code: '[foo = (bar)] = array.filter(bar)', + output: '[foo = (bar)] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'foo = array.find(bar) ?? (bar)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'foo = array.find(bar) || (bar)' + } + ] + }] + }, + // Default value has lower precedence + { + code: '[foo = a ? b : c] = array.filter(bar)', + output: '[foo = a ? b : c] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'foo = array.find(bar) ?? (a ? b : c)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'foo = array.find(bar) || (a ? b : c)' + } + ] + }] + }, + { + code: '[foo = a || b] = array.filter(bar)', + output: '[foo = a || b] = array.filter(bar)', + errors: [{ + messageId: ERROR_DESTRUCTURING_ASSIGNMENT, + suggestions: [ + { + messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR, + output: 'foo = array.find(bar) ?? (a || b)' + }, + { + messageId: SUGGESTION_LOGICAL_OR_OPERATOR, + output: 'foo = array.find(bar) || (a || b)' + } + ] + }] + } + ] +}); + +// `const foo = array.filter(); foo[0]; [bar] = foo` +ruleTester.run('prefer-array-find', rule, { + valid: [ + 'const foo = array.find(bar), first = foo[0];', + 'const foo = array.filter(bar), first = notFoo[0];', + 'const foo = array.filter(bar), first = foo[+0];', + 'const foo = array.filter(bar); first = foo;', + 'const foo = array.filter(bar), first = a[foo][0];', + 'const foo = array.filter(bar), first = foo[-0];', + 'const foo = array.filter(bar), first = foo[1-1];', + 'const foo = array.filter(bar), first = foo["0"];', + 'const foo = array.filter(bar), first = foo.first;', + 'foo = array.filter(bar); const first = foo[+0];', + 'const {foo} = array.filter(bar), first = foo[0];', + outdent` + const foo = array.filter(bar); + doSomething(foo); + const first = foo[0]; + `, + outdent` + var foo = array.filter(bar); + var foo = array.filter(bar); + const first = foo[0]; + `, + outdent` + export const foo = array.filter(bar); + const first = foo[0]; + `, + + 'const foo = array.find(bar); const [first] = foo;', + 'const foo = array.find(bar); [first] = foo;', + 'const foo = array.filter(bar); const [first] = notFoo;', + 'const foo = array.filter(bar); [first] = notFoo;', + 'const foo = array.filter(bar); const first = foo;', + 'const foo = array.filter(bar); first = foo;', + 'const foo = array.filter(bar); const {0: first} = foo;', + 'const foo = array.filter(bar); ({0: first} = foo);', + 'const foo = array.filter(bar); const [] = foo;', + 'const foo = array.filter(bar); const [first, another] = foo;', + 'const foo = array.filter(bar); [first, another] = foo;', + 'const foo = array.filter(bar); const [,first] = foo;', + 'const foo = array.filter(bar); [,first] = foo;', + 'const foo = array.filter(bar); const [...first] = foo;', + 'const foo = array.filter(bar); [...first] = foo;', + outdent` + const foo = array.filter(bar); + function a([bar] = foo) {} + `, + + // Test `.filter()` + // Not `CallExpression` + 'const foo = array.filter; const first = foo[0]', + // Not `MemberExpression` + 'const foo = filter(bar); const first = foo[0]', + // `callee.property` is not a `Identifier` + 'const foo = array["filter"](bar); const first = foo[0]', + // Computed + 'const foo = array[filter](bar); const first = foo[0]', + // Not `filter` + 'const foo = array.notFilter(bar); const first = foo[0]', + // 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]' + ], + invalid: [ + { + code: 'const foo = array.filter(bar); const first = foo[0];', + output: 'const foo = array.find(bar); const first = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'const foo = array.filter(bar), first = foo[0];', + output: 'const foo = array.find(bar), first = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'var foo = array.filter(bar), first = foo[0];', + output: 'var foo = array.find(bar), first = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'let foo = array.filter(bar), first = foo[0];', + output: 'let foo = array.find(bar), first = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'const foo = array.filter(bar); const [first] = foo;', + output: 'const foo = array.find(bar); const first = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'const foo = array.filter(bar); [first] = foo;', + output: 'const foo = array.find(bar); first = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'const foo = array.filter(bar); const [{propOfFirst = unicorn}] = foo;', + output: 'const foo = array.find(bar); const {propOfFirst = unicorn} = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'const foo = array.filter(bar); [{propOfFirst = unicorn}] = foo;', + output: 'const foo = array.find(bar); ({propOfFirst = unicorn} = foo);', + errors: [{messageId: ERROR_DECLARATION}] + }, + // Not fixable + { + code: 'const foo = array.filter(bar); const [first = bar] = foo;', + output: 'const foo = array.filter(bar); const [first = bar] = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: 'const foo = array.filter(bar); [first = bar] = foo;', + output: 'const foo = array.filter(bar); [first = bar] = foo;', + errors: [{messageId: ERROR_DECLARATION}] + }, + // Many + { + code: 'let foo = array.filter(bar);foo[0](foo[0])[foo[0]];', + output: 'let foo = array.find(bar);foo(foo)[foo];', + errors: [{messageId: ERROR_DECLARATION}] + }, + { + code: outdent` + let baz; + const foo = array.filter(bar); + const [bar] = foo; + [{bar}] = foo; + function getValueOfFirst() { + return foo[0].value; + } + function getPropertyOfFirst(property) { + return foo[0][property]; + } + `, + output: outdent` + let baz; + const foo = array.find(bar); + const bar = foo; + ({bar} = foo); + function getValueOfFirst() { + return foo.value; + } + function getPropertyOfFirst(property) { + return foo[property]; + } + `, + errors: [{messageId: ERROR_DECLARATION}] + } + ] +}); + +// Mixed +ruleTester.run('prefer-array-find', rule, { + valid: [], + invalid: [ + { + code: outdent` + const quz = array.filter(fn); + const [foo] = array.filter(quz[0]); + [{bar: baz}] = foo[ + array.filter(fn)[0] + ].filter( + array.filter(fn).shift() + ); + `, + // Eslint can't fix all of them, + // But run test on this again will fix to correct code, + // See next test + output: outdent` + const quz = array.find(fn); + const [foo] = array.filter(quz); + ({bar: baz} = foo[ + array.filter(fn)[0] + ].find( + array.filter(fn).shift() + )); + `, + errors: [ + {messageId: ERROR_DECLARATION}, + {messageId: ERROR_DESTRUCTURING_DECLARATION}, + {messageId: ERROR_ZERO_INDEX}, + {messageId: ERROR_DESTRUCTURING_ASSIGNMENT}, + {messageId: ERROR_SHIFT} + ] + }, + { + // This code from previous output + code: outdent` + const quz = array.find(fn); + const [foo] = array.filter(quz); + ({bar: baz} = foo[ + array.filter(fn)[0] + ].find( + array.filter(fn).shift() + )); + `, + output: outdent` + const quz = array.find(fn); + const foo = array.find(quz); + ({bar: baz} = foo[ + array.find(fn) + ].find( + array.find(fn) + )); + `, + errors: [ + {messageId: ERROR_DESTRUCTURING_DECLARATION}, + {messageId: ERROR_ZERO_INDEX}, + {messageId: ERROR_SHIFT} + ] + } + ] +});