prefer-array-find.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  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. /* istanbul ignore next */
  120. || node.type === 'AssignmentExpression'
  121. || node.type === 'YieldExpression'
  122. || node.type === 'SequenceExpression'
  123. );
  124. const getDestructuringLeftAndRight = node => {
  125. /* istanbul ignore next */
  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
  189. && node.parent.property.type === 'Literal'
  190. && node.parent.property.raw === '0';
  191. const isDestructuringFirstElement = node => {
  192. const {left, right} = getDestructuringLeftAndRight(node.parent);
  193. return left
  194. && right
  195. && right === node
  196. && left.type === 'ArrayPattern'
  197. && left.elements
  198. && left.elements.length === 1
  199. && left.elements[0].type !== 'RestElement';
  200. };
  201. /** @param {import('eslint').Rule.RuleContext} context */
  202. const create = context => {
  203. const sourceCode = context.getSourceCode();
  204. return {
  205. [zeroIndexSelector](node) {
  206. return {
  207. node: node.object.callee.property,
  208. messageId: ERROR_ZERO_INDEX,
  209. fix: fixer => [
  210. fixer.replaceText(node.object.callee.property, 'find'),
  211. removeMemberExpressionProperty(fixer, node, sourceCode),
  212. ],
  213. };
  214. },
  215. [shiftSelector](node) {
  216. return {
  217. node: node.callee.object.callee.property,
  218. messageId: ERROR_SHIFT,
  219. fix: fixer => [
  220. fixer.replaceText(node.callee.object.callee.property, 'find'),
  221. ...removeMethodCall(fixer, node, sourceCode),
  222. ],
  223. };
  224. },
  225. [destructuringDeclaratorSelector](node) {
  226. return {
  227. node: node.init.callee.property,
  228. messageId: ERROR_DESTRUCTURING_DECLARATION,
  229. ...fixDestructuringAndReplaceFilter(sourceCode, node),
  230. };
  231. },
  232. [destructuringAssignmentSelector](node) {
  233. return {
  234. node: node.right.callee.property,
  235. messageId: ERROR_DESTRUCTURING_ASSIGNMENT,
  236. ...fixDestructuringAndReplaceFilter(sourceCode, node),
  237. };
  238. },
  239. [filterVariableSelector](node) {
  240. const scope = context.getScope();
  241. const variable = findVariable(scope, node.id);
  242. const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node.id);
  243. if (identifiers.length === 0) {
  244. return;
  245. }
  246. const zeroIndexNodes = [];
  247. const destructuringNodes = [];
  248. for (const identifier of identifiers) {
  249. if (isAccessingZeroIndex(identifier)) {
  250. zeroIndexNodes.push(identifier.parent);
  251. } else if (isDestructuringFirstElement(identifier)) {
  252. destructuringNodes.push(identifier.parent);
  253. } else {
  254. return;
  255. }
  256. }
  257. const problem = {
  258. node: node.init.callee.property,
  259. messageId: ERROR_DECLARATION,
  260. };
  261. // `const [foo = bar] = baz` is not fixable
  262. if (!destructuringNodes.some(node => hasDefaultValue(node))) {
  263. problem.fix = function * (fixer) {
  264. yield fixer.replaceText(node.init.callee.property, 'find');
  265. const singularName = singular(node.id.name);
  266. if (singularName) {
  267. // Rename variable to be singularized now that it refers to a single item in the array instead of the entire array.
  268. const singularizedName = avoidCapture(singularName, getScopes(scope));
  269. yield * renameVariable(variable, singularizedName, fixer);
  270. // Prevent possible variable conflicts
  271. yield * extendFixRange(fixer, sourceCode.ast.range);
  272. }
  273. for (const node of zeroIndexNodes) {
  274. yield removeMemberExpressionProperty(fixer, node, sourceCode);
  275. }
  276. for (const node of destructuringNodes) {
  277. yield * fixDestructuring(node, sourceCode, fixer);
  278. }
  279. };
  280. }
  281. return problem;
  282. },
  283. };
  284. };
  285. /** @type {import('eslint').Rule.RuleModule} */
  286. module.exports = {
  287. create,
  288. meta: {
  289. type: 'suggestion',
  290. docs: {
  291. description: 'Prefer `.find(…)` over the first element from `.filter(…)`.',
  292. },
  293. fixable: 'code',
  294. hasSuggestions: true,
  295. messages,
  296. },
  297. };