diff --git a/docs/rules/prefer-array-some.md b/docs/rules/prefer-array-some.md new file mode 100644 index 0000000000..ec8d820a02 --- /dev/null +++ b/docs/rules/prefer-array-some.md @@ -0,0 +1,27 @@ +# Prefer `.some(…)` over `.find(…)`. + +Prefer using [`Array#some`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some) over [`Array#find`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find) when ensuring at least one element in the array passes a given check. + +## Fail + +```js +if (array.find(element => element === 'πŸ¦„')) { + // … +} +``` + +```js +const foo = array.find(element => element === 'πŸ¦„') ? bar : baz; +``` + +## Pass + +```js +if (array.some(element => element === 'πŸ¦„')) { + // … +} +``` + +```js +const foo = array.find(element => element === 'πŸ¦„') || bar; +``` diff --git a/index.js b/index.js index 5dcd13483b..49ac9af143 100644 --- a/index.js +++ b/index.js @@ -65,6 +65,7 @@ module.exports = { 'unicorn/numeric-separators-style': 'off', 'unicorn/prefer-add-event-listener': 'error', 'unicorn/prefer-array-find': 'error', + 'unicorn/prefer-array-some': 'error', 'unicorn/prefer-dataset': 'error', 'unicorn/prefer-date-now': 'error', 'unicorn/prefer-default-parameters': 'error', diff --git a/readme.md b/readme.md index 4760042dcb..0f4647b3f9 100644 --- a/readme.md +++ b/readme.md @@ -70,6 +70,7 @@ Configure it in `package.json`. "unicorn/numeric-separators-style": "off", "unicorn/prefer-add-event-listener": "error", "unicorn/prefer-array-find": "error", + "unicorn/prefer-array-some": "error", "unicorn/prefer-dataset": "error", "unicorn/prefer-date-now": "error", "unicorn/prefer-default-parameters": "error", @@ -140,6 +141,7 @@ Configure it in `package.json`. - [numeric-separators-style](docs/rules/numeric-separators-style.md) - Enforce the style of numeric separators by correctly grouping digits. *(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-array-some](docs/rules/prefer-array-some.md) - Prefer `.some(…)` over `.find(…)`. - [prefer-dataset](docs/rules/prefer-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)* - [prefer-date-now](docs/rules/prefer-date-now.md) - Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. *(fixable)* - [prefer-default-parameters](docs/rules/prefer-default-parameters.md) - Prefer default parameters over reassignment. *(fixable)* diff --git a/rules/explicit-length-check.js b/rules/explicit-length-check.js index 1cb404478f..9bdcabd154 100644 --- a/rules/explicit-length-check.js +++ b/rules/explicit-length-check.js @@ -2,6 +2,7 @@ const {isParenthesized} = require('eslint-utils'); const getDocumentationUrl = require('./utils/get-documentation-url'); const isLiteralValue = require('./utils/is-literal-value'); +const {isBooleanNode, getBooleanAncestor} = require('./utils/boolean'); const TYPE_NON_ZERO = 'non-zero'; const TYPE_ZERO = 'zero'; @@ -12,23 +13,6 @@ const messages = { [MESSAGE_ID_SUGGESTION]: 'Replace `.length` with `.length {{code}}`.' }; -const isLogicNot = node => - node && - node.type === 'UnaryExpression' && - node.operator === '!'; -const isLogicNotArgument = node => - isLogicNot(node.parent) && - node.parent.argument === node; -const isBooleanCall = node => - node && - node.type === 'CallExpression' && - node.callee && - node.callee.type === 'Identifier' && - node.callee.name === 'Boolean' && - node.arguments.length === 1; -const isBooleanCallArgument = node => - isBooleanCall(node.parent) && - node.parent.arguments[0] === node; const isCompareRight = (node, operator, value) => node.type === 'BinaryExpression' && node.operator === operator && @@ -72,23 +56,6 @@ const lengthSelector = [ '[property.name="length"]' ].join(''); -function getBooleanAncestor(node) { - let isNegative = false; - // eslint-disable-next-line no-constant-condition - while (true) { - if (isLogicNotArgument(node)) { - isNegative = !isNegative; - node = node.parent; - } else if (isBooleanCallArgument(node)) { - node = node.parent; - } else { - break; - } - } - - return {node, isNegative}; -} - function getLengthCheckNode(node) { node = node.parent; @@ -135,37 +102,6 @@ function getLengthCheckNode(node) { return {}; } -function isBooleanNode(node) { - if ( - isLogicNot(node) || - isLogicNotArgument(node) || - isBooleanCall(node) || - isBooleanCallArgument(node) - ) { - return true; - } - - const {parent} = node; - if ( - ( - parent.type === 'IfStatement' || - parent.type === 'ConditionalExpression' || - parent.type === 'WhileStatement' || - parent.type === 'DoWhileStatement' || - parent.type === 'ForStatement' - ) && - parent.test === node - ) { - return true; - } - - if (parent.type === 'LogicalExpression') { - return isBooleanNode(parent); - } - - return false; -} - function create(context) { const options = { 'non-zero': 'greater-than', diff --git a/rules/prefer-array-some.js b/rules/prefer-array-some.js new file mode 100644 index 0000000000..9574787ccf --- /dev/null +++ b/rules/prefer-array-some.js @@ -0,0 +1,48 @@ +'use strict'; +const getDocumentationUrl = require('./utils/get-documentation-url'); +const methodSelector = require('./utils/method-selector'); +const {isBooleanNode} = require('./utils/boolean'); + +const MESSAGE_ID_ERROR = 'error'; +const MESSAGE_ID_SUGGESTION = 'suggestion'; +const messages = { + [MESSAGE_ID_ERROR]: 'Prefer `.some(…)` over `.find(…)`.', + [MESSAGE_ID_SUGGESTION]: 'Replace `.find(…)` with `.some(…)`.' +}; + +const arrayFindCallSelector = methodSelector({ + name: 'find', + min: 1, + max: 2 +}); + +const create = context => { + return { + [arrayFindCallSelector](node) { + if (isBooleanNode(node)) { + node = node.callee.property; + context.report({ + node, + messageId: MESSAGE_ID_ERROR, + suggest: [ + { + messageId: MESSAGE_ID_SUGGESTION, + fix: fixer => fixer.replaceText(node, 'some') + } + ] + }); + } + } + }; +}; + +module.exports = { + create, + meta: { + type: 'suggestion', + docs: { + url: getDocumentationUrl(__filename) + }, + messages + } +}; diff --git a/rules/prefer-event-key.js b/rules/prefer-event-key.js index 17c1ff501b..ee70eb413d 100644 --- a/rules/prefer-event-key.js +++ b/rules/prefer-event-key.js @@ -171,7 +171,7 @@ const create = context => { if ( references && - references.find(reference => isPropertyOf(node, reference.identifier)) + references.some(reference => isPropertyOf(node, reference.identifier)) ) { report(node); } @@ -201,7 +201,7 @@ const create = context => { // Make sure initObject is a reference of eventVariable if ( references && - references.find(reference => reference.identifier === initObject) + references.some(reference => reference.identifier === initObject) ) { report(node.value); return; diff --git a/rules/utils/boolean.js b/rules/utils/boolean.js new file mode 100644 index 0000000000..6eef61c96a --- /dev/null +++ b/rules/utils/boolean.js @@ -0,0 +1,83 @@ +'use strict'; + +const isLogicNot = node => + node && + node.type === 'UnaryExpression' && + node.operator === '!'; +const isLogicNotArgument = node => + isLogicNot(node.parent) && + node.parent.argument === node; +const isBooleanCallArgument = node => + isBooleanCall(node.parent) && + node.parent.arguments[0] === node; +const isBooleanCall = node => + node && + node.type === 'CallExpression' && + node.callee && + node.callee.type === 'Identifier' && + node.callee.name === 'Boolean' && + node.arguments.length === 1; + +/** +Check if the value of node is a `boolean`. + +@param {Node} node +@returns {boolean} +*/ +function isBooleanNode(node) { + if ( + isLogicNot(node) || + isLogicNotArgument(node) || + isBooleanCall(node) || + isBooleanCallArgument(node) + ) { + return true; + } + + const {parent} = node; + if ( + ( + parent.type === 'IfStatement' || + parent.type === 'ConditionalExpression' || + parent.type === 'WhileStatement' || + parent.type === 'DoWhileStatement' || + parent.type === 'ForStatement' + ) && + parent.test === node + ) { + return true; + } + + if (parent.type === 'LogicalExpression') { + return isBooleanNode(parent); + } + + return false; +} + +/** +Get the boolean type-casting ancestor. + +@typedef {{ node: Node, isNegative: boolean }} Result + +@param {Node} node +@returns {Result} +*/ +function getBooleanAncestor(node) { + let isNegative = false; + // eslint-disable-next-line no-constant-condition + while (true) { + if (isLogicNotArgument(node)) { + isNegative = !isNegative; + node = node.parent; + } else if (isBooleanCallArgument(node)) { + node = node.parent; + } else { + break; + } + } + + return {node, isNegative}; +} + +module.exports = {isBooleanNode, getBooleanAncestor}; diff --git a/test/prefer-array-some.js b/test/prefer-array-some.js new file mode 100644 index 0000000000..d77ac245f1 --- /dev/null +++ b/test/prefer-array-some.js @@ -0,0 +1,105 @@ +import {outdent} from 'outdent'; +import {test} from './utils/test'; + +const MESSAGE_ID_ERROR = 'error'; +const MESSAGE_ID_SUGGESTION = 'suggestion'; + +const invalidCase = ({code, suggestionOutput}) => ({ + code, + output: code, + errors: [ + { + messageId: MESSAGE_ID_ERROR, + suggestions: [ + { + messageId: MESSAGE_ID_SUGGESTION, + output: suggestionOutput + } + ] + } + ] +}); + +test({ + valid: [ + // Not `boolean` + 'const bar = foo.find(fn)', + 'const bar = foo.find(fn) || baz', + + // Not matched `CallExpression` + ...[ + // Not `CallExpression` + 'new foo.find(fn)', + // Not `MemberExpression` + 'find(fn)', + // `callee.property` is not a `Identifier` + 'foo["find"](fn)', + 'foo["fi" + "nd"](fn)', + 'foo[`find`](fn)', + // Computed + 'foo[find](fn)', + // Not `.find` + 'foo.notFind(fn)', + // More or less argument(s) + 'foo.find()', + 'foo.find(fn, thisArgument, extraArgument)', + 'foo.find(...argumentsArray)' + ].map(code => `if (${code}) {}`) + ], + invalid: [ + ...[ + 'const bar = !foo.find(fn)', + 'const bar = Boolean(foo.find(fn))', + 'if (foo.find(fn)) {}', + 'const bar = foo.find(fn) ? 1 : 2', + 'while (foo.find(fn)) foo.shift();', + 'do {foo.shift();} while (foo.find(fn));', + 'for (; foo.find(fn); ) foo.shift();' + ].map(code => invalidCase({ + code, + suggestionOutput: code.replace('find', 'some') + })), + // Comments + invalidCase({ + code: 'console.log(foo /* comment 1 */ . /* comment 2 */ find /* comment 3 */ (fn) ? a : b)', + suggestionOutput: 'console.log(foo /* comment 1 */ . /* comment 2 */ some /* comment 3 */ (fn) ? a : b)' + }), + // This should not be reported, but `jQuery.find()` is always `truly`, + // It should not use as a boolean + invalidCase({ + code: 'if (jQuery.find(".outer > div")) {}', + suggestionOutput: 'if (jQuery.some(".outer > div")) {}' + }), + // Actual messages + { + code: 'if (foo.find(fn)) {}', + output: 'if (foo.find(fn)) {}', + errors: [ + { + message: 'Prefer `.some(…)` over `.find(…)`.', + suggestions: [ + { + desc: 'Replace `.find(…)` with `.some(…)`.', + output: 'if (foo.some(fn)) {}' + } + ] + } + ] + } + ] +}); + +test.visualize([ + 'if (array.find(element => element === "πŸ¦„")) {}', + 'const foo = array.find(element => element === "πŸ¦„") ? bar : baz;', + outdent` + if ( + array + .find(element => Array.isArray(element)) + // ^^^^ This should NOT report + .find(x => x === 0) + // ^^^^ This should report + ) { + } + ` +]); diff --git a/test/snapshots/prefer-array-some.js.md b/test/snapshots/prefer-array-some.js.md new file mode 100644 index 0000000000..9678f3c996 --- /dev/null +++ b/test/snapshots/prefer-array-some.js.md @@ -0,0 +1,42 @@ +# Snapshot report for `test/prefer-array-some.js` + +The actual snapshot is saved in `prefer-array-some.js.snap`. + +Generated by [AVA](https://avajs.dev). + +## prefer-array-some - #1 + +> Snapshot 1 + + `␊ + Error 1/1:␊ + > 1 | if (array.find(element => element === "πŸ¦„")) {}␊ + | ^^^^ Prefer `.some(…)` over `.find(…)`.␊ + ` + +## prefer-array-some - #2 + +> Snapshot 1 + + `␊ + Error 1/1:␊ + > 1 | const foo = array.find(element => element === "πŸ¦„") ? bar : baz;␊ + | ^^^^ Prefer `.some(…)` over `.find(…)`.␊ + ` + +## prefer-array-some - #3 + +> Snapshot 1 + + `␊ + Error 1/1:␊ + 1 | if (␊ + 2 | array␊ + 3 | .find(element => Array.isArray(element))␊ + 4 | // ^^^^ This should NOT report␊ + > 5 | .find(x => x === 0)␊ + | ^^^^ Prefer `.some(…)` over `.find(…)`.␊ + 6 | // ^^^^ This should report␊ + 7 | ) {␊ + 8 | }␊ + ` diff --git a/test/snapshots/prefer-array-some.js.snap b/test/snapshots/prefer-array-some.js.snap new file mode 100644 index 0000000000..1515e231c8 Binary files /dev/null and b/test/snapshots/prefer-array-some.js.snap differ