UNPKG

6.85 kBJavaScriptView Raw
1/**
2 * @fileoverview A rule to suggest using arrow functions as callbacks.
3 * @author Toru Nagashima
4 * @copyright 2015 Toru Nagashima. All rights reserved.
5 */
6
7"use strict";
8
9//------------------------------------------------------------------------------
10// Helpers
11//------------------------------------------------------------------------------
12
13/**
14 * Checks whether or not a given variable is a function name.
15 * @param {escope.Variable} variable - A variable to check.
16 * @returns {boolean} `true` if the variable is a function name.
17 */
18function isFunctionName(variable) {
19 return variable && variable.defs[0].type === "FunctionName";
20}
21
22/**
23 * Checks whether or not a given MetaProperty node equals to a given value.
24 * @param {ASTNode} node - A MetaProperty node to check.
25 * @param {string} metaName - The name of `MetaProperty.meta`.
26 * @param {string} propertyName - The name of `MetaProperty.property`.
27 * @returns {boolean} `true` if the node is the specific value.
28 */
29function checkMetaProperty(node, metaName, propertyName) {
30 // TODO: Remove this if block after https://github.com/eslint/espree/issues/206 was fixed.
31 if (typeof node.meta === "string") {
32 return node.meta === metaName && node.property === propertyName;
33 }
34 return node.meta.name === metaName && node.property.name === propertyName;
35}
36
37/**
38 * Gets the variable object of `arguments` which is defined implicitly.
39 * @param {escope.Scope} scope - A scope to get.
40 * @returns {escope.Variable} The found variable object.
41 */
42function getVariableOfArguments(scope) {
43 var variables = scope.variables;
44 for (var i = 0; i < variables.length; ++i) {
45 var variable = variables[i];
46 if (variable.name === "arguments") {
47 // If there was a parameter which is named "arguments", the implicit "arguments" is not defined.
48 // So does fast return with null.
49 return (variable.identifiers.length === 0) ? variable : null;
50 }
51 }
52
53 /* istanbul ignore next */
54 return null;
55}
56
57/**
58 * Checkes whether or not a given node is a callback.
59 * @param {ASTNode} node - A node to check.
60 * @returns {object}
61 * {boolean} retv.isCallback - `true` if the node is a callback.
62 * {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`.
63 */
64function getCallbackInfo(node) {
65 var retv = {isCallback: false, isLexicalThis: false};
66 var parent = node.parent;
67 while (node) {
68 switch (parent.type) {
69 // Checks parents recursively.
70 case "LogicalExpression":
71 case "ConditionalExpression":
72 break;
73
74 // Checks whether the parent node is `.bind(this)` call.
75 case "MemberExpression":
76 if (parent.object === node &&
77 !parent.property.computed &&
78 parent.property.type === "Identifier" &&
79 parent.property.name === "bind" &&
80 parent.parent.type === "CallExpression" &&
81 parent.parent.callee === parent
82 ) {
83 retv.isLexicalThis = (
84 parent.parent.arguments.length === 1 &&
85 parent.parent.arguments[0].type === "ThisExpression"
86 );
87 node = parent;
88 parent = parent.parent;
89 } else {
90 return retv;
91 }
92 break;
93
94 // Checks whether the node is a callback.
95 case "CallExpression":
96 case "NewExpression":
97 if (parent.callee !== node) {
98 retv.isCallback = true;
99 }
100 return retv;
101
102 default:
103 return retv;
104 }
105
106 node = parent;
107 parent = parent.parent;
108 }
109
110 /* istanbul ignore next */
111 throw new Error("unreachable");
112}
113
114//------------------------------------------------------------------------------
115// Rule Definition
116//------------------------------------------------------------------------------
117
118module.exports = function(context) {
119 // {Array<{this: boolean, super: boolean, meta: boolean}>}
120 // - this - A flag which shows there are one or more ThisExpression.
121 // - super - A flag which shows there are one or more Super.
122 // - meta - A flag which shows there are one or more MethProperty.
123 var stack = [];
124
125 /**
126 * Pushes new function scope with all `false` flags.
127 * @returns {void}
128 */
129 function enterScope() {
130 stack.push({this: false, super: false, meta: false});
131 }
132
133 /**
134 * Pops a function scope from the stack.
135 * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
136 */
137 function exitScope() {
138 return stack.pop();
139 }
140
141 return {
142 // Reset internal state.
143 Program: function() {
144 stack = [];
145 },
146
147 // If there are below, it cannot replace with arrow functions merely.
148 ThisExpression: function() {
149 var info = stack[stack.length - 1];
150 if (info) {
151 info.this = true;
152 }
153 },
154 Super: function() {
155 var info = stack[stack.length - 1];
156 if (info) {
157 info.super = true;
158 }
159 },
160 MetaProperty: function(node) {
161 var info = stack[stack.length - 1];
162 if (info && checkMetaProperty(node, "new", "target")) {
163 info.meta = true;
164 }
165 },
166
167 // To skip nested scopes.
168 FunctionDeclaration: enterScope,
169 "FunctionDeclaration:exit": exitScope,
170
171 // Main.
172 FunctionExpression: enterScope,
173 "FunctionExpression:exit": function(node) {
174 var scopeInfo = exitScope();
175
176 // Skip generators.
177 if (node.generator) {
178 return;
179 }
180
181 // Skip recursive functions.
182 var nameVar = context.getDeclaredVariables(node)[0];
183 if (isFunctionName(nameVar) && nameVar.references.length > 0) {
184 return;
185 }
186
187 // Skip if it's using arguments.
188 var variable = getVariableOfArguments(context.getScope());
189 if (variable && variable.references.length > 0) {
190 return;
191 }
192
193 // Reports if it's a callback which can replace with arrows.
194 var callbackInfo = getCallbackInfo(node);
195 if (callbackInfo.isCallback &&
196 (!scopeInfo.this || callbackInfo.isLexicalThis) &&
197 !scopeInfo.super &&
198 !scopeInfo.meta
199 ) {
200 context.report(node, "Unexpected function expression.");
201 }
202 }
203 };
204};
205
206module.exports.schema = [];