no-array-callback-reference.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. 'use strict';
  2. const {isParenthesized} = require('eslint-utils');
  3. const {methodCallSelector, notFunctionSelector} = require('./selectors/index.js');
  4. const {isNodeMatches} = require('./utils/is-node-matches.js');
  5. const ERROR_WITH_NAME_MESSAGE_ID = 'error-with-name';
  6. const ERROR_WITHOUT_NAME_MESSAGE_ID = 'error-without-name';
  7. const REPLACE_WITH_NAME_MESSAGE_ID = 'replace-with-name';
  8. const REPLACE_WITHOUT_NAME_MESSAGE_ID = 'replace-without-name';
  9. const messages = {
  10. [ERROR_WITH_NAME_MESSAGE_ID]: 'Do not pass function `{{name}}` directly to `.{{method}}(…)`.',
  11. [ERROR_WITHOUT_NAME_MESSAGE_ID]: 'Do not pass function directly to `.{{method}}(…)`.',
  12. [REPLACE_WITH_NAME_MESSAGE_ID]: 'Replace function `{{name}}` with `… => {{name}}({{parameters}})`.',
  13. [REPLACE_WITHOUT_NAME_MESSAGE_ID]: 'Replace function with `… => …({{parameters}})`.',
  14. };
  15. const iteratorMethods = [
  16. [
  17. 'every',
  18. {
  19. ignore: [
  20. 'Boolean',
  21. ],
  22. },
  23. ],
  24. [
  25. 'filter', {
  26. extraSelector: '[callee.object.name!="Vue"]',
  27. ignore: [
  28. 'Boolean',
  29. ],
  30. },
  31. ],
  32. [
  33. 'find',
  34. {
  35. ignore: [
  36. 'Boolean',
  37. ],
  38. },
  39. ],
  40. [
  41. 'findIndex',
  42. {
  43. ignore: [
  44. 'Boolean',
  45. ],
  46. },
  47. ],
  48. [
  49. 'flatMap',
  50. ],
  51. [
  52. 'forEach',
  53. {
  54. returnsUndefined: true,
  55. },
  56. ],
  57. [
  58. 'map',
  59. {
  60. extraSelector: '[callee.object.name!="types"]',
  61. ignore: [
  62. 'String',
  63. 'Number',
  64. 'BigInt',
  65. 'Boolean',
  66. 'Symbol',
  67. ],
  68. },
  69. ],
  70. [
  71. 'reduce',
  72. {
  73. parameters: [
  74. 'accumulator',
  75. 'element',
  76. 'index',
  77. 'array',
  78. ],
  79. minParameters: 2,
  80. },
  81. ],
  82. [
  83. 'reduceRight',
  84. {
  85. parameters: [
  86. 'accumulator',
  87. 'element',
  88. 'index',
  89. 'array',
  90. ],
  91. minParameters: 2,
  92. },
  93. ],
  94. [
  95. 'some',
  96. {
  97. ignore: [
  98. 'Boolean',
  99. ],
  100. },
  101. ],
  102. ].map(([method, options]) => {
  103. options = {
  104. parameters: ['element', 'index', 'array'],
  105. ignore: [],
  106. minParameters: 1,
  107. extraSelector: '',
  108. returnsUndefined: false,
  109. ...options,
  110. };
  111. return [method, options];
  112. });
  113. const ignoredCallee = [
  114. // http://bluebirdjs.com/docs/api/promise.map.html
  115. 'Promise',
  116. 'React.Children',
  117. 'Children',
  118. 'lodash',
  119. 'underscore',
  120. '_',
  121. 'Async',
  122. 'async',
  123. 'this',
  124. '$',
  125. 'jQuery',
  126. ];
  127. function getProblem(context, node, method, options) {
  128. const {type} = node;
  129. const name = type === 'Identifier' ? node.name : '';
  130. if (type === 'Identifier' && options.ignore.includes(name)) {
  131. return;
  132. }
  133. const problem = {
  134. node,
  135. messageId: name ? ERROR_WITH_NAME_MESSAGE_ID : ERROR_WITHOUT_NAME_MESSAGE_ID,
  136. data: {
  137. name,
  138. method,
  139. },
  140. suggest: [],
  141. };
  142. const {parameters, minParameters, returnsUndefined} = options;
  143. for (let parameterLength = minParameters; parameterLength <= parameters.length; parameterLength++) {
  144. const suggestionParameters = parameters.slice(0, parameterLength).join(', ');
  145. const suggest = {
  146. messageId: name ? REPLACE_WITH_NAME_MESSAGE_ID : REPLACE_WITHOUT_NAME_MESSAGE_ID,
  147. data: {
  148. name,
  149. parameters: suggestionParameters,
  150. },
  151. fix(fixer) {
  152. const sourceCode = context.getSourceCode();
  153. let nodeText = sourceCode.getText(node);
  154. if (isParenthesized(node, sourceCode) || type === 'ConditionalExpression') {
  155. nodeText = `(${nodeText})`;
  156. }
  157. return fixer.replaceText(
  158. node,
  159. returnsUndefined
  160. ? `(${suggestionParameters}) => { ${nodeText}(${suggestionParameters}); }`
  161. : `(${suggestionParameters}) => ${nodeText}(${suggestionParameters})`,
  162. );
  163. },
  164. };
  165. problem.suggest.push(suggest);
  166. }
  167. return problem;
  168. }
  169. const ignoredFirstArgumentSelector = [
  170. notFunctionSelector('arguments.0'),
  171. // Ignore all `CallExpression`s include `function.bind()`
  172. '[arguments.0.type!="CallExpression"]',
  173. '[arguments.0.type!="FunctionExpression"]',
  174. '[arguments.0.type!="ArrowFunctionExpression"]',
  175. ].join('');
  176. /** @param {import('eslint').Rule.RuleContext} context */
  177. const create = context => {
  178. const rules = {};
  179. for (const [method, options] of iteratorMethods) {
  180. const selector = [
  181. method === 'reduce' || method === 'reduceRight' ? '' : ':not(AwaitExpression) > ',
  182. methodCallSelector({
  183. method,
  184. minimumArguments: 1,
  185. maximumArguments: 2,
  186. }),
  187. options.extraSelector,
  188. ignoredFirstArgumentSelector,
  189. ].join('');
  190. rules[selector] = node => {
  191. if (isNodeMatches(node.callee.object, ignoredCallee)) {
  192. return;
  193. }
  194. if (node.callee.object.type === 'CallExpression' && isNodeMatches(node.callee.object.callee, ignoredCallee)) {
  195. return;
  196. }
  197. const [iterator] = node.arguments;
  198. return getProblem(context, iterator, method, options);
  199. };
  200. }
  201. return rules;
  202. };
  203. /** @type {import('eslint').Rule.RuleModule} */
  204. module.exports = {
  205. create,
  206. meta: {
  207. type: 'problem',
  208. docs: {
  209. description: 'Prevent passing a function reference directly to iterator methods.',
  210. },
  211. hasSuggestions: true,
  212. messages,
  213. },
  214. };