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