prefer-includes.js 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. 'use strict';
  2. const isMethodNamed = require('./utils/is-method-named.js');
  3. const simpleArraySearchRule = require('./shared/simple-array-search-rule.js');
  4. const {isLiteral} = require('./ast/index.js');
  5. const MESSAGE_ID = 'prefer-includes';
  6. const messages = {
  7. [MESSAGE_ID]: 'Use `.includes()`, rather than `.indexOf()`, when checking for existence.',
  8. };
  9. // Ignore {_,lodash,underscore}.indexOf
  10. const ignoredVariables = new Set(['_', 'lodash', 'underscore']);
  11. const isIgnoredTarget = node => node.type === 'Identifier' && ignoredVariables.has(node.name);
  12. const isNegativeOne = node => node.type === 'UnaryExpression' && node.operator === '-' && node.argument && node.argument.type === 'Literal' && node.argument.value === 1;
  13. const isLiteralZero = node => isLiteral(node, 0);
  14. const isNegativeResult = node => ['===', '==', '<'].includes(node.operator);
  15. const getProblem = (context, node, target, argumentsNodes) => {
  16. const sourceCode = context.getSourceCode();
  17. const memberExpressionNode = target.parent;
  18. const dotToken = sourceCode.getTokenBefore(memberExpressionNode.property);
  19. const targetSource = sourceCode.getText().slice(memberExpressionNode.range[0], dotToken.range[0]);
  20. // Strip default `fromIndex`
  21. if (isLiteralZero(argumentsNodes[1])) {
  22. argumentsNodes = argumentsNodes.slice(0, 1);
  23. }
  24. const argumentsSource = argumentsNodes.map(argument => sourceCode.getText(argument));
  25. return {
  26. node: memberExpressionNode.property,
  27. messageId: MESSAGE_ID,
  28. fix(fixer) {
  29. const replacement = `${isNegativeResult(node) ? '!' : ''}${targetSource}.includes(${argumentsSource.join(', ')})`;
  30. return fixer.replaceText(node, replacement);
  31. },
  32. };
  33. };
  34. const includesOverSomeRule = simpleArraySearchRule({
  35. method: 'some',
  36. replacement: 'includes',
  37. });
  38. /** @param {import('eslint').Rule.RuleContext} context */
  39. const create = context => ({
  40. BinaryExpression(node) {
  41. const {left, right, operator} = node;
  42. if (!isMethodNamed(left, 'indexOf')) {
  43. return;
  44. }
  45. const target = left.callee.object;
  46. if (isIgnoredTarget(target)) {
  47. return;
  48. }
  49. const {arguments: argumentsNodes} = left;
  50. // Ignore something.indexOf(foo, 0, another)
  51. if (argumentsNodes.length > 2) {
  52. return;
  53. }
  54. if (
  55. (['!==', '!=', '>', '===', '=='].includes(operator) && isNegativeOne(right))
  56. || (['>=', '<'].includes(operator) && isLiteralZero(right))
  57. ) {
  58. return getProblem(
  59. context,
  60. node,
  61. target,
  62. argumentsNodes,
  63. );
  64. }
  65. },
  66. ...includesOverSomeRule.createListeners(context),
  67. });
  68. /** @type {import('eslint').Rule.RuleModule} */
  69. module.exports = {
  70. create,
  71. meta: {
  72. type: 'suggestion',
  73. docs: {
  74. description: 'Prefer `.includes()` over `.indexOf()` and `Array#some()` when checking for existence or non-existence.',
  75. },
  76. fixable: 'code',
  77. hasSuggestions: true,
  78. messages: {
  79. ...messages,
  80. ...includesOverSomeRule.messages,
  81. },
  82. },
  83. };