prefer-set-has.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. 'use strict';
  2. const {findVariable} = require('@eslint-community/eslint-utils');
  3. const getVariableIdentifiers = require('./utils/get-variable-identifiers.js');
  4. const {
  5. matches,
  6. not,
  7. methodCallSelector,
  8. callOrNewExpressionSelector,
  9. } = require('./selectors/index.js');
  10. const MESSAGE_ID_ERROR = 'error';
  11. const MESSAGE_ID_SUGGESTION = 'suggestion';
  12. const messages = {
  13. [MESSAGE_ID_ERROR]: '`{{name}}` should be a `Set`, and use `{{name}}.has()` to check existence or non-existence.',
  14. [MESSAGE_ID_SUGGESTION]: 'Switch `{{name}}` to `Set`.',
  15. };
  16. // `[]`
  17. const arrayExpressionSelector = [
  18. '[init.type="ArrayExpression"]',
  19. ].join('');
  20. // `Array()` and `new Array()`
  21. const newArraySelector = callOrNewExpressionSelector({name: 'Array', path: 'init'});
  22. // `Array.from()` and `Array.of()`
  23. const arrayStaticMethodSelector = methodCallSelector({
  24. object: 'Array',
  25. methods: ['from', 'of'],
  26. path: 'init',
  27. });
  28. // `array.concat()`
  29. // `array.copyWithin()`
  30. // `array.fill()`
  31. // `array.filter()`
  32. // `array.flat()`
  33. // `array.flatMap()`
  34. // `array.map()`
  35. // `array.reverse()`
  36. // `array.slice()`
  37. // `array.sort()`
  38. // `array.splice()`
  39. const arrayMethodSelector = methodCallSelector({
  40. methods: [
  41. 'concat',
  42. 'copyWithin',
  43. 'fill',
  44. 'filter',
  45. 'flat',
  46. 'flatMap',
  47. 'map',
  48. 'reverse',
  49. 'slice',
  50. 'sort',
  51. 'splice',
  52. ],
  53. path: 'init',
  54. });
  55. const selector = [
  56. 'VariableDeclaration',
  57. // Exclude `export const foo = [];`
  58. not('ExportNamedDeclaration > .declaration'),
  59. ' > ',
  60. 'VariableDeclarator.declarations',
  61. matches([
  62. arrayExpressionSelector,
  63. newArraySelector,
  64. arrayStaticMethodSelector,
  65. arrayMethodSelector,
  66. ]),
  67. ' > ',
  68. 'Identifier.id',
  69. ].join('');
  70. const isIncludesCall = node => {
  71. const {type, optional, callee, arguments: includesArguments} = node.parent.parent ?? {};
  72. return (
  73. type === 'CallExpression'
  74. && !optional
  75. && callee.type === 'MemberExpression'
  76. && !callee.computed
  77. && !callee.optional
  78. && callee.object === node
  79. && callee.property.type === 'Identifier'
  80. && callee.property.name === 'includes'
  81. && includesArguments.length === 1
  82. && includesArguments[0].type !== 'SpreadElement'
  83. );
  84. };
  85. const multipleCallNodeTypes = new Set([
  86. 'ForOfStatement',
  87. 'ForStatement',
  88. 'ForInStatement',
  89. 'WhileStatement',
  90. 'DoWhileStatement',
  91. 'FunctionDeclaration',
  92. 'FunctionExpression',
  93. 'ArrowFunctionExpression',
  94. ]);
  95. const isMultipleCall = (identifier, node) => {
  96. const root = node.parent.parent.parent;
  97. let {parent} = identifier.parent; // `.include()` callExpression
  98. while (
  99. parent
  100. && parent !== root
  101. ) {
  102. if (multipleCallNodeTypes.has(parent.type)) {
  103. return true;
  104. }
  105. parent = parent.parent;
  106. }
  107. return false;
  108. };
  109. /** @param {import('eslint').Rule.RuleContext} context */
  110. const create = context => ({
  111. [selector](node) {
  112. const variable = findVariable(context.getScope(), node);
  113. // This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342
  114. // But can't reproduce, just ignore this case
  115. /* c8 ignore next 3 */
  116. if (!variable) {
  117. return;
  118. }
  119. const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node);
  120. if (
  121. identifiers.length === 0
  122. || identifiers.some(identifier => !isIncludesCall(identifier))
  123. ) {
  124. return;
  125. }
  126. if (
  127. identifiers.length === 1
  128. && identifiers.every(identifier => !isMultipleCall(identifier, node))
  129. ) {
  130. return;
  131. }
  132. const problem = {
  133. node,
  134. messageId: MESSAGE_ID_ERROR,
  135. data: {
  136. name: node.name,
  137. },
  138. };
  139. const fix = function * (fixer) {
  140. yield fixer.insertTextBefore(node.parent.init, 'new Set(');
  141. yield fixer.insertTextAfter(node.parent.init, ')');
  142. for (const identifier of identifiers) {
  143. yield fixer.replaceText(identifier.parent.property, 'has');
  144. }
  145. };
  146. if (node.typeAnnotation) {
  147. problem.suggest = [
  148. {
  149. messageId: MESSAGE_ID_SUGGESTION,
  150. fix,
  151. },
  152. ];
  153. } else {
  154. problem.fix = fix;
  155. }
  156. return problem;
  157. },
  158. });
  159. /** @type {import('eslint').Rule.RuleModule} */
  160. module.exports = {
  161. create,
  162. meta: {
  163. type: 'suggestion',
  164. docs: {
  165. description: 'Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence.',
  166. },
  167. fixable: 'code',
  168. hasSuggestions: true,
  169. messages,
  170. },
  171. };