Software zum Installieren eines Smart-Mirror Frameworks , zum Nutzen von hochschulrelevanten Informationen, auf einem Raspberry-Pi.
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 14KB

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