]>
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 | ||
34eeec05 | 23 | /** @type {import('../shared/types').Rule} */ |
eb39fafa DC |
24 | module.exports = { |
25 | meta: { | |
26 | type: "suggestion", | |
27 | ||
28 | docs: { | |
8f9d1d4d | 29 | description: "Disallow unnecessary calls to `.bind()`", |
eb39fafa DC |
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) { | |
6f036462 TL |
64 | const memberNode = node.parent; |
65 | const callNode = memberNode.parent.type === "ChainExpression" | |
66 | ? memberNode.parent.parent | |
67 | : memberNode.parent; | |
68 | ||
eb39fafa | 69 | context.report({ |
6f036462 | 70 | node: callNode, |
eb39fafa | 71 | messageId: "unexpected", |
6f036462 TL |
72 | loc: memberNode.property.loc, |
73 | ||
eb39fafa | 74 | fix(fixer) { |
6f036462 | 75 | if (!isSideEffectFree(callNode.arguments[0])) { |
eb39fafa DC |
76 | return null; |
77 | } | |
78 | ||
6f036462 TL |
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]; | |
eb39fafa DC |
113 | |
114 | if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) { | |
115 | return null; | |
116 | } | |
117 | ||
6f036462 TL |
118 | return tokenPairs.map(([start, end]) => |
119 | fixer.removeRange([start.range[0], end.range[1]])); | |
eb39fafa DC |
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) { | |
6f036462 TL |
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; | |
eb39fafa DC |
142 | |
143 | return ( | |
6f036462 TL |
144 | bindNode.parent.type === "CallExpression" && |
145 | bindNode.parent.callee === bindNode && | |
146 | bindNode.parent.arguments.length === 1 && | |
147 | bindNode.parent.arguments[0].type !== "SpreadElement" | |
eb39fafa DC |
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 | }; |