prefer-arrow-callback.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. /**
  2. * @fileoverview A rule to suggest using arrow functions as callbacks.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. /**
  10. * Checks whether or not a given variable is a function name.
  11. * @param {eslint-scope.Variable} variable A variable to check.
  12. * @returns {boolean} `true` if the variable is a function name.
  13. */
  14. function isFunctionName(variable) {
  15. return variable && variable.defs[0].type === "FunctionName";
  16. }
  17. /**
  18. * Checks whether or not a given MetaProperty node equals to a given value.
  19. * @param {ASTNode} node A MetaProperty node to check.
  20. * @param {string} metaName The name of `MetaProperty.meta`.
  21. * @param {string} propertyName The name of `MetaProperty.property`.
  22. * @returns {boolean} `true` if the node is the specific value.
  23. */
  24. function checkMetaProperty(node, metaName, propertyName) {
  25. return node.meta.name === metaName && node.property.name === propertyName;
  26. }
  27. /**
  28. * Gets the variable object of `arguments` which is defined implicitly.
  29. * @param {eslint-scope.Scope} scope A scope to get.
  30. * @returns {eslint-scope.Variable} The found variable object.
  31. */
  32. function getVariableOfArguments(scope) {
  33. const variables = scope.variables;
  34. for (let i = 0; i < variables.length; ++i) {
  35. const variable = variables[i];
  36. if (variable.name === "arguments") {
  37. /*
  38. * If there was a parameter which is named "arguments", the
  39. * implicit "arguments" is not defined.
  40. * So does fast return with null.
  41. */
  42. return (variable.identifiers.length === 0) ? variable : null;
  43. }
  44. }
  45. /* istanbul ignore next */
  46. return null;
  47. }
  48. /**
  49. * Checkes whether or not a given node is a callback.
  50. * @param {ASTNode} node A node to check.
  51. * @returns {Object}
  52. * {boolean} retv.isCallback - `true` if the node is a callback.
  53. * {boolean} retv.isLexicalThis - `true` if the node is with `.bind(this)`.
  54. */
  55. function getCallbackInfo(node) {
  56. const retv = { isCallback: false, isLexicalThis: false };
  57. let currentNode = node;
  58. let parent = node.parent;
  59. while (currentNode) {
  60. switch (parent.type) {
  61. // Checks parents recursively.
  62. case "LogicalExpression":
  63. case "ConditionalExpression":
  64. break;
  65. // Checks whether the parent node is `.bind(this)` call.
  66. case "MemberExpression":
  67. if (parent.object === currentNode &&
  68. !parent.property.computed &&
  69. parent.property.type === "Identifier" &&
  70. parent.property.name === "bind" &&
  71. parent.parent.type === "CallExpression" &&
  72. parent.parent.callee === parent
  73. ) {
  74. retv.isLexicalThis = (
  75. parent.parent.arguments.length === 1 &&
  76. parent.parent.arguments[0].type === "ThisExpression"
  77. );
  78. parent = parent.parent;
  79. } else {
  80. return retv;
  81. }
  82. break;
  83. // Checks whether the node is a callback.
  84. case "CallExpression":
  85. case "NewExpression":
  86. if (parent.callee !== currentNode) {
  87. retv.isCallback = true;
  88. }
  89. return retv;
  90. default:
  91. return retv;
  92. }
  93. currentNode = parent;
  94. parent = parent.parent;
  95. }
  96. /* istanbul ignore next */
  97. throw new Error("unreachable");
  98. }
  99. /**
  100. * Checks whether a simple list of parameters contains any duplicates. This does not handle complex
  101. * parameter lists (e.g. with destructuring), since complex parameter lists are a SyntaxError with duplicate
  102. * parameter names anyway. Instead, it always returns `false` for complex parameter lists.
  103. * @param {ASTNode[]} paramsList The list of parameters for a function
  104. * @returns {boolean} `true` if the list of parameters contains any duplicates
  105. */
  106. function hasDuplicateParams(paramsList) {
  107. return paramsList.every(param => param.type === "Identifier") && paramsList.length !== new Set(paramsList.map(param => param.name)).size;
  108. }
  109. //------------------------------------------------------------------------------
  110. // Rule Definition
  111. //------------------------------------------------------------------------------
  112. module.exports = {
  113. meta: {
  114. type: "suggestion",
  115. docs: {
  116. description: "require using arrow functions for callbacks",
  117. category: "ECMAScript 6",
  118. recommended: false,
  119. url: "https://eslint.org/docs/rules/prefer-arrow-callback"
  120. },
  121. schema: [
  122. {
  123. type: "object",
  124. properties: {
  125. allowNamedFunctions: {
  126. type: "boolean",
  127. default: false
  128. },
  129. allowUnboundThis: {
  130. type: "boolean",
  131. default: true
  132. }
  133. },
  134. additionalProperties: false
  135. }
  136. ],
  137. fixable: "code"
  138. },
  139. create(context) {
  140. const options = context.options[0] || {};
  141. const allowUnboundThis = options.allowUnboundThis !== false; // default to true
  142. const allowNamedFunctions = options.allowNamedFunctions;
  143. const sourceCode = context.getSourceCode();
  144. /*
  145. * {Array<{this: boolean, super: boolean, meta: boolean}>}
  146. * - this - A flag which shows there are one or more ThisExpression.
  147. * - super - A flag which shows there are one or more Super.
  148. * - meta - A flag which shows there are one or more MethProperty.
  149. */
  150. let stack = [];
  151. /**
  152. * Pushes new function scope with all `false` flags.
  153. * @returns {void}
  154. */
  155. function enterScope() {
  156. stack.push({ this: false, super: false, meta: false });
  157. }
  158. /**
  159. * Pops a function scope from the stack.
  160. * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
  161. */
  162. function exitScope() {
  163. return stack.pop();
  164. }
  165. return {
  166. // Reset internal state.
  167. Program() {
  168. stack = [];
  169. },
  170. // If there are below, it cannot replace with arrow functions merely.
  171. ThisExpression() {
  172. const info = stack[stack.length - 1];
  173. if (info) {
  174. info.this = true;
  175. }
  176. },
  177. Super() {
  178. const info = stack[stack.length - 1];
  179. if (info) {
  180. info.super = true;
  181. }
  182. },
  183. MetaProperty(node) {
  184. const info = stack[stack.length - 1];
  185. if (info && checkMetaProperty(node, "new", "target")) {
  186. info.meta = true;
  187. }
  188. },
  189. // To skip nested scopes.
  190. FunctionDeclaration: enterScope,
  191. "FunctionDeclaration:exit": exitScope,
  192. // Main.
  193. FunctionExpression: enterScope,
  194. "FunctionExpression:exit"(node) {
  195. const scopeInfo = exitScope();
  196. // Skip named function expressions
  197. if (allowNamedFunctions && node.id && node.id.name) {
  198. return;
  199. }
  200. // Skip generators.
  201. if (node.generator) {
  202. return;
  203. }
  204. // Skip recursive functions.
  205. const nameVar = context.getDeclaredVariables(node)[0];
  206. if (isFunctionName(nameVar) && nameVar.references.length > 0) {
  207. return;
  208. }
  209. // Skip if it's using arguments.
  210. const variable = getVariableOfArguments(context.getScope());
  211. if (variable && variable.references.length > 0) {
  212. return;
  213. }
  214. // Reports if it's a callback which can replace with arrows.
  215. const callbackInfo = getCallbackInfo(node);
  216. if (callbackInfo.isCallback &&
  217. (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) &&
  218. !scopeInfo.super &&
  219. !scopeInfo.meta
  220. ) {
  221. context.report({
  222. node,
  223. message: "Unexpected function expression.",
  224. fix(fixer) {
  225. if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) {
  226. /*
  227. * If the callback function does not have .bind(this) and contains a reference to `this`, there
  228. * is no way to determine what `this` should be, so don't perform any fixes.
  229. * If the callback function has duplicates in its list of parameters (possible in sloppy mode),
  230. * don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
  231. */
  232. return null;
  233. }
  234. const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1);
  235. const paramsRightParen = sourceCode.getTokenBefore(node.body);
  236. const asyncKeyword = node.async ? "async " : "";
  237. const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
  238. const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
  239. /*
  240. * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
  241. * Otherwise, just replace the arrow function itself.
  242. */
  243. const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
  244. /*
  245. * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
  246. * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
  247. * though `foo || function() {}` is valid.
  248. */
  249. const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression";
  250. const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
  251. return fixer.replaceText(replacedNode, replacementText);
  252. }
  253. });
  254. }
  255. }
  256. };
  257. }
  258. };