prefer-set-has.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. 'use strict';
  2. const {findVariable} = require('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. /* istanbul ignore next */
  72. if (!node.parent || !node.parent.parent) {
  73. return false;
  74. }
  75. const {type, optional, callee, arguments: includesArguments} = node.parent.parent;
  76. return (
  77. type === 'CallExpression'
  78. && !optional
  79. && callee
  80. && callee.type === 'MemberExpression'
  81. && !callee.computed
  82. && !callee.optional
  83. && callee.object === node
  84. && callee.property.type === 'Identifier'
  85. && callee.property.name === 'includes'
  86. && includesArguments.length === 1
  87. && includesArguments[0].type !== 'SpreadElement'
  88. );
  89. };
  90. const multipleCallNodeTypes = new Set([
  91. 'ForOfStatement',
  92. 'ForStatement',
  93. 'ForInStatement',
  94. 'WhileStatement',
  95. 'DoWhileStatement',
  96. 'FunctionDeclaration',
  97. 'FunctionExpression',
  98. 'ArrowFunctionExpression',
  99. ]);
  100. const isMultipleCall = (identifier, node) => {
  101. const root = node.parent.parent.parent;
  102. let {parent} = identifier.parent; // `.include()` callExpression
  103. while (
  104. parent
  105. && parent !== root
  106. ) {
  107. if (multipleCallNodeTypes.has(parent.type)) {
  108. return true;
  109. }
  110. parent = parent.parent;
  111. }
  112. return false;
  113. };
  114. /** @param {import('eslint').Rule.RuleContext} context */
  115. const create = context => ({
  116. [selector]: node => {
  117. const variable = findVariable(context.getScope(), node);
  118. // This was reported https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1075#issuecomment-768073342
  119. // But can't reproduce, just ignore this case
  120. /* istanbul ignore next */
  121. if (!variable) {
  122. return;
  123. }
  124. const identifiers = getVariableIdentifiers(variable).filter(identifier => identifier !== node);
  125. if (
  126. identifiers.length === 0
  127. || identifiers.some(identifier => !isIncludesCall(identifier))
  128. ) {
  129. return;
  130. }
  131. if (
  132. identifiers.length === 1
  133. && identifiers.every(identifier => !isMultipleCall(identifier, node))
  134. ) {
  135. return;
  136. }
  137. const problem = {
  138. node,
  139. messageId: MESSAGE_ID_ERROR,
  140. data: {
  141. name: node.name,
  142. },
  143. };
  144. const fix = function * (fixer) {
  145. yield fixer.insertTextBefore(node.parent.init, 'new Set(');
  146. yield fixer.insertTextAfter(node.parent.init, ')');
  147. for (const identifier of identifiers) {
  148. yield fixer.replaceText(identifier.parent.property, 'has');
  149. }
  150. };
  151. if (node.typeAnnotation) {
  152. problem.suggest = [
  153. {
  154. messageId: MESSAGE_ID_SUGGESTION,
  155. data: {
  156. name: node.name,
  157. },
  158. fix,
  159. },
  160. ];
  161. } else {
  162. problem.fix = fix;
  163. }
  164. return problem;
  165. },
  166. });
  167. /** @type {import('eslint').Rule.RuleModule} */
  168. module.exports = {
  169. create,
  170. meta: {
  171. type: 'suggestion',
  172. docs: {
  173. description: 'Prefer `Set#has()` over `Array#includes()` when checking for existence or non-existence.',
  174. },
  175. fixable: 'code',
  176. hasSuggestions: true,
  177. messages,
  178. },
  179. };