require-await.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. /**
  2. * @fileoverview Rule to disallow async functions which have no `await` expression.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Helpers
  12. //------------------------------------------------------------------------------
  13. /**
  14. * Capitalize the 1st letter of the given text.
  15. * @param {string} text The text to capitalize.
  16. * @returns {string} The text that the 1st letter was capitalized.
  17. */
  18. function capitalizeFirstLetter(text) {
  19. return text[0].toUpperCase() + text.slice(1);
  20. }
  21. //------------------------------------------------------------------------------
  22. // Rule Definition
  23. //------------------------------------------------------------------------------
  24. /** @type {import('../types').Rule.RuleModule} */
  25. module.exports = {
  26. meta: {
  27. type: "suggestion",
  28. docs: {
  29. description:
  30. "Disallow async functions which have no `await` expression",
  31. recommended: false,
  32. url: "https://eslint.org/docs/latest/rules/require-await",
  33. },
  34. schema: [],
  35. messages: {
  36. missingAwait: "{{name}} has no 'await' expression.",
  37. removeAsync: "Remove 'async'.",
  38. },
  39. hasSuggestions: true,
  40. },
  41. create(context) {
  42. const sourceCode = context.sourceCode;
  43. let scopeInfo = null;
  44. /**
  45. * Push the scope info object to the stack.
  46. * @returns {void}
  47. */
  48. function enterFunction() {
  49. scopeInfo = {
  50. upper: scopeInfo,
  51. hasAwait: false,
  52. };
  53. }
  54. /**
  55. * Pop the top scope info object from the stack.
  56. * Also, it reports the function if needed.
  57. * @param {ASTNode} node The node to report.
  58. * @returns {void}
  59. */
  60. function exitFunction(node) {
  61. if (
  62. !node.generator &&
  63. node.async &&
  64. !scopeInfo.hasAwait &&
  65. !astUtils.isEmptyFunction(node)
  66. ) {
  67. /*
  68. * If the function belongs to a method definition or
  69. * property, then the function's range may not include the
  70. * `async` keyword and we should look at the parent instead.
  71. */
  72. const nodeWithAsyncKeyword =
  73. (node.parent.type === "MethodDefinition" &&
  74. node.parent.value === node) ||
  75. (node.parent.type === "Property" &&
  76. node.parent.method &&
  77. node.parent.value === node)
  78. ? node.parent
  79. : node;
  80. const asyncToken = sourceCode.getFirstToken(
  81. nodeWithAsyncKeyword,
  82. token => token.value === "async",
  83. );
  84. const asyncRange = [
  85. asyncToken.range[0],
  86. sourceCode.getTokenAfter(asyncToken, {
  87. includeComments: true,
  88. }).range[0],
  89. ];
  90. /*
  91. * Removing the `async` keyword can cause parsing errors if the current
  92. * statement is relying on automatic semicolon insertion. If ASI is currently
  93. * being used, then we should replace the `async` keyword with a semicolon.
  94. */
  95. const nextToken = sourceCode.getTokenAfter(asyncToken);
  96. const addSemiColon =
  97. nextToken.type === "Punctuator" &&
  98. (nextToken.value === "[" || nextToken.value === "(") &&
  99. (nodeWithAsyncKeyword.type === "MethodDefinition" ||
  100. astUtils.isStartOfExpressionStatement(
  101. nodeWithAsyncKeyword,
  102. )) &&
  103. astUtils.needsPrecedingSemicolon(
  104. sourceCode,
  105. nodeWithAsyncKeyword,
  106. );
  107. context.report({
  108. node,
  109. loc: astUtils.getFunctionHeadLoc(node, sourceCode),
  110. messageId: "missingAwait",
  111. data: {
  112. name: capitalizeFirstLetter(
  113. astUtils.getFunctionNameWithKind(node),
  114. ),
  115. },
  116. suggest: [
  117. {
  118. messageId: "removeAsync",
  119. fix: fixer =>
  120. fixer.replaceTextRange(
  121. asyncRange,
  122. addSemiColon ? ";" : "",
  123. ),
  124. },
  125. ],
  126. });
  127. }
  128. scopeInfo = scopeInfo.upper;
  129. }
  130. return {
  131. FunctionDeclaration: enterFunction,
  132. FunctionExpression: enterFunction,
  133. ArrowFunctionExpression: enterFunction,
  134. "FunctionDeclaration:exit": exitFunction,
  135. "FunctionExpression:exit": exitFunction,
  136. "ArrowFunctionExpression:exit": exitFunction,
  137. AwaitExpression() {
  138. if (!scopeInfo) {
  139. return;
  140. }
  141. scopeInfo.hasAwait = true;
  142. },
  143. ForOfStatement(node) {
  144. if (!scopeInfo) {
  145. return;
  146. }
  147. if (node.await) {
  148. scopeInfo.hasAwait = true;
  149. }
  150. },
  151. VariableDeclaration(node) {
  152. if (!scopeInfo) {
  153. return;
  154. }
  155. if (node.kind === "await using") {
  156. scopeInfo.hasAwait = true;
  157. }
  158. },
  159. };
  160. },
  161. };