simple-array-search-rule.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. 'use strict';
  2. const {hasSideEffect, isParenthesized, findVariable} = require('@eslint-community/eslint-utils');
  3. const {matches, methodCallSelector} = require('../selectors/index.js');
  4. const isFunctionSelfUsedInside = require('../utils/is-function-self-used-inside.js');
  5. const getBinaryExpressionSelector = path => [
  6. `[${path}.type="BinaryExpression"]`,
  7. `[${path}.operator="==="]`,
  8. `:matches([${path}.left.type="Identifier"], [${path}.right.type="Identifier"])`,
  9. ].join('');
  10. const getFunctionSelector = path => [
  11. `[${path}.generator!=true]`,
  12. `[${path}.async!=true]`,
  13. `[${path}.params.length=1]`,
  14. `[${path}.params.0.type="Identifier"]`,
  15. ].join('');
  16. const callbackFunctionSelector = path => matches([
  17. // Matches `foo.findIndex(bar => bar === baz)`
  18. [
  19. `[${path}.type="ArrowFunctionExpression"]`,
  20. getFunctionSelector(path),
  21. getBinaryExpressionSelector(`${path}.body`),
  22. ].join(''),
  23. // Matches `foo.findIndex(bar => {return bar === baz})`
  24. // Matches `foo.findIndex(function (bar) {return bar === baz})`
  25. [
  26. `:matches([${path}.type="ArrowFunctionExpression"], [${path}.type="FunctionExpression"])`,
  27. getFunctionSelector(path),
  28. `[${path}.body.type="BlockStatement"]`,
  29. `[${path}.body.body.length=1]`,
  30. `[${path}.body.body.0.type="ReturnStatement"]`,
  31. getBinaryExpressionSelector(`${path}.body.body.0.argument`),
  32. ].join(''),
  33. ]);
  34. const isIdentifierNamed = ({type, name}, expectName) => type === 'Identifier' && name === expectName;
  35. function simpleArraySearchRule({method, replacement}) {
  36. // Add prefix to avoid conflicts in `prefer-includes` rule
  37. const MESSAGE_ID_PREFIX = `prefer-${replacement}-over-${method}/`;
  38. const ERROR = `${MESSAGE_ID_PREFIX}error`;
  39. const SUGGESTION = `${MESSAGE_ID_PREFIX}suggestion`;
  40. const ERROR_MESSAGES = {
  41. findIndex: 'Use `.indexOf()` instead of `.findIndex()` when looking for the index of an item.',
  42. findLastIndex: 'Use `.lastIndexOf()` instead of `findLastIndex() when looking for the index of an item.`',
  43. some: `Use \`.${replacement}()\` instead of \`.${method}()\` when checking value existence.`,
  44. };
  45. const messages = {
  46. [ERROR]: ERROR_MESSAGES[method],
  47. [SUGGESTION]: `Replace \`.${method}()\` with \`.${replacement}()\`.`,
  48. };
  49. const selector = [
  50. methodCallSelector({
  51. method,
  52. argumentsLength: 1,
  53. }),
  54. callbackFunctionSelector('arguments.0'),
  55. ].join('');
  56. function createListeners(context) {
  57. const sourceCode = context.getSourceCode();
  58. const {scopeManager} = sourceCode;
  59. return {
  60. [selector](node) {
  61. const [callback] = node.arguments;
  62. const binaryExpression = callback.body.type === 'BinaryExpression'
  63. ? callback.body
  64. : callback.body.body[0].argument;
  65. const [parameter] = callback.params;
  66. const {left, right} = binaryExpression;
  67. const {name} = parameter;
  68. let searchValueNode;
  69. let parameterInBinaryExpression;
  70. if (isIdentifierNamed(left, name)) {
  71. searchValueNode = right;
  72. parameterInBinaryExpression = left;
  73. } else if (isIdentifierNamed(right, name)) {
  74. searchValueNode = left;
  75. parameterInBinaryExpression = right;
  76. } else {
  77. return;
  78. }
  79. const callbackScope = scopeManager.acquire(callback);
  80. if (
  81. // `parameter` is used somewhere else
  82. findVariable(callbackScope, parameter).references.some(({identifier}) => identifier !== parameterInBinaryExpression)
  83. || isFunctionSelfUsedInside(callback, callbackScope)
  84. ) {
  85. return;
  86. }
  87. const method = node.callee.property;
  88. const problem = {
  89. node: method,
  90. messageId: ERROR,
  91. suggest: [],
  92. };
  93. const fix = function * (fixer) {
  94. let text = sourceCode.getText(searchValueNode);
  95. if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) {
  96. text = `(${text})`;
  97. }
  98. yield fixer.replaceText(method, replacement);
  99. yield fixer.replaceText(callback, text);
  100. };
  101. if (hasSideEffect(searchValueNode, sourceCode)) {
  102. problem.suggest.push({messageId: SUGGESTION, fix});
  103. } else {
  104. problem.fix = fix;
  105. }
  106. return problem;
  107. },
  108. };
  109. }
  110. return {messages, createListeners};
  111. }
  112. module.exports = simpleArraySearchRule;