Ohm-Management - Projektarbeit B-ME
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

prefer-arrow-callback.js 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  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. },
  128. allowUnboundThis: {
  129. type: "boolean"
  130. }
  131. },
  132. additionalProperties: false
  133. }
  134. ],
  135. fixable: "code"
  136. },
  137. create(context) {
  138. const options = context.options[0] || {};
  139. const allowUnboundThis = options.allowUnboundThis !== false; // default to true
  140. const allowNamedFunctions = options.allowNamedFunctions;
  141. const sourceCode = context.getSourceCode();
  142. /*
  143. * {Array<{this: boolean, super: boolean, meta: boolean}>}
  144. * - this - A flag which shows there are one or more ThisExpression.
  145. * - super - A flag which shows there are one or more Super.
  146. * - meta - A flag which shows there are one or more MethProperty.
  147. */
  148. let stack = [];
  149. /**
  150. * Pushes new function scope with all `false` flags.
  151. * @returns {void}
  152. */
  153. function enterScope() {
  154. stack.push({ this: false, super: false, meta: false });
  155. }
  156. /**
  157. * Pops a function scope from the stack.
  158. * @returns {{this: boolean, super: boolean, meta: boolean}} The information of the last scope.
  159. */
  160. function exitScope() {
  161. return stack.pop();
  162. }
  163. return {
  164. // Reset internal state.
  165. Program() {
  166. stack = [];
  167. },
  168. // If there are below, it cannot replace with arrow functions merely.
  169. ThisExpression() {
  170. const info = stack[stack.length - 1];
  171. if (info) {
  172. info.this = true;
  173. }
  174. },
  175. Super() {
  176. const info = stack[stack.length - 1];
  177. if (info) {
  178. info.super = true;
  179. }
  180. },
  181. MetaProperty(node) {
  182. const info = stack[stack.length - 1];
  183. if (info && checkMetaProperty(node, "new", "target")) {
  184. info.meta = true;
  185. }
  186. },
  187. // To skip nested scopes.
  188. FunctionDeclaration: enterScope,
  189. "FunctionDeclaration:exit": exitScope,
  190. // Main.
  191. FunctionExpression: enterScope,
  192. "FunctionExpression:exit"(node) {
  193. const scopeInfo = exitScope();
  194. // Skip named function expressions
  195. if (allowNamedFunctions && node.id && node.id.name) {
  196. return;
  197. }
  198. // Skip generators.
  199. if (node.generator) {
  200. return;
  201. }
  202. // Skip recursive functions.
  203. const nameVar = context.getDeclaredVariables(node)[0];
  204. if (isFunctionName(nameVar) && nameVar.references.length > 0) {
  205. return;
  206. }
  207. // Skip if it's using arguments.
  208. const variable = getVariableOfArguments(context.getScope());
  209. if (variable && variable.references.length > 0) {
  210. return;
  211. }
  212. // Reports if it's a callback which can replace with arrows.
  213. const callbackInfo = getCallbackInfo(node);
  214. if (callbackInfo.isCallback &&
  215. (!allowUnboundThis || !scopeInfo.this || callbackInfo.isLexicalThis) &&
  216. !scopeInfo.super &&
  217. !scopeInfo.meta
  218. ) {
  219. context.report({
  220. node,
  221. message: "Unexpected function expression.",
  222. fix(fixer) {
  223. if ((!callbackInfo.isLexicalThis && scopeInfo.this) || hasDuplicateParams(node.params)) {
  224. /*
  225. * If the callback function does not have .bind(this) and contains a reference to `this`, there
  226. * is no way to determine what `this` should be, so don't perform any fixes.
  227. * If the callback function has duplicates in its list of parameters (possible in sloppy mode),
  228. * don't replace it with an arrow function, because this is a SyntaxError with arrow functions.
  229. */
  230. return null;
  231. }
  232. const paramsLeftParen = node.params.length ? sourceCode.getTokenBefore(node.params[0]) : sourceCode.getTokenBefore(node.body, 1);
  233. const paramsRightParen = sourceCode.getTokenBefore(node.body);
  234. const asyncKeyword = node.async ? "async " : "";
  235. const paramsFullText = sourceCode.text.slice(paramsLeftParen.range[0], paramsRightParen.range[1]);
  236. const arrowFunctionText = `${asyncKeyword}${paramsFullText} => ${sourceCode.getText(node.body)}`;
  237. /*
  238. * If the callback function has `.bind(this)`, replace it with an arrow function and remove the binding.
  239. * Otherwise, just replace the arrow function itself.
  240. */
  241. const replacedNode = callbackInfo.isLexicalThis ? node.parent.parent : node;
  242. /*
  243. * If the replaced node is part of a BinaryExpression, LogicalExpression, or MemberExpression, then
  244. * the arrow function needs to be parenthesized, because `foo || () => {}` is invalid syntax even
  245. * though `foo || function() {}` is valid.
  246. */
  247. const needsParens = replacedNode.parent.type !== "CallExpression" && replacedNode.parent.type !== "ConditionalExpression";
  248. const replacementText = needsParens ? `(${arrowFunctionText})` : arrowFunctionText;
  249. return fixer.replaceText(replacedNode, replacementText);
  250. }
  251. });
  252. }
  253. }
  254. };
  255. }
  256. };