UNPKG

11.2 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to suggest using arrow functions as callbacks.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
8//------------------------------------------------------------------------------
9// Helpers
10//------------------------------------------------------------------------------
11
12/**
13 * Checks whether or not a given variable is a function name.
14 * @param {eslint-scope.Variable} variable - A variable to check.
15 * @returns {boolean} `true` if the variable is a function name.
16 */
17function isFunctionName(variable) {
18 return variable && variable.defs[0].type === "FunctionName";
19}
20
21/**
22 * Checks whether or not a given MetaProperty node equals to a given value.
23 * @param {ASTNode} node - A MetaProperty node to check.
24 * @param {string} metaName - The name of `MetaProperty.meta`.
25 * @param {string} propertyName - The name of `MetaProperty.property`.
26 * @returns {boolean} `true` if the node is the specific value.
27 */
28function checkMetaProperty(node, metaName, propertyName) {
29 return node.meta.name === metaName && node.property.name === propertyName;
30}
31
32/**
33 * Gets the variable object of `arguments` which is defined implicitly.
34 * @param {eslint-scope.Scope} scope - A scope to get.
35 * @returns {eslint-scope.Variable} The found variable object.
36 */
37function getVariableOfArguments(scope) {
38 const variables = scope.variables;
39
40 for (let i = 0; i < variables.length; ++i) {
41 const variable = variables[i];
42
43 if (variable.name === "arguments") {
44
45 /*
46 * If there was a parameter which is named "arguments", the
47 * implicit "arguments" is not defined.
48 * So does fast return with null.
49 */
50 return (variable.identifiers.length === 0) ? variable : null;
51 }
52 }
53
54 /* istanbul ignore next */
55 return null;
56}
57
58/**
59 * Checkes whether or not a given node is a callback.
60 * @param {ASTNode} node - A node to check.
61 * @returns {Object}
62 * {boolean} retv.isCallback - `true` if the node is a callback.
63 * {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`.
64 */
65function getCallbackInfo(node) {
66 const retv = { isCallback: false, isLexicalThis: false };
67 let parent = node.parent;
68
69 while (node) {
70 switch (parent.type) {
71
72 // Checks parents recursively.
73
74 case "LogicalExpression":
75 case "ConditionalExpression":
76 break;
77
78 // Checks whether the parent node is `.bind(this)` call.
79 case "MemberExpression":
80 if (parent.object === node &&
81 !parent.property.computed &&
82 parent.property.type === "Identifier" &&
83 parent.property.name === "bind" &&
84 parent.parent.type === "CallExpression" &&
85 parent.parent.callee === parent
86 ) {
87 retv.isLexicalThis = (
88 parent.parent.arguments.length === 1 &&
89 parent.parent.arguments[0].type === "ThisExpression"
90 );
91 node = parent;
92 parent = parent.parent;
93 } else {
94 return retv;
95 }
96 break;
97
98 // Checks whether the node is a callback.
99 case "CallExpression":
100 case "NewExpression":
101 if (parent.callee !== node) {
102 retv.isCallback = true;
103 }
104 return retv;
105
106 default:
107 return retv;
108 }
109
110 node = parent;
111 parent = parent.parent;
112 }
113
114 /* istanbul ignore next */
115 throw new Error("unreachable");
116}
117
118/**
119* Checks whether a simple list of parameters contains any duplicates. This does not handle complex
120parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate
121parameter names anyway. Instead, it always returns `false` for complex parameter lists.
122* @param {ASTNode[]} paramsList The list of parameters for a function
123* @returns {boolean} `true` if the list of parameters contains any duplicates
124*/
125function hasDuplicateParams(paramsList) {
126 return paramsList.every(param => param.type === "Identifier") && paramsList.length !== new Set(paramsList.map(param => param.name)).size;
127}
128
129//------------------------------------------------------------------------------
130// Rule Definition
131//------------------------------------------------------------------------------
132
133module.exports = {
134 meta: {
135 docs: {
136 description: "require arrow functions as callbacks",
137 category: "ECMAScript 6",
138 recommended: false
139 },
140
141 schema: [
142 {
143 type: "object",
144 properties: {
145 allowNamedFunctions: {
146 type: "boolean"
147 },
148 allowUnboundThis: {
149 type: "boolean"
150 }
151 },
152 additionalProperties: false
153 }
154 ],
155
156 fixable: "code"
157 },
158
159 create(context) {
160 const options = context.options[0] || {};
161
162 const allowUnboundThis = options.allowUnboundThis !== false; // default to true
163 const allowNamedFunctions = options.allowNamedFunctions;
164 const sourceCode = context.getSourceCode();
165
166 /*
167 * {Array<{this: boolean, super: boolean, meta: boolean}>}
168 * - this - A flag which shows there are one or more ThisExpression.
169 * - super - A flag which shows there are one or more Super.
170 * - meta - A flag which shows there are one or more MethProperty.
171 */
172 let stack = [];
173
174 /**
175 * Pushes new function scope with all `false` flags.
176 * @returns {void}
177 */
178 function enterScope() {
179 stack.push({ this: false, super: false, meta: false });
180 }
181
182 /**
183 * Pops a function scope from the stack.
184 * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
185 */
186 function exitScope() {
187 return stack.pop();
188 }
189
190 return {
191
192 // Reset internal state.
193 Program() {
194 stack = [];
195 },
196
197 // If there are below, it cannot replace with arrow functions merely.
198 ThisExpression() {
199 const info = stack[stack.length - 1];
200
201 if (info) {
202 info.this = true;
203 }
204 },
205
206 Super() {
207 const info = stack[stack.length - 1];
208
209 if (info) {
210 info.super = true;
211 }
212 },
213
214 MetaProperty(node) {
215 const info = stack[stack.length - 1];
216
217 if (info && checkMetaProperty(node, "new", "target")) {
218 info.meta = true;
219 }
220 },
221
222 // To skip nested scopes.
223 FunctionDeclaration: enterScope,
224 "FunctionDeclaration:exit": exitScope,
225
226 // Main.
227 FunctionExpression: enterScope,
228 "FunctionExpression:exit"(node) {
229 const scopeInfo = exitScope();
230
231 // Skip named function expressions
232 if (allowNamedFunctions && node.id && node.id.name) {
233 return;
234 }
235
236 // Skip generators.
237 if (node.generator) {
238 return;
239 }
240
241 // Skip recursive functions.
242 const nameVar = context.getDeclaredVariables(node)[0];
243
244 if (isFunctionName(nameVar) && nameVar.references.length > 0) {
245 return;
246 }
247
248 // Skip if it's using arguments.
249 const variable = getVariableOfArguments(context.getScope());
250
251 if (variable && variable.references.length > 0) {
252 return;
253 }
254
255 // Reports if it's a callback which can replace with arrows.
256 const callbackInfo = getCallbackInfo(node);
257
258 if (callbackInfo.isCallback &&
259 (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) &&
260 !scopeInfo.super &&
261 !scopeInfo.meta
262 ) {
263 context.report({
264 node,
265 message: "Unexpected function expression.",
266 fix(fixer) {
267 if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) {
268
269 // If the callback function does not have .bind(this) and contains a reference to `this`, there
270 // is no way to determine what `this` should be, so don't perform any fixes.
271 // If the callback function has duplicates in its list of parameters (possible in sloppy mode),
272 // don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
273 return null;
274 }
275
276 const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1);
277 const paramsRightParen = sourceCode.getTokenBefore(node.body);
278 const asyncKeyword = node.async ? "async " : "";
279 const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
280 const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
281
282 /*
283 * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
284 * Otherwise, just replace the arrow function itself.
285 */
286 const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
287
288 /*
289 * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
290 * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
291 * though `foo || function() {}` is valid.
292 */
293 const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression";
294 const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
295
296 return fixer.replaceText(replacedNode, replacementText);
297 }
298 });
299 }
300 }
301 };
302 }
303};