| /** |
| * @fileoverview A rule to suggest using arrow functions as callbacks. |
| * @author Toru Nagashima |
| */ |
| |
| "use strict"; |
| |
| //------------------------------------------------------------------------------ |
| // Helpers |
| //------------------------------------------------------------------------------ |
| |
| /** |
| * Checks whether or not a given variable is a function name. |
| * @param {eslint-scope.Variable} variable - A variable to check. |
| * @returns {boolean} `true` if the variable is a function name. |
| */ |
| function isFunctionName(variable) { |
| return variable && variable.defs[0].type === "FunctionName"; |
| } |
| |
| /** |
| * Checks whether or not a given MetaProperty node equals to a given value. |
| * @param {ASTNode} node - A MetaProperty node to check. |
| * @param {string} metaName - The name of `MetaProperty.meta`. |
| * @param {string} propertyName - The name of `MetaProperty.property`. |
| * @returns {boolean} `true` if the node is the specific value. |
| */ |
| function checkMetaProperty(node, metaName, propertyName) { |
| return node.meta.name === metaName && node.property.name === propertyName; |
| } |
| |
| /** |
| * Gets the variable object of `arguments` which is defined implicitly. |
| * @param {eslint-scope.Scope} scope - A scope to get. |
| * @returns {eslint-scope.Variable} The found variable object. |
| */ |
| function getVariableOfArguments(scope) { |
| const variables = scope.variables; |
| |
| for (let i = 0; i < variables.length; ++i) { |
| const variable = variables[i]; |
| |
| if (variable.name === "arguments") { |
| |
| /* |
| * If there was a parameter which is named "arguments", the |
| * implicit "arguments" is not defined. |
| * So does fast return with null. |
| */ |
| return (variable.identifiers.length === 0) ? variable : null; |
| } |
| } |
| |
| /* istanbul ignore next */ |
| return null; |
| } |
| |
| /** |
| * Checkes whether or not a given node is a callback. |
| * @param {ASTNode} node - A node to check. |
| * @returns {Object} |
| * {boolean} retv.isCallback - `true` if the node is a callback. |
| * {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`. |
| */ |
| function getCallbackInfo(node) { |
| const retv = { isCallback: false, isLexicalThis: false }; |
| let currentNode = node; |
| let parent = node.parent; |
| |
| while (currentNode) { |
| switch (parent.type) { |
| |
| // Checks parents recursively. |
| |
| case "LogicalExpression": |
| case "ConditionalExpression": |
| break; |
| |
| // Checks whether the parent node is `.bind(this)` call. |
| case "MemberExpression": |
| if (parent.object === currentNode && |
| !parent.property.computed && |
| parent.property.type === "Identifier" && |
| parent.property.name === "bind" && |
| parent.parent.type === "CallExpression" && |
| parent.parent.callee === parent |
| ) { |
| retv.isLexicalThis = ( |
| parent.parent.arguments.length === 1 && |
| parent.parent.arguments[0].type === "ThisExpression" |
| ); |
| parent = parent.parent; |
| } else { |
| return retv; |
| } |
| break; |
| |
| // Checks whether the node is a callback. |
| case "CallExpression": |
| case "NewExpression": |
| if (parent.callee !== currentNode) { |
| retv.isCallback = true; |
| } |
| return retv; |
| |
| default: |
| return retv; |
| } |
| |
| currentNode = parent; |
| parent = parent.parent; |
| } |
| |
| /* istanbul ignore next */ |
| throw new Error("unreachable"); |
| } |
| |
| /** |
| * Checks whether a simple list of parameters contains any duplicates. This does not handle complex |
| * parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate |
| * parameter names anyway. Instead, it always returns `false` for complex parameter lists. |
| * @param {ASTNode[]} paramsList The list of parameters for a function |
| * @returns {boolean} `true` if the list of parameters contains any duplicates |
| */ |
| function hasDuplicateParams(paramsList) { |
| return paramsList.every(param => param.type === "Identifier") && paramsList.length !== new Set(paramsList.map(param => param.name)).size; |
| } |
| |
| //------------------------------------------------------------------------------ |
| // Rule Definition |
| //------------------------------------------------------------------------------ |
| |
| module.exports = { |
| meta: { |
| type: "suggestion", |
| |
| docs: { |
| description: "require using arrow functions for callbacks", |
| category: "ECMAScript 6", |
| recommended: false, |
| url: "https://eslint.org/docs/rules/prefer-arrow-callback" |
| }, |
| |
| schema: [ |
| { |
| type: "object", |
| properties: { |
| allowNamedFunctions: { |
| type: "boolean", |
| default: false |
| }, |
| allowUnboundThis: { |
| type: "boolean", |
| default: true |
| } |
| }, |
| additionalProperties: false |
| } |
| ], |
| |
| fixable: "code" |
| }, |
| |
| create(context) { |
| const options = context.options[0] || {}; |
| |
| const allowUnboundThis = options.allowUnboundThis !== false; // default to true |
| const allowNamedFunctions = options.allowNamedFunctions; |
| const sourceCode = context.getSourceCode(); |
| |
| /* |
| * {Array<{this: boolean, super: boolean, meta: boolean}>} |
| * - this - A flag which shows there are one or more ThisExpression. |
| * - super - A flag which shows there are one or more Super. |
| * - meta - A flag which shows there are one or more MethProperty. |
| */ |
| let stack = []; |
| |
| /** |
| * Pushes new function scope with all `false` flags. |
| * @returns {void} |
| */ |
| function enterScope() { |
| stack.push({ this: false, super: false, meta: false }); |
| } |
| |
| /** |
| * Pops a function scope from the stack. |
| * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope. |
| */ |
| function exitScope() { |
| return stack.pop(); |
| } |
| |
| return { |
| |
| // Reset internal state. |
| Program() { |
| stack = []; |
| }, |
| |
| // If there are below, it cannot replace with arrow functions merely. |
| ThisExpression() { |
| const info = stack[stack.length - 1]; |
| |
| if (info) { |
| info.this = true; |
| } |
| }, |
| |
| Super() { |
| const info = stack[stack.length - 1]; |
| |
| if (info) { |
| info.super = true; |
| } |
| }, |
| |
| MetaProperty(node) { |
| const info = stack[stack.length - 1]; |
| |
| if (info && checkMetaProperty(node, "new", "target")) { |
| info.meta = true; |
| } |
| }, |
| |
| // To skip nested scopes. |
| FunctionDeclaration: enterScope, |
| "FunctionDeclaration:exit": exitScope, |
| |
| // Main. |
| FunctionExpression: enterScope, |
| "FunctionExpression:exit"(node) { |
| const scopeInfo = exitScope(); |
| |
| // Skip named function expressions |
| if (allowNamedFunctions && node.id && node.id.name) { |
| return; |
| } |
| |
| // Skip generators. |
| if (node.generator) { |
| return; |
| } |
| |
| // Skip recursive functions. |
| const nameVar = context.getDeclaredVariables(node)[0]; |
| |
| if (isFunctionName(nameVar) && nameVar.references.length > 0) { |
| return; |
| } |
| |
| // Skip if it's using arguments. |
| const variable = getVariableOfArguments(context.getScope()); |
| |
| if (variable && variable.references.length > 0) { |
| return; |
| } |
| |
| // Reports if it's a callback which can replace with arrows. |
| const callbackInfo = getCallbackInfo(node); |
| |
| if (callbackInfo.isCallback && |
| (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) && |
| !scopeInfo.super && |
| !scopeInfo.meta |
| ) { |
| context.report({ |
| node, |
| message: "Unexpected function expression.", |
| fix(fixer) { |
| if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) { |
| |
| /* |
| * If the callback function does not have .bind(this) and contains a reference to `this`, there |
| * is no way to determine what `this` should be, so don't perform any fixes. |
| * If the callback function has duplicates in its list of parameters (possible in sloppy mode), |
| * don't replace it with an arrow function, because this is a SyntaxError with arrow functions. |
| */ |
| return null; |
| } |
| |
| const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1); |
| const paramsRightParen = sourceCode.getTokenBefore(node.body); |
| const asyncKeyword = node.async ? "async " : ""; |
| const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]); |
| const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`; |
| |
| /* |
| * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding. |
| * Otherwise, just replace the arrow function itself. |
| */ |
| const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node; |
| |
| /* |
| * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then |
| * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even |
| * though `foo || function() {}` is valid. |
| */ |
| const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression"; |
| const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText; |
| |
| return fixer.replaceText(replacedNode, replacementText); |
| } |
| }); |
| } |
| } |
| }; |
| } |
| }; |