no-extra-bind.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. * @fileoverview Rule to flag unnecessary bind calls
  3. * @author Bence Dányi <bence@danyi.me>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. const SIDE_EFFECT_FREE_NODE_TYPES = new Set(["Literal", "Identifier", "ThisExpression", "FunctionExpression"]);
  14. //------------------------------------------------------------------------------
  15. // Rule Definition
  16. //------------------------------------------------------------------------------
  17. /** @type {import('../shared/types').Rule} */
  18. module.exports = {
  19. meta: {
  20. type: "suggestion",
  21. docs: {
  22. description: "Disallow unnecessary calls to `.bind()`",
  23. recommended: false,
  24. url: "https://eslint.org/docs/rules/no-extra-bind"
  25. },
  26. schema: [],
  27. fixable: "code",
  28. messages: {
  29. unexpected: "The function binding is unnecessary."
  30. }
  31. },
  32. create(context) {
  33. const sourceCode = context.getSourceCode();
  34. let scopeInfo = null;
  35. /**
  36. * Checks if a node is free of side effects.
  37. *
  38. * This check is stricter than it needs to be, in order to keep the implementation simple.
  39. * @param {ASTNode} node A node to check.
  40. * @returns {boolean} True if the node is known to be side-effect free, false otherwise.
  41. */
  42. function isSideEffectFree(node) {
  43. return SIDE_EFFECT_FREE_NODE_TYPES.has(node.type);
  44. }
  45. /**
  46. * Reports a given function node.
  47. * @param {ASTNode} node A node to report. This is a FunctionExpression or
  48. * an ArrowFunctionExpression.
  49. * @returns {void}
  50. */
  51. function report(node) {
  52. const memberNode = node.parent;
  53. const callNode = memberNode.parent.type === "ChainExpression"
  54. ? memberNode.parent.parent
  55. : memberNode.parent;
  56. context.report({
  57. node: callNode,
  58. messageId: "unexpected",
  59. loc: memberNode.property.loc,
  60. fix(fixer) {
  61. if (!isSideEffectFree(callNode.arguments[0])) {
  62. return null;
  63. }
  64. /*
  65. * The list of the first/last token pair of a removal range.
  66. * This is two parts because closing parentheses may exist between the method name and arguments.
  67. * E.g. `(function(){}.bind ) (obj)`
  68. * ^^^^^ ^^^^^ < removal ranges
  69. * E.g. `(function(){}?.['bind'] ) ?.(obj)`
  70. * ^^^^^^^^^^ ^^^^^^^ < removal ranges
  71. */
  72. const tokenPairs = [
  73. [
  74. // `.`, `?.`, or `[` token.
  75. sourceCode.getTokenAfter(
  76. memberNode.object,
  77. astUtils.isNotClosingParenToken
  78. ),
  79. // property name or `]` token.
  80. sourceCode.getLastToken(memberNode)
  81. ],
  82. [
  83. // `?.` or `(` token of arguments.
  84. sourceCode.getTokenAfter(
  85. memberNode,
  86. astUtils.isNotClosingParenToken
  87. ),
  88. // `)` token of arguments.
  89. sourceCode.getLastToken(callNode)
  90. ]
  91. ];
  92. const firstTokenToRemove = tokenPairs[0][0];
  93. const lastTokenToRemove = tokenPairs[1][1];
  94. if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) {
  95. return null;
  96. }
  97. return tokenPairs.map(([start, end]) =>
  98. fixer.removeRange([start.range[0], end.range[1]]));
  99. }
  100. });
  101. }
  102. /**
  103. * Checks whether or not a given function node is the callee of `.bind()`
  104. * method.
  105. *
  106. * e.g. `(function() {}.bind(foo))`
  107. * @param {ASTNode} node A node to report. This is a FunctionExpression or
  108. * an ArrowFunctionExpression.
  109. * @returns {boolean} `true` if the node is the callee of `.bind()` method.
  110. */
  111. function isCalleeOfBindMethod(node) {
  112. if (!astUtils.isSpecificMemberAccess(node.parent, null, "bind")) {
  113. return false;
  114. }
  115. // The node of `*.bind` member access.
  116. const bindNode = node.parent.parent.type === "ChainExpression"
  117. ? node.parent.parent
  118. : node.parent;
  119. return (
  120. bindNode.parent.type === "CallExpression" &&
  121. bindNode.parent.callee === bindNode &&
  122. bindNode.parent.arguments.length === 1 &&
  123. bindNode.parent.arguments[0].type !== "SpreadElement"
  124. );
  125. }
  126. /**
  127. * Adds a scope information object to the stack.
  128. * @param {ASTNode} node A node to add. This node is a FunctionExpression
  129. * or a FunctionDeclaration node.
  130. * @returns {void}
  131. */
  132. function enterFunction(node) {
  133. scopeInfo = {
  134. isBound: isCalleeOfBindMethod(node),
  135. thisFound: false,
  136. upper: scopeInfo
  137. };
  138. }
  139. /**
  140. * Removes the scope information object from the top of the stack.
  141. * At the same time, this reports the function node if the function has
  142. * `.bind()` and the `this` keywords found.
  143. * @param {ASTNode} node A node to remove. This node is a
  144. * FunctionExpression or a FunctionDeclaration node.
  145. * @returns {void}
  146. */
  147. function exitFunction(node) {
  148. if (scopeInfo.isBound && !scopeInfo.thisFound) {
  149. report(node);
  150. }
  151. scopeInfo = scopeInfo.upper;
  152. }
  153. /**
  154. * Reports a given arrow function if the function is callee of `.bind()`
  155. * method.
  156. * @param {ASTNode} node A node to report. This node is an
  157. * ArrowFunctionExpression.
  158. * @returns {void}
  159. */
  160. function exitArrowFunction(node) {
  161. if (isCalleeOfBindMethod(node)) {
  162. report(node);
  163. }
  164. }
  165. /**
  166. * Set the mark as the `this` keyword was found in this scope.
  167. * @returns {void}
  168. */
  169. function markAsThisFound() {
  170. if (scopeInfo) {
  171. scopeInfo.thisFound = true;
  172. }
  173. }
  174. return {
  175. "ArrowFunctionExpression:exit": exitArrowFunction,
  176. FunctionDeclaration: enterFunction,
  177. "FunctionDeclaration:exit": exitFunction,
  178. FunctionExpression: enterFunction,
  179. "FunctionExpression:exit": exitFunction,
  180. ThisExpression: markAsThisFound
  181. };
  182. }
  183. };