prefer-array-find.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. 'use strict';
  2. const {isParenthesized, findVariable} = require('eslint-utils');
  3. const {
  4. not,
  5. methodCallSelector,
  6. notLeftHandSideSelector,
  7. } = require('./selectors/index.js');
  8. const getVariableIdentifiers = require('./utils/get-variable-identifiers.js');
  9. const avoidCapture = require('./utils/avoid-capture.js');
  10. const getScopes = require('./utils/get-scopes.js');
  11. const singular = require('./utils/singular.js');
  12. const {
  13. extendFixRange,
  14. removeMemberExpressionProperty,
  15. removeMethodCall,
  16. renameVariable,
  17. } = require('./fix/index.js');
  18. const ERROR_ZERO_INDEX = 'error-zero-index';
  19. const ERROR_SHIFT = 'error-shift';
  20. const ERROR_DESTRUCTURING_DECLARATION = 'error-destructuring-declaration';
  21. const ERROR_DESTRUCTURING_ASSIGNMENT = 'error-destructuring-assignment';
  22. const ERROR_DECLARATION = 'error-variable';
  23. const SUGGESTION_NULLISH_COALESCING_OPERATOR = 'suggest-nullish-coalescing-operator';
  24. const SUGGESTION_LOGICAL_OR_OPERATOR = 'suggest-logical-or-operator';
  25. const messages = {
  26. [ERROR_DECLARATION]: 'Prefer `.find(…)` over `.filter(…)`.',
  27. [ERROR_ZERO_INDEX]: 'Prefer `.find(…)` over `.filter(…)[0]`.',
  28. [ERROR_SHIFT]: 'Prefer `.find(…)` over `.filter(…).shift()`.',
  29. [ERROR_DESTRUCTURING_DECLARATION]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',
  30. // Same message as `ERROR_DESTRUCTURING_DECLARATION`, but different case
  31. [ERROR_DESTRUCTURING_ASSIGNMENT]: 'Prefer `.find(…)` over destructuring `.filter(…)`.',
  32. [SUGGESTION_NULLISH_COALESCING_OPERATOR]: 'Replace `.filter(…)` with `.find(…) ?? …`.',
  33. [SUGGESTION_LOGICAL_OR_OPERATOR]: 'Replace `.filter(…)` with `.find(…) || …`.',
  34. };
  35. const filterMethodSelectorOptions = {
  36. method: 'filter',
  37. minimumArguments: 1,
  38. maximumArguments: 2,
  39. };
  40. const filterVariableSelector = [
  41. 'VariableDeclaration',
  42. // Exclude `export const foo = [];`
  43. not('ExportNamedDeclaration > .declaration'),
  44. ' > ',
  45. 'VariableDeclarator.declarations',
  46. '[id.type="Identifier"]',
  47. methodCallSelector({
  48. ...filterMethodSelectorOptions,
  49. path: 'init',
  50. }),
  51. ].join('');
  52. const zeroIndexSelector = [
  53. 'MemberExpression',
  54. '[computed!=false]',
  55. '[property.type="Literal"]',
  56. '[property.raw="0"]',
  57. notLeftHandSideSelector(),
  58. methodCallSelector({
  59. ...filterMethodSelectorOptions,
  60. path: 'object',
  61. }),
  62. ].join('');
  63. const shiftSelector = [
  64. methodCallSelector({
  65. method: 'shift',
  66. argumentsLength: 0,
  67. }),
  68. methodCallSelector({
  69. ...filterMethodSelectorOptions,
  70. path: 'callee.object',
  71. }),
  72. ].join('');
  73. const destructuringDeclaratorSelector = [
  74. 'VariableDeclarator',
  75. '[id.type="ArrayPattern"]',
  76. '[id.elements.length=1]',
  77. '[id.elements.0.type!="RestElement"]',
  78. methodCallSelector({
  79. ...filterMethodSelectorOptions,
  80. path: 'init',
  81. }),
  82. ].join('');
  83. const destructuringAssignmentSelector = [
  84. 'AssignmentExpression',
  85. '[left.type="ArrayPattern"]',
  86. '[left.elements.length=1]',
  87. '[left.elements.0.type!="RestElement"]',
  88. methodCallSelector({
  89. ...filterMethodSelectorOptions,
  90. path: 'right',
  91. }),
  92. ].join('');
  93. // Need add `()` to the `AssignmentExpression`
  94. // - `ObjectExpression`: `[{foo}] = array.filter(bar)` fix to `{foo} = array.find(bar)`
  95. // - `ObjectPattern`: `[{foo = baz}] = array.filter(bar)`
  96. const assignmentNeedParenthesize = (node, sourceCode) => {
  97. const isAssign = node.type === 'AssignmentExpression';
  98. if (!isAssign || isParenthesized(node, sourceCode)) {
  99. return false;
  100. }
  101. const {left} = getDestructuringLeftAndRight(node);
  102. const [element] = left.elements;
  103. const {type} = element.type === 'AssignmentPattern' ? element.left : element;
  104. return type === 'ObjectExpression' || type === 'ObjectPattern';
  105. };
  106. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#Table
  107. const hasLowerPrecedence = (node, operator) => (
  108. (node.type === 'LogicalExpression' && (
  109. node.operator === operator
  110. // https://tc39.es/proposal-nullish-coalescing/ says
  111. // `??` has lower precedence than `||`
  112. // But MDN says
  113. // `??` has higher precedence than `||`
  114. || (operator === '||' && node.operator === '??')
  115. || (operator === '??' && (node.operator === '||' || node.operator === '&&'))
  116. ))
  117. || node.type === 'ConditionalExpression'
  118. // Lower than `assignment`, should already parenthesized
  119. /* c8 ignore next */
  120. || node.type === 'AssignmentExpression'
  121. || node.type === 'YieldExpression'
  122. || node.type === 'SequenceExpression'
  123. );
  124. const getDestructuringLeftAndRight = node => {
  125. /* c8 ignore next 3 */
  126. if (!node) {
  127. return {};
  128. }
  129. if (node.type === 'AssignmentExpression') {
  130. return node;
  131. }
  132. if (node.type === 'VariableDeclarator') {
  133. return {left: node.id, right: node.init};
  134. }
  135. return {};
  136. };
  137. function * fixDestructuring(node, sourceCode, fixer) {
  138. const {left} = getDestructuringLeftAndRight(node);
  139. const [element] = left.elements;
  140. const leftText = sourceCode.getText(element.type === 'AssignmentPattern' ? element.left : element);
  141. yield fixer.replaceText(left, leftText);
  142. // `AssignmentExpression` always starts with `[` or `(`, so we don't need check ASI
  143. if (assignmentNeedParenthesize(node, sourceCode)) {
  144. yield fixer.insertTextBefore(node, '(');
  145. yield fixer.insertTextAfter(node, ')');
  146. }
  147. }
  148. const hasDefaultValue = node => getDestructuringLeftAndRight(node).left.elements[0].type === 'AssignmentPattern';
  149. const fixDestructuringDefaultValue = (node, sourceCode, fixer, operator) => {
  150. const {left, right} = getDestructuringLeftAndRight(node);
  151. const [element] = left.elements;
  152. const defaultValue = element.right;
  153. let defaultValueText = sourceCode.getText(defaultValue);
  154. if (isParenthesized(defaultValue, sourceCode) || hasLowerPrecedence(defaultValue, operator)) {
  155. defaultValueText = `(${defaultValueText})`;
  156. }
  157. return fixer.insertTextAfter(right, ` ${operator} ${defaultValueText}`);
  158. };
  159. const fixDestructuringAndReplaceFilter = (sourceCode, node) => {
  160. const {property} = getDestructuringLeftAndRight(node).right.callee;
  161. let suggest;
  162. let fix;
  163. if (hasDefaultValue(node)) {
  164. suggest = [
  165. {operator: '??', messageId: SUGGESTION_NULLISH_COALESCING_OPERATOR},
  166. {operator: '||', messageId: SUGGESTION_LOGICAL_OR_OPERATOR},
  167. ].map(({messageId, operator}) => ({
  168. messageId,
  169. * fix(fixer) {
  170. yield fixer.replaceText(property, 'find');
  171. yield fixDestructuringDefaultValue(node, sourceCode, fixer, operator);
  172. yield * fixDestructuring(node, sourceCode, fixer);
  173. },
  174. }));
  175. } else {
  176. fix = function * (fixer) {
  177. yield fixer.replaceText(property, 'find');
  178. yield * fixDestructuring(node, sourceCode, fixer);
  179. };
  180. }
  181. return {fix, suggest};
  182. };
  183. const isAccessingZeroIndex = node =>
  184. node.parent
  185. && node.parent.type === 'MemberExpression'
  186. && node.parent.computed === true
  187. && node.parent.object === node
  188. && node.parent.property?.type === 'Literal'
  189. && node.parent.property.raw === '0';
  190. const isDestructuringFirstElement = node => {
  191. const {left, right} = getDestructuringLeftAndRight(node.parent);
  192. return left
  193. && right
  194. && right === node
  195. && left.type === 'ArrayPattern'
  196. && left.elements
  197. && left.elements.length === 1
  198. && left.elements[0].type !== 'RestElement';
  199. };
  200. /** @param {import('eslint').Rule.RuleContext} context */
  201. const create = context => {
  202. const sourceCode = context.getSourceCode();
  203. return {
  204. [zeroIndexSelector](node) {
  205. return {
  206. node: node.object.callee.property,
  207. messageId: ERROR_ZERO_INDEX,
  208. fix: fixer => [
  209. fixer.replaceText(node.object.callee.property, 'find'),
  210. removeMemberExpressionProperty(fixer, node, sourceCode),
  211. ],
  212. };
  213. },
  214. [shiftSelector](node) {
  215. return {
  216. node: node.callee.object.callee.property,
  217. messageId: ERROR_SHIFT,
  218. fix: fixer => [
  219. fixer.replaceText(node.callee.object.callee.property, 'find'),
  220. ...removeMethodCall(fixer, node, sourceCode),
  221. ],
  222. };
  223. },
  224. [destructuringDeclaratorSelector](node) {
  225. return {
  226. node: node.init.callee.property,
  227. messageId: ERROR_DESTRUCTURING_DECLARATION,
  228. ...fixDestructuringAndReplaceFilter(sourceCode, node),
  229. };
  230. },
  231. [destructuringAssignmentSelector](node) {
  232. return {
  233. node: node.right.callee.property,
  234. messageId: ERROR_DESTRUCTURING_ASSIGNMENT,
  235. ...fixDestructuringAndReplaceFilter(sourceCode, node),
  236. };
  237. },
  238. [filterVariableSelector](node) {
  239. const scope = context.getScope();
  240. const variable = findVariable(scope, node.id);
  241. const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id);
  242. if (identifiers.length === 0) {
  243. return;
  244. }
  245. const zeroIndexNodes = [];
  246. const destructuringNodes = [];
  247. for (const identifier of identifiers) {
  248. if (isAccessingZeroIndex(identifier)) {
  249. zeroIndexNodes.push(identifier.parent);
  250. } else if (isDestructuringFirstElement(identifier)) {
  251. destructuringNodes.push(identifier.parent);
  252. } else {
  253. return;
  254. }
  255. }
  256. const problem = {
  257. node: node.init.callee.property,
  258. messageId: ERROR_DECLARATION,
  259. };
  260. // `const [foo = bar] = baz` is not fixable
  261. if (!destructuringNodes.some(node => hasDefaultValue(node))) {
  262. problem.fix = function * (fixer) {
  263. yield fixer.replaceText(node.init.callee.property, 'find');
  264. const singularName = singular(node.id.name);
  265. if (singularName) {
  266. // Rename variable to be singularized now that it refers to a single item in the array instead of the entire array.
  267. const singularizedName = avoidCapture(singularName, getScopes(scope));
  268. yield * renameVariable(variable, singularizedName, fixer);
  269. // Prevent possible variable conflicts
  270. yield * extendFixRange(fixer, sourceCode.ast.range);
  271. }
  272. for (const node of zeroIndexNodes) {
  273. yield removeMemberExpressionProperty(fixer, node, sourceCode);
  274. }
  275. for (const node of destructuringNodes) {
  276. yield * fixDestructuring(node, sourceCode, fixer);
  277. }
  278. };
  279. }
  280. return problem;
  281. },
  282. };
  283. };
  284. /** @type {import('eslint').Rule.RuleModule} */
  285. module.exports = {
  286. create,
  287. meta: {
  288. type: 'suggestion',
  289. docs: {
  290. description: 'Prefer `.find(…)` over the first element from `.filter(…)`.',
  291. },
  292. fixable: 'code',
  293. hasSuggestions: true,
  294. messages,
  295. },
  296. };