simple-array-search-rule.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. 'use strict';
  2. const {hasSideEffect, isParenthesized, findVariable} = require('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. some: `Use \`.${replacement}()\` instead of \`.${method}()\` when checking value existence.`,
  43. };
  44. const messages = {
  45. [ERROR]: ERROR_MESSAGES[method],
  46. [SUGGESTION]: `Replace \`.${method}()\` with \`.${replacement}()\`.`,
  47. };
  48. const selector = [
  49. methodCallSelector({
  50. method,
  51. argumentsLength: 1,
  52. }),
  53. callbackFunctionSelector('arguments.0'),
  54. ].join('');
  55. function createListeners(context) {
  56. const sourceCode = context.getSourceCode();
  57. const {scopeManager} = sourceCode;
  58. return {
  59. [selector](node) {
  60. const [callback] = node.arguments;
  61. const binaryExpression = callback.body.type === 'BinaryExpression'
  62. ? callback.body
  63. : callback.body.body[0].argument;
  64. const [parameter] = callback.params;
  65. const {left, right} = binaryExpression;
  66. const {name} = parameter;
  67. let searchValueNode;
  68. let parameterInBinaryExpression;
  69. if (isIdentifierNamed(left, name)) {
  70. searchValueNode = right;
  71. parameterInBinaryExpression = left;
  72. } else if (isIdentifierNamed(right, name)) {
  73. searchValueNode = left;
  74. parameterInBinaryExpression = right;
  75. } else {
  76. return;
  77. }
  78. const callbackScope = scopeManager.acquire(callback);
  79. if (
  80. // `parameter` is used somewhere else
  81. findVariable(callbackScope, parameter).references.some(({identifier}) => identifier !== parameterInBinaryExpression)
  82. || isFunctionSelfUsedInside(callback, callbackScope)
  83. ) {
  84. return;
  85. }
  86. const method = node.callee.property;
  87. const problem = {
  88. node: method,
  89. messageId: ERROR,
  90. suggest: [],
  91. };
  92. const fix = function * (fixer) {
  93. let text = sourceCode.getText(searchValueNode);
  94. if (isParenthesized(searchValueNode, sourceCode) && !isParenthesized(callback, sourceCode)) {
  95. text = `(${text})`;
  96. }
  97. yield fixer.replaceText(method, replacement);
  98. yield fixer.replaceText(callback, text);
  99. };
  100. if (hasSideEffect(searchValueNode, sourceCode)) {
  101. problem.suggest.push({messageId: SUGGESTION, fix});
  102. } else {
  103. problem.fix = fix;
  104. }
  105. return problem;
  106. },
  107. };
  108. }
  109. return {messages, createListeners};
  110. }
  111. module.exports = simpleArraySearchRule;