]> git.proxmox.com Git - pve-eslint.git/blame - eslint/lib/rules/prefer-arrow-callback.js
bump version to 8.41.0-3
[pve-eslint.git] / eslint / lib / rules / prefer-arrow-callback.js
CommitLineData
eb39fafa
DC
1/**
2 * @fileoverview A rule to suggest using arrow functions as callbacks.
3 * @author Toru Nagashima
4 */
5
6"use strict";
7
6f036462
TL
8const astUtils = require("./utils/ast-utils");
9
eb39fafa
DC
10//------------------------------------------------------------------------------
11// Helpers
12//------------------------------------------------------------------------------
13
14/**
15 * Checks whether or not a given variable is a function name.
16 * @param {eslint-scope.Variable} variable A variable to check.
17 * @returns {boolean} `true` if the variable is a function name.
18 */
19function isFunctionName(variable) {
20 return variable && variable.defs[0].type === "FunctionName";
21}
22
23/**
24 * Checks whether or not a given MetaProperty node equals to a given value.
25 * @param {ASTNode} node A MetaProperty node to check.
26 * @param {string} metaName The name of `MetaProperty.meta`.
27 * @param {string} propertyName The name of `MetaProperty.property`.
28 * @returns {boolean} `true` if the node is the specific value.
29 */
30function checkMetaProperty(node, metaName, propertyName) {
31 return node.meta.name === metaName && node.property.name === propertyName;
32}
33
34/**
35 * Gets the variable object of `arguments` which is defined implicitly.
36 * @param {eslint-scope.Scope} scope A scope to get.
37 * @returns {eslint-scope.Variable} The found variable object.
38 */
39function getVariableOfArguments(scope) {
40 const variables = scope.variables;
41
42 for (let i = 0; i < variables.length; ++i) {
43 const variable = variables[i];
44
45 if (variable.name === "arguments") {
46
47 /*
48 * If there was a parameter which is named "arguments", the
49 * implicit "arguments" is not defined.
50 * So does fast return with null.
51 */
52 return (variable.identifiers.length === 0) ? variable : null;
53 }
54 }
55
8f9d1d4d 56 /* c8 ignore next */
eb39fafa
DC
57 return null;
58}
59
60/**
61 * Checks whether or not a given node is a callback.
62 * @param {ASTNode} node A node to check.
609c276f 63 * @throws {Error} (Unreachable.)
eb39fafa
DC
64 * @returns {Object}
65 * {boolean} retv.isCallback - `true` if the node is a callback.
66 * {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`.
67 */
68function getCallbackInfo(node) {
69 const retv = { isCallback: false, isLexicalThis: false };
70 let currentNode = node;
71 let parent = node.parent;
6f036462 72 let bound = false;
eb39fafa
DC
73
74 while (currentNode) {
75 switch (parent.type) {
76
77 // Checks parents recursively.
78
79 case "LogicalExpression":
6f036462 80 case "ChainExpression":
eb39fafa
DC
81 case "ConditionalExpression":
82 break;
83
84 // Checks whether the parent node is `.bind(this)` call.
85 case "MemberExpression":
6f036462
TL
86 if (
87 parent.object === currentNode &&
eb39fafa
DC
88 !parent.property.computed &&
89 parent.property.type === "Identifier" &&
6f036462 90 parent.property.name === "bind"
eb39fafa 91 ) {
6f036462
TL
92 const maybeCallee = parent.parent.type === "ChainExpression"
93 ? parent.parent
94 : parent;
95
96 if (astUtils.isCallee(maybeCallee)) {
97 if (!bound) {
98 bound = true; // Use only the first `.bind()` to make `isLexicalThis` value.
99 retv.isLexicalThis = (
100 maybeCallee.parent.arguments.length === 1 &&
101 maybeCallee.parent.arguments[0].type === "ThisExpression"
102 );
103 }
104 parent = maybeCallee.parent;
105 } else {
106 return retv;
107 }
eb39fafa
DC
108 } else {
109 return retv;
110 }
111 break;
112
113 // Checks whether the node is a callback.
114 case "CallExpression":
115 case "NewExpression":
116 if (parent.callee !== currentNode) {
117 retv.isCallback = true;
118 }
119 return retv;
120
121 default:
122 return retv;
123 }
124
125 currentNode = parent;
126 parent = parent.parent;
127 }
128
8f9d1d4d 129 /* c8 ignore next */
eb39fafa
DC
130 throw new Error("unreachable");
131}
132
133/**
134 * Checks whether a simple list of parameters contains any duplicates. This does not handle complex
135 * parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate
136 * parameter names anyway. Instead, it always returns `false` for complex parameter lists.
137 * @param {ASTNode[]} paramsList The list of parameters for a function
138 * @returns {boolean} `true` if the list of parameters contains any duplicates
139 */
140function hasDuplicateParams(paramsList) {
141 return paramsList.every(param => param.type === "Identifier") && paramsList.length !== new Set(paramsList.map(param => param.name)).size;
142}
143
144//------------------------------------------------------------------------------
145// Rule Definition
146//------------------------------------------------------------------------------
147
34eeec05 148/** @type {import('../shared/types').Rule} */
eb39fafa
DC
149module.exports = {
150 meta: {
151 type: "suggestion",
152
153 docs: {
8f9d1d4d 154 description: "Require using arrow functions for callbacks",
eb39fafa 155 recommended: false,
f2a92ac6 156 url: "https://eslint.org/docs/latest/rules/prefer-arrow-callback"
eb39fafa
DC
157 },
158
159 schema: [
160 {
161 type: "object",
162 properties: {
163 allowNamedFunctions: {
164 type: "boolean",
165 default: false
166 },
167 allowUnboundThis: {
168 type: "boolean",
169 default: true
170 }
171 },
172 additionalProperties: false
173 }
174 ],
175
176 fixable: "code",
177
178 messages: {
179 preferArrowCallback: "Unexpected function expression."
180 }
181 },
182
183 create(context) {
184 const options = context.options[0] || {};
185
186 const allowUnboundThis = options.allowUnboundThis !== false; // default to true
187 const allowNamedFunctions = options.allowNamedFunctions;
f2a92ac6 188 const sourceCode = context.sourceCode;
eb39fafa
DC
189
190 /*
191 * {Array<{this: boolean, super: boolean, meta: boolean}>}
192 * - this - A flag which shows there are one or more ThisExpression.
193 * - super - A flag which shows there are one or more Super.
194 * - meta - A flag which shows there are one or more MethProperty.
195 */
196 let stack = [];
197
198 /**
199 * Pushes new function scope with all `false` flags.
200 * @returns {void}
201 */
202 function enterScope() {
203 stack.push({ this: false, super: false, meta: false });
204 }
205
206 /**
207 * Pops a function scope from the stack.
208 * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
209 */
210 function exitScope() {
211 return stack.pop();
212 }
213
214 return {
215
216 // Reset internal state.
217 Program() {
218 stack = [];
219 },
220
221 // If there are below, it cannot replace with arrow functions merely.
222 ThisExpression() {
223 const info = stack[stack.length - 1];
224
225 if (info) {
226 info.this = true;
227 }
228 },
229
230 Super() {
231 const info = stack[stack.length - 1];
232
233 if (info) {
234 info.super = true;
235 }
236 },
237
238 MetaProperty(node) {
239 const info = stack[stack.length - 1];
240
241 if (info && checkMetaProperty(node, "new", "target")) {
242 info.meta = true;
243 }
244 },
245
246 // To skip nested scopes.
247 FunctionDeclaration: enterScope,
248 "FunctionDeclaration:exit": exitScope,
249
250 // Main.
251 FunctionExpression: enterScope,
252 "FunctionExpression:exit"(node) {
253 const scopeInfo = exitScope();
254
255 // Skip named function expressions
256 if (allowNamedFunctions && node.id && node.id.name) {
257 return;
258 }
259
260 // Skip generators.
261 if (node.generator) {
262 return;
263 }
264
265 // Skip recursive functions.
f2a92ac6 266 const nameVar = sourceCode.getDeclaredVariables(node)[0];
eb39fafa
DC
267
268 if (isFunctionName(nameVar) && nameVar.references.length > 0) {
269 return;
270 }
271
272 // Skip if it's using arguments.
f2a92ac6 273 const variable = getVariableOfArguments(sourceCode.getScope(node));
eb39fafa
DC
274
275 if (variable && variable.references.length > 0) {
276 return;
277 }
278
279 // Reports if it's a callback which can replace with arrows.
280 const callbackInfo = getCallbackInfo(node);
281
282 if (callbackInfo.isCallback &&
283 (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) &&
284 !scopeInfo.super &&
285 !scopeInfo.meta
286 ) {
287 context.report({
288 node,
289 messageId: "preferArrowCallback",
6f036462 290 *fix(fixer) {
eb39fafa
DC
291 if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) {
292
293 /*
294 * If the callback function does not have .bind(this) and contains a reference to `this`, there
295 * is no way to determine what `this` should be, so don't perform any fixes.
296 * If the callback function has duplicates in its list of parameters (possible in sloppy mode),
297 * don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
298 */
609c276f 299 return;
eb39fafa
DC
300 }
301
6f036462
TL
302 // Remove `.bind(this)` if exists.
303 if (callbackInfo.isLexicalThis) {
304 const memberNode = node.parent;
eb39fafa 305
6f036462
TL
306 /*
307 * If `.bind(this)` exists but the parent is not `.bind(this)`, don't remove it automatically.
308 * E.g. `(foo || function(){}).bind(this)`
309 */
310 if (memberNode.type !== "MemberExpression") {
609c276f 311 return;
6f036462
TL
312 }
313
314 const callNode = memberNode.parent;
315 const firstTokenToRemove = sourceCode.getTokenAfter(memberNode.object, astUtils.isNotClosingParenToken);
316 const lastTokenToRemove = sourceCode.getLastToken(callNode);
317
318 /*
319 * If the member expression is parenthesized, don't remove the right paren.
320 * E.g. `(function(){}.bind)(this)`
321 * ^^^^^^^^^^^^
322 */
323 if (astUtils.isParenthesised(sourceCode, memberNode)) {
609c276f 324 return;
6f036462
TL
325 }
326
327 // If comments exist in the `.bind(this)`, don't remove those.
328 if (sourceCode.commentsExistBetween(firstTokenToRemove, lastTokenToRemove)) {
609c276f 329 return;
6f036462
TL
330 }
331
332 yield fixer.removeRange([firstTokenToRemove.range[0], lastTokenToRemove.range[1]]);
333 }
334
335 // Convert the function expression to an arrow function.
336 const functionToken = sourceCode.getFirstToken(node, node.async ? 1 : 0);
337 const leftParenToken = sourceCode.getTokenAfter(functionToken, astUtils.isOpeningParenToken);
f2a92ac6 338 const tokenBeforeBody = sourceCode.getTokenBefore(node.body);
6f036462
TL
339
340 if (sourceCode.commentsExistBetween(functionToken, leftParenToken)) {
341
342 // Remove only extra tokens to keep comments.
343 yield fixer.remove(functionToken);
344 if (node.id) {
345 yield fixer.remove(node.id);
346 }
347 } else {
348
349 // Remove extra tokens and spaces.
350 yield fixer.removeRange([functionToken.range[0], leftParenToken.range[0]]);
351 }
f2a92ac6 352 yield fixer.insertTextAfter(tokenBeforeBody, " =>");
6f036462
TL
353
354 // Get the node that will become the new arrow function.
355 let replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
356
357 if (replacedNode.type === "ChainExpression") {
358 replacedNode = replacedNode.parent;
359 }
eb39fafa
DC
360
361 /*
362 * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
363 * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
364 * though `foo || function() {}` is valid.
365 */
6f036462
TL
366 if (
367 replacedNode.parent.type !== "CallExpression" &&
368 replacedNode.parent.type !== "ConditionalExpression" &&
369 !astUtils.isParenthesised(sourceCode, replacedNode) &&
370 !astUtils.isParenthesised(sourceCode, node)
371 ) {
372 yield fixer.insertTextBefore(replacedNode, "(");
373 yield fixer.insertTextAfter(replacedNode, ")");
374 }
eb39fafa
DC
375 }
376 });
377 }
378 }
379 };
380 }
381};