minor adjustment to readme
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / rules / prefer-arrow-callback.js
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  */
17 function 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  */
28 function 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  */
37 function 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  */
65 function getCallbackInfo(node) {
66     const retv = { isCallback: false, isLexicalThis: false };
67     let currentNode = node;
68     let parent = node.parent;
69
70     while (currentNode) {
71         switch (parent.type) {
72
73             // Checks parents recursively.
74
75             case "LogicalExpression":
76             case "ConditionalExpression":
77                 break;
78
79             // Checks whether the parent node is `.bind(this)` call.
80             case "MemberExpression":
81                 if (parent.object === currentNode &&
82                     !parent.property.computed &&
83                     parent.property.type === "Identifier" &&
84                     parent.property.name === "bind" &&
85                     parent.parent.type === "CallExpression" &&
86                     parent.parent.callee === parent
87                 ) {
88                     retv.isLexicalThis = (
89                         parent.parent.arguments.length === 1 &&
90                         parent.parent.arguments[0].type === "ThisExpression"
91                     );
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 !== currentNode) {
102                     retv.isCallback = true;
103                 }
104                 return retv;
105
106             default:
107                 return retv;
108         }
109
110         currentNode = 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
120  * parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate
121  * parameter 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  */
125 function 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
133 module.exports = {
134     meta: {
135         type: "suggestion",
136
137         docs: {
138             description: "require using arrow functions for callbacks",
139             category: "ECMAScript 6",
140             recommended: false,
141             url: "https://eslint.org/docs/rules/prefer-arrow-callback"
142         },
143
144         schema: [
145             {
146                 type: "object",
147                 properties: {
148                     allowNamedFunctions: {
149                         type: "boolean",
150                         default: false
151                     },
152                     allowUnboundThis: {
153                         type: "boolean",
154                         default: true
155                     }
156                 },
157                 additionalProperties: false
158             }
159         ],
160
161         fixable: "code"
162     },
163
164     create(context) {
165         const options = context.options[0] || {};
166
167         const allowUnboundThis = options.allowUnboundThis !== false; // default to true
168         const allowNamedFunctions = options.allowNamedFunctions;
169         const sourceCode = context.getSourceCode();
170
171         /*
172          * {Array<{this: boolean, super: boolean, meta: boolean}>}
173          * - this - A flag which shows there are one or more ThisExpression.
174          * - super - A flag which shows there are one or more Super.
175          * - meta - A flag which shows there are one or more MethProperty.
176          */
177         let stack = [];
178
179         /**
180          * Pushes new function scope with all `false` flags.
181          * @returns {void}
182          */
183         function enterScope() {
184             stack.push({ this: false, super: false, meta: false });
185         }
186
187         /**
188          * Pops a function scope from the stack.
189          * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
190          */
191         function exitScope() {
192             return stack.pop();
193         }
194
195         return {
196
197             // Reset internal state.
198             Program() {
199                 stack = [];
200             },
201
202             // If there are below, it cannot replace with arrow functions merely.
203             ThisExpression() {
204                 const info = stack[stack.length - 1];
205
206                 if (info) {
207                     info.this = true;
208                 }
209             },
210
211             Super() {
212                 const info = stack[stack.length - 1];
213
214                 if (info) {
215                     info.super = true;
216                 }
217             },
218
219             MetaProperty(node) {
220                 const info = stack[stack.length - 1];
221
222                 if (info && checkMetaProperty(node, "new", "target")) {
223                     info.meta = true;
224                 }
225             },
226
227             // To skip nested scopes.
228             FunctionDeclaration: enterScope,
229             "FunctionDeclaration:exit": exitScope,
230
231             // Main.
232             FunctionExpression: enterScope,
233             "FunctionExpression:exit"(node) {
234                 const scopeInfo = exitScope();
235
236                 // Skip named function expressions
237                 if (allowNamedFunctions && node.id && node.id.name) {
238                     return;
239                 }
240
241                 // Skip generators.
242                 if (node.generator) {
243                     return;
244                 }
245
246                 // Skip recursive functions.
247                 const nameVar = context.getDeclaredVariables(node)[0];
248
249                 if (isFunctionName(nameVar) && nameVar.references.length > 0) {
250                     return;
251                 }
252
253                 // Skip if it's using arguments.
254                 const variable = getVariableOfArguments(context.getScope());
255
256                 if (variable && variable.references.length > 0) {
257                     return;
258                 }
259
260                 // Reports if it's a callback which can replace with arrows.
261                 const callbackInfo = getCallbackInfo(node);
262
263                 if (callbackInfo.isCallback &&
264                     (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) &&
265                     !scopeInfo.super &&
266                     !scopeInfo.meta
267                 ) {
268                     context.report({
269                         node,
270                         message: "Unexpected function expression.",
271                         fix(fixer) {
272                             if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) {
273
274                                 /*
275                                  * If the callback function does not have .bind(this) and contains a reference to `this`, there
276                                  * is no way to determine what `this` should be, so don't perform any fixes.
277                                  * If the callback function has duplicates in its list of parameters (possible in sloppy mode),
278                                  * don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
279                                  */
280                                 return null;
281                             }
282
283                             const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1);
284                             const paramsRightParen = sourceCode.getTokenBefore(node.body);
285                             const asyncKeyword = node.async ? "async " : "";
286                             const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
287                             const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
288
289                             /*
290                              * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
291                              * Otherwise, just replace the arrow function itself.
292                              */
293                             const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
294
295                             /*
296                              * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
297                              * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
298                              * though `foo || function() {}` is valid.
299                              */
300                             const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression";
301                             const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
302
303                             return fixer.replaceText(replacedNode, replacementText);
304                         }
305                     });
306                 }
307             }
308         };
309     }
310 };