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 | ;
|
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 | */
|
18 | function 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 | */
|
29 | function 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 | */
|
42 | function 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 | */
|
64 | function 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 |
|
118 | module.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 |
|
206 | module.exports.schema = [];
|