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