no-array-method-this-argument.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. 'use strict';
  2. const {hasSideEffect} = require('@eslint-community/eslint-utils');
  3. const {methodCallSelector, notFunctionSelector} = require('./selectors/index.js');
  4. const {removeArgument} = require('./fix/index.js');
  5. const {getParentheses, getParenthesizedText} = require('./utils/parentheses.js');
  6. const shouldAddParenthesesToMemberExpressionObject = require('./utils/should-add-parentheses-to-member-expression-object.js');
  7. const {isNodeMatches} = require('./utils/is-node-matches.js');
  8. const ERROR = 'error';
  9. const SUGGESTION_BIND = 'suggestion-bind';
  10. const SUGGESTION_REMOVE = 'suggestion-remove';
  11. const messages = {
  12. [ERROR]: 'Do not use the `this` argument in `Array#{{method}}()`.',
  13. [SUGGESTION_REMOVE]: 'Remove the second argument.',
  14. [SUGGESTION_BIND]: 'Use a bound function.',
  15. };
  16. const ignored = [
  17. 'lodash.every',
  18. '_.every',
  19. 'underscore.every',
  20. 'lodash.filter',
  21. '_.filter',
  22. 'underscore.filter',
  23. 'Vue.filter',
  24. 'R.filter',
  25. 'lodash.find',
  26. '_.find',
  27. 'underscore.find',
  28. 'R.find',
  29. 'lodash.findLast',
  30. '_.findLast',
  31. 'underscore.findLast',
  32. 'R.findLast',
  33. 'lodash.findIndex',
  34. '_.findIndex',
  35. 'underscore.findIndex',
  36. 'R.findIndex',
  37. 'lodash.findLastIndex',
  38. '_.findLastIndex',
  39. 'underscore.findLastIndex',
  40. 'R.findLastIndex',
  41. 'lodash.flatMap',
  42. '_.flatMap',
  43. 'lodash.forEach',
  44. '_.forEach',
  45. 'React.Children.forEach',
  46. 'Children.forEach',
  47. 'R.forEach',
  48. 'lodash.map',
  49. '_.map',
  50. 'underscore.map',
  51. 'React.Children.map',
  52. 'Children.map',
  53. 'jQuery.map',
  54. '$.map',
  55. 'R.map',
  56. 'lodash.some',
  57. '_.some',
  58. 'underscore.some',
  59. ];
  60. const selector = [
  61. methodCallSelector({
  62. methods: [
  63. 'every',
  64. 'filter',
  65. 'find',
  66. 'findLast',
  67. 'findIndex',
  68. 'findLastIndex',
  69. 'flatMap',
  70. 'forEach',
  71. 'map',
  72. 'some',
  73. ],
  74. argumentsLength: 2,
  75. }),
  76. notFunctionSelector('arguments.0'),
  77. ].join('');
  78. function removeThisArgument(callExpression, sourceCode) {
  79. return fixer => removeArgument(fixer, callExpression.arguments[1], sourceCode);
  80. }
  81. function useBoundFunction(callExpression, sourceCode) {
  82. return function * (fixer) {
  83. yield removeThisArgument(callExpression, sourceCode)(fixer);
  84. const [callback, thisArgument] = callExpression.arguments;
  85. const callbackParentheses = getParentheses(callback, sourceCode);
  86. const isParenthesized = callbackParentheses.length > 0;
  87. const callbackLastToken = isParenthesized
  88. ? callbackParentheses[callbackParentheses.length - 1]
  89. : callback;
  90. if (
  91. !isParenthesized
  92. && shouldAddParenthesesToMemberExpressionObject(callback, sourceCode)
  93. ) {
  94. yield fixer.insertTextBefore(callbackLastToken, '(');
  95. yield fixer.insertTextAfter(callbackLastToken, ')');
  96. }
  97. const thisArgumentText = getParenthesizedText(thisArgument, sourceCode);
  98. // `thisArgument` was a argument, no need add extra parentheses
  99. yield fixer.insertTextAfter(callbackLastToken, `.bind(${thisArgumentText})`);
  100. };
  101. }
  102. /** @param {import('eslint').Rule.RuleContext} context */
  103. const create = context => {
  104. const sourceCode = context.getSourceCode();
  105. return {
  106. [selector](callExpression) {
  107. const {callee} = callExpression;
  108. if (isNodeMatches(callee, ignored)) {
  109. return;
  110. }
  111. const method = callee.property.name;
  112. const [callback, thisArgument] = callExpression.arguments;
  113. const problem = {
  114. node: thisArgument,
  115. messageId: ERROR,
  116. data: {method},
  117. };
  118. const thisArgumentHasSideEffect = hasSideEffect(thisArgument, sourceCode);
  119. const isArrowCallback = callback.type === 'ArrowFunctionExpression';
  120. if (isArrowCallback) {
  121. if (thisArgumentHasSideEffect) {
  122. problem.suggest = [
  123. {
  124. messageId: SUGGESTION_REMOVE,
  125. fix: removeThisArgument(callExpression, sourceCode),
  126. },
  127. ];
  128. } else {
  129. problem.fix = removeThisArgument(callExpression, sourceCode);
  130. }
  131. return problem;
  132. }
  133. problem.suggest = [
  134. {
  135. messageId: SUGGESTION_REMOVE,
  136. fix: removeThisArgument(callExpression, sourceCode),
  137. },
  138. {
  139. messageId: SUGGESTION_BIND,
  140. fix: useBoundFunction(callExpression, sourceCode),
  141. },
  142. ];
  143. return problem;
  144. },
  145. };
  146. };
  147. /** @type {import('eslint').Rule.RuleModule} */
  148. module.exports = {
  149. create,
  150. meta: {
  151. type: 'suggestion',
  152. docs: {
  153. description: 'Disallow using the `this` argument in array methods.',
  154. },
  155. fixable: 'code',
  156. hasSuggestions: true,
  157. messages,
  158. },
  159. };