no-extra-bind.js 5.9 KB

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