Skip to content

⬅️ Back to Table of Contents

📄 require-await

📊 Analysis Summary

Metric Count
🔧 Functions 9
📦 Imports 13
📐 Interfaces 1
📑 Type Aliases 1

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/require-await.ts

📤 Default Export

export default createRule({ ... })
Property Value
name 'require-await'
meta.type 'suggestion'
meta.docs.description 'Disallow async functions which do not return promises and have no await expression'
meta.docs.extendsBaseRule true
meta.docs.recommended 'recommended'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.missingAwait "{{name}} has no 'await' expression."
meta.messages.removeAsync "Remove 'async'."
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
AST @typescript-eslint/utils/ts-eslint
RuleFix @typescript-eslint/utils/ts-eslint
AST_NODE_TYPES @typescript-eslint/utils
AST_TOKEN_TYPES @typescript-eslint/utils
createRule ../util
getFunctionHeadLoc ../util
getFunctionNameWithKind ../util
getParserServices ../util
isStartOfExpressionStatement ../util
needsPrecedingSemicolon ../util
nullThrows ../util
upperCaseFirst ../util

Functions

create(context: any): { ArrowFunctionExpression: (node: FunctionNode) => void; 'A…

Parameters:

  • context any

Returns: { ArrowFunctionExpression: (node: FunctionNode) => void; 'ArrowFunctionExpression:exit': (node: FunctionNode) => void; AwaitExpression: () => void; 'ForOfStatement[await = true]': () => void; FunctionDeclaration: (node: FunctionNode) => void; 'FunctionDeclaration:exit': (node: FunctionNode) => void; FunctionExpression: (node: FunctionNode) => void; 'FunctionExpression:exit': (node: FunctionNode) => void; 'VariableDeclaration[kind = "await using"]': () => void; YieldExpression: (node: TSESTree.YieldExpression) => void; 'ArrowFunctionExpression[async = true] > :not(BlockStatement, AwaitExpression)'(node: Exclude<TSESTree.Node, TSESTree.AwaitExpression | TSESTree.BlockStatement>): void; ReturnStatement(node: any): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • isEmptyFunction
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • context.sourceCode.getTokenAfter
  • isStartOfExpressionStatement (from ../util)
  • needsPrecedingSemicolon (from ../util)
  • hasTypeName
  • changes.push
  • context.sourceCode.getLastToken
  • context.report
  • getFunctionHeadLoc (from ../util)
  • upperCaseFirst (from ../util)
  • getFunctionNameWithKind (from ../util)
  • changes.map
  • fixer.replaceTextRange
  • fixer.removeRange
  • checker.getTypeAtLocation
  • tsutils.isThenableType
  • isThenableType
  • services.esTreeNodeToTSNodeMap.get
  • services.getTypeAtLocation
  • expandUnionOrIntersectionType
  • tsutils.getWellKnownSymbolPropertyOfType
  • markAsHasAwait

Internal Comments:

/**
     * Push the scope info object to the stack.
     */
/**
     * Pop the top scope info object from the stack.
     * Also, it reports the function if needed.
     */
/* istanbul ignore if */
// this shouldn't ever happen, as we have to exit a function after we enter it
// If the function belongs to a method definition or (x2)
// property, then the function's range may not include the (x2)
// `async` keyword and we should look at the parent instead. (x2)
// Removing the `async` keyword can cause parsing errors if the (x2)
// current statement is relying on automatic semicolon insertion. (x2)
// If ASI is currently being used, then we should replace the (x2)
// `async` keyword with a semicolon. (x2)
// If there's a return type annotation and it's a
// `Promise<T>`, we can also change the return type
// annotation to just `T` as part of the suggestion.
// Alternatively, if the function is a generator and
// the return type annotation is `AsyncGenerator<T>`,
// then we can change it to `Generator<T>`.
// Remove the closing angled bracket.
// Remove the "Promise" identifier
// and the opening angled bracket.
/**
     * Checks if the node returns a thenable type
     */
/**
     * Marks the current scope as having an await
     */
/**
     * Mark `scopeInfo.isAsyncYield` to `true` if it
     *  1) delegates async generator function
     *    or
     *  2) yields thenable type
     */
// ignoring this as for literals we don't need to check the definition
// eg : async function* run() { yield* 1 }
// check body-less async arrow function. (x2)
// ignore `async () => await foo` because it's obviously correct (x2)
// short circuit early to avoid unnecessary type checks

Code
create(context) {
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();

    let scopeInfo: ScopeInfo | null = null;

    /**
     * Push the scope info object to the stack.
     */
    function enterFunction(node: FunctionNode): void {
      scopeInfo = {
        hasAsync: node.async,
        hasAwait: false,
        isAsyncYield: false,
        isGen: node.generator || false,
        upper: scopeInfo,
      };
    }

    /**
     * Pop the top scope info object from the stack.
     * Also, it reports the function if needed.
     */
    function exitFunction(node: FunctionNode): void {
      /* istanbul ignore if */ if (!scopeInfo) {
        // this shouldn't ever happen, as we have to exit a function after we enter it
        return;
      }

      if (
        node.async &&
        !scopeInfo.hasAwait &&
        !isEmptyFunction(node) &&
        !(scopeInfo.isGen && scopeInfo.isAsyncYield)
      ) {
        // If the function belongs to a method definition or
        // property, then the function's range may not include the
        // `async` keyword and we should look at the parent instead.
        const nodeWithAsyncKeyword =
          (node.parent.type === AST_NODE_TYPES.MethodDefinition &&
            node.parent.value === node) ||
          (node.parent.type === AST_NODE_TYPES.Property &&
            node.parent.method &&
            node.parent.value === node)
            ? node.parent
            : node;

        const asyncToken = nullThrows(
          context.sourceCode.getFirstToken(
            nodeWithAsyncKeyword,
            token => token.value === 'async',
          ),
          'The node is an async function, so it must have an "async" token.',
        );

        const asyncRange: Readonly<AST.Range> = [
          asyncToken.range[0],
          nullThrows(
            context.sourceCode.getTokenAfter(asyncToken, {
              includeComments: true,
            }),
            'There will always be a token after the "async" keyword.',
          ).range[0],
        ] as const;

        // Removing the `async` keyword can cause parsing errors if the
        // current statement is relying on automatic semicolon insertion.
        // If ASI is currently being used, then we should replace the
        // `async` keyword with a semicolon.
        const nextToken = nullThrows(
          context.sourceCode.getTokenAfter(asyncToken),
          'There will always be a token after the "async" keyword.',
        );
        const addSemiColon =
          nextToken.type === AST_TOKEN_TYPES.Punctuator &&
          (nextToken.value === '[' || nextToken.value === '(') &&
          (nodeWithAsyncKeyword.type === AST_NODE_TYPES.MethodDefinition ||
            isStartOfExpressionStatement(nodeWithAsyncKeyword)) &&
          needsPrecedingSemicolon(context.sourceCode, nodeWithAsyncKeyword);

        const changes = [
          { range: asyncRange, replacement: addSemiColon ? ';' : undefined },
        ];

        // If there's a return type annotation and it's a
        // `Promise<T>`, we can also change the return type
        // annotation to just `T` as part of the suggestion.
        // Alternatively, if the function is a generator and
        // the return type annotation is `AsyncGenerator<T>`,
        // then we can change it to `Generator<T>`.
        if (
          node.returnType?.typeAnnotation.type ===
          AST_NODE_TYPES.TSTypeReference
        ) {
          if (scopeInfo.isGen) {
            if (hasTypeName(node.returnType.typeAnnotation, 'AsyncGenerator')) {
              changes.push({
                range: node.returnType.typeAnnotation.typeName.range,
                replacement: 'Generator',
              });
            }
          } else if (
            hasTypeName(node.returnType.typeAnnotation, 'Promise') &&
            node.returnType.typeAnnotation.typeArguments != null
          ) {
            const openAngle = nullThrows(
              context.sourceCode.getFirstToken(
                node.returnType.typeAnnotation,
                token =>
                  token.type === AST_TOKEN_TYPES.Punctuator &&
                  token.value === '<',
              ),
              'There are type arguments, so the angle bracket will exist.',
            );
            const closeAngle = nullThrows(
              context.sourceCode.getLastToken(
                node.returnType.typeAnnotation,
                token =>
                  token.type === AST_TOKEN_TYPES.Punctuator &&
                  token.value === '>',
              ),
              'There are type arguments, so the angle bracket will exist.',
            );
            changes.push(
              // Remove the closing angled bracket.
              { range: closeAngle.range, replacement: undefined },
              // Remove the "Promise" identifier
              // and the opening angled bracket.
              {
                range: [
                  node.returnType.typeAnnotation.typeName.range[0],
                  openAngle.range[1],
                ],
                replacement: undefined,
              },
            );
          }
        }

        context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: 'missingAwait',
          data: {
            name: upperCaseFirst(getFunctionNameWithKind(node)),
          },
          suggest: [
            {
              messageId: 'removeAsync',
              fix: (fixer): RuleFix[] =>
                changes.map(change =>
                  change.replacement != null
                    ? fixer.replaceTextRange(change.range, change.replacement)
                    : fixer.removeRange(change.range),
                ),
            },
          ],
        });
      }

      scopeInfo = scopeInfo.upper;
    }

    /**
     * Checks if the node returns a thenable type
     */
    function isThenableType(node: ts.Node): boolean {
      const type = checker.getTypeAtLocation(node);

      return tsutils.isThenableType(checker, node, type);
    }

    /**
     * Marks the current scope as having an await
     */
    function markAsHasAwait(): void {
      if (!scopeInfo) {
        return;
      }
      scopeInfo.hasAwait = true;
    }

    /**
     * Mark `scopeInfo.isAsyncYield` to `true` if it
     *  1) delegates async generator function
     *    or
     *  2) yields thenable type
     */
    function visitYieldExpression(node: TSESTree.YieldExpression): void {
      if (!scopeInfo?.isGen || !node.argument) {
        return;
      }

      if (node.argument.type === AST_NODE_TYPES.Literal) {
        // ignoring this as for literals we don't need to check the definition
        // eg : async function* run() { yield* 1 }
        return;
      }

      if (!node.delegate) {
        if (isThenableType(services.esTreeNodeToTSNodeMap.get(node.argument))) {
          scopeInfo.isAsyncYield = true;
        }
        return;
      }

      const type = services.getTypeAtLocation(node.argument);
      const typesToCheck = expandUnionOrIntersectionType(type);
      for (const type of typesToCheck) {
        const asyncIterator = tsutils.getWellKnownSymbolPropertyOfType(
          type,
          'asyncIterator',
          checker,
        );
        if (asyncIterator != null) {
          scopeInfo.isAsyncYield = true;
          break;
        }
      }
    }

    return {
      ArrowFunctionExpression: enterFunction,
      'ArrowFunctionExpression:exit': exitFunction,
      AwaitExpression: markAsHasAwait,
      'ForOfStatement[await = true]': markAsHasAwait,
      FunctionDeclaration: enterFunction,
      'FunctionDeclaration:exit': exitFunction,

      FunctionExpression: enterFunction,
      'FunctionExpression:exit': exitFunction,
      'VariableDeclaration[kind = "await using"]': markAsHasAwait,
      YieldExpression: visitYieldExpression,

      // check body-less async arrow function.
      // ignore `async () => await foo` because it's obviously correct
      'ArrowFunctionExpression[async = true] > :not(BlockStatement, AwaitExpression)'(
        node: Exclude<
          TSESTree.Node,
          TSESTree.AwaitExpression | TSESTree.BlockStatement
        >,
      ): void {
        const expression = services.esTreeNodeToTSNodeMap.get(node);
        if (isThenableType(expression)) {
          markAsHasAwait();
        }
      },
      ReturnStatement(node): void {
        // short circuit early to avoid unnecessary type checks
        if (!scopeInfo || scopeInfo.hasAwait || !scopeInfo.hasAsync) {
          return;
        }

        const { expression } = services.esTreeNodeToTSNodeMap.get(node);
        if (expression && isThenableType(expression)) {
          markAsHasAwait();
        }
      },
    };
  }

isEmptyFunction(node: FunctionNode): boolean

Parameters:

  • node FunctionNode

Returns: boolean

Code
function isEmptyFunction(node: FunctionNode): boolean {
  return (
    node.body.type === AST_NODE_TYPES.BlockStatement &&
    node.body.body.length === 0
  );
}

expandUnionOrIntersectionType(type: ts.Type): ts.Type[]

Parameters:

  • type ts.Type

Returns: ts.Type[]

Calls:

  • type.isUnionOrIntersection
  • type.types.flatMap
Code
function expandUnionOrIntersectionType(type: ts.Type): ts.Type[] {
  if (type.isUnionOrIntersection()) {
    return type.types.flatMap(expandUnionOrIntersectionType);
  }
  return [type];
}

hasTypeName(typeReference: TSESTree.TSTypeReference, typeName: string): boolean

Parameters:

  • typeReference TSESTree.TSTypeReference
  • typeName string

Returns: boolean

Code
function hasTypeName(
  typeReference: TSESTree.TSTypeReference,
  typeName: string,
): boolean {
  return (
    typeReference.typeName.type === AST_NODE_TYPES.Identifier &&
    typeReference.typeName.name === typeName
  );
}

Internal helpers

Declared inside another function in this file.

enterFunction(node: FunctionNode): void

Push the scope info object to the stack.

Raw JSDoc
/**
     * Push the scope info object to the stack.
     */
Code
function enterFunction(node: FunctionNode): void {
      scopeInfo = {
        hasAsync: node.async,
        hasAwait: false,
        isAsyncYield: false,
        isGen: node.generator || false,
        upper: scopeInfo,
      };
    }

exitFunction(node: FunctionNode): void

Pop the top scope info object from the stack. Also, it reports the function if needed.

Raw JSDoc
/**
     * Pop the top scope info object from the stack.
     * Also, it reports the function if needed.
     */

Calls:

  • isEmptyFunction
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • context.sourceCode.getTokenAfter
  • isStartOfExpressionStatement (from ../util)
  • needsPrecedingSemicolon (from ../util)
  • hasTypeName
  • changes.push
  • context.sourceCode.getLastToken
  • context.report
  • getFunctionHeadLoc (from ../util)
  • upperCaseFirst (from ../util)
  • getFunctionNameWithKind (from ../util)
  • changes.map
  • fixer.replaceTextRange
  • fixer.removeRange

Internal Comments:

/* istanbul ignore if */
// this shouldn't ever happen, as we have to exit a function after we enter it
// If the function belongs to a method definition or (x2)
// property, then the function's range may not include the (x2)
// `async` keyword and we should look at the parent instead. (x2)
// Removing the `async` keyword can cause parsing errors if the (x2)
// current statement is relying on automatic semicolon insertion. (x2)
// If ASI is currently being used, then we should replace the (x2)
// `async` keyword with a semicolon. (x2)
// If there's a return type annotation and it's a
// `Promise<T>`, we can also change the return type
// annotation to just `T` as part of the suggestion.
// Alternatively, if the function is a generator and
// the return type annotation is `AsyncGenerator<T>`,
// then we can change it to `Generator<T>`.
// Remove the closing angled bracket.
// Remove the "Promise" identifier
// and the opening angled bracket.

Code
function exitFunction(node: FunctionNode): void {
      /* istanbul ignore if */ if (!scopeInfo) {
        // this shouldn't ever happen, as we have to exit a function after we enter it
        return;
      }

      if (
        node.async &&
        !scopeInfo.hasAwait &&
        !isEmptyFunction(node) &&
        !(scopeInfo.isGen && scopeInfo.isAsyncYield)
      ) {
        // If the function belongs to a method definition or
        // property, then the function's range may not include the
        // `async` keyword and we should look at the parent instead.
        const nodeWithAsyncKeyword =
          (node.parent.type === AST_NODE_TYPES.MethodDefinition &&
            node.parent.value === node) ||
          (node.parent.type === AST_NODE_TYPES.Property &&
            node.parent.method &&
            node.parent.value === node)
            ? node.parent
            : node;

        const asyncToken = nullThrows(
          context.sourceCode.getFirstToken(
            nodeWithAsyncKeyword,
            token => token.value === 'async',
          ),
          'The node is an async function, so it must have an "async" token.',
        );

        const asyncRange: Readonly<AST.Range> = [
          asyncToken.range[0],
          nullThrows(
            context.sourceCode.getTokenAfter(asyncToken, {
              includeComments: true,
            }),
            'There will always be a token after the "async" keyword.',
          ).range[0],
        ] as const;

        // Removing the `async` keyword can cause parsing errors if the
        // current statement is relying on automatic semicolon insertion.
        // If ASI is currently being used, then we should replace the
        // `async` keyword with a semicolon.
        const nextToken = nullThrows(
          context.sourceCode.getTokenAfter(asyncToken),
          'There will always be a token after the "async" keyword.',
        );
        const addSemiColon =
          nextToken.type === AST_TOKEN_TYPES.Punctuator &&
          (nextToken.value === '[' || nextToken.value === '(') &&
          (nodeWithAsyncKeyword.type === AST_NODE_TYPES.MethodDefinition ||
            isStartOfExpressionStatement(nodeWithAsyncKeyword)) &&
          needsPrecedingSemicolon(context.sourceCode, nodeWithAsyncKeyword);

        const changes = [
          { range: asyncRange, replacement: addSemiColon ? ';' : undefined },
        ];

        // If there's a return type annotation and it's a
        // `Promise<T>`, we can also change the return type
        // annotation to just `T` as part of the suggestion.
        // Alternatively, if the function is a generator and
        // the return type annotation is `AsyncGenerator<T>`,
        // then we can change it to `Generator<T>`.
        if (
          node.returnType?.typeAnnotation.type ===
          AST_NODE_TYPES.TSTypeReference
        ) {
          if (scopeInfo.isGen) {
            if (hasTypeName(node.returnType.typeAnnotation, 'AsyncGenerator')) {
              changes.push({
                range: node.returnType.typeAnnotation.typeName.range,
                replacement: 'Generator',
              });
            }
          } else if (
            hasTypeName(node.returnType.typeAnnotation, 'Promise') &&
            node.returnType.typeAnnotation.typeArguments != null
          ) {
            const openAngle = nullThrows(
              context.sourceCode.getFirstToken(
                node.returnType.typeAnnotation,
                token =>
                  token.type === AST_TOKEN_TYPES.Punctuator &&
                  token.value === '<',
              ),
              'There are type arguments, so the angle bracket will exist.',
            );
            const closeAngle = nullThrows(
              context.sourceCode.getLastToken(
                node.returnType.typeAnnotation,
                token =>
                  token.type === AST_TOKEN_TYPES.Punctuator &&
                  token.value === '>',
              ),
              'There are type arguments, so the angle bracket will exist.',
            );
            changes.push(
              // Remove the closing angled bracket.
              { range: closeAngle.range, replacement: undefined },
              // Remove the "Promise" identifier
              // and the opening angled bracket.
              {
                range: [
                  node.returnType.typeAnnotation.typeName.range[0],
                  openAngle.range[1],
                ],
                replacement: undefined,
              },
            );
          }
        }

        context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: 'missingAwait',
          data: {
            name: upperCaseFirst(getFunctionNameWithKind(node)),
          },
          suggest: [
            {
              messageId: 'removeAsync',
              fix: (fixer): RuleFix[] =>
                changes.map(change =>
                  change.replacement != null
                    ? fixer.replaceTextRange(change.range, change.replacement)
                    : fixer.removeRange(change.range),
                ),
            },
          ],
        });
      }

      scopeInfo = scopeInfo.upper;
    }

isThenableType(node: ts.Node): boolean

Checks if the node returns a thenable type

Raw JSDoc
/**
     * Checks if the node returns a thenable type
     */

Calls:

  • checker.getTypeAtLocation
  • tsutils.isThenableType
Code
function isThenableType(node: ts.Node): boolean {
      const type = checker.getTypeAtLocation(node);

      return tsutils.isThenableType(checker, node, type);
    }

markAsHasAwait(): void

Marks the current scope as having an await

Raw JSDoc
/**
     * Marks the current scope as having an await
     */
Code
function markAsHasAwait(): void {
      if (!scopeInfo) {
        return;
      }
      scopeInfo.hasAwait = true;
    }

visitYieldExpression(node: TSESTree.YieldExpression): void

Mark scopeInfo.isAsyncYield to true if it 1) delegates async generator function or 2) yields thenable type

Raw JSDoc
/**
     * Mark `scopeInfo.isAsyncYield` to `true` if it
     *  1) delegates async generator function
     *    or
     *  2) yields thenable type
     */

Calls:

  • isThenableType
  • services.esTreeNodeToTSNodeMap.get
  • services.getTypeAtLocation
  • expandUnionOrIntersectionType
  • tsutils.getWellKnownSymbolPropertyOfType

Internal Comments:

// ignoring this as for literals we don't need to check the definition
// eg : async function* run() { yield* 1 }

Code
function visitYieldExpression(node: TSESTree.YieldExpression): void {
      if (!scopeInfo?.isGen || !node.argument) {
        return;
      }

      if (node.argument.type === AST_NODE_TYPES.Literal) {
        // ignoring this as for literals we don't need to check the definition
        // eg : async function* run() { yield* 1 }
        return;
      }

      if (!node.delegate) {
        if (isThenableType(services.esTreeNodeToTSNodeMap.get(node.argument))) {
          scopeInfo.isAsyncYield = true;
        }
        return;
      }

      const type = services.getTypeAtLocation(node.argument);
      const typesToCheck = expandUnionOrIntersectionType(type);
      for (const type of typesToCheck) {
        const asyncIterator = tsutils.getWellKnownSymbolPropertyOfType(
          type,
          'asyncIterator',
          checker,
        );
        if (asyncIterator != null) {
          scopeInfo.isAsyncYield = true;
          break;
        }
      }
    }

Interfaces

ScopeInfo

Interface Code
interface ScopeInfo {
  hasAsync: boolean;
  hasAwait: boolean;
  isAsyncYield: boolean;
  isGen: boolean;
  upper: ScopeInfo | null;
}

Properties

Name Type Optional Description
hasAsync boolean not shown
hasAwait boolean not shown
isAsyncYield boolean not shown
isGen boolean not shown
upper ScopeInfo \| null not shown

Type Aliases

FunctionNode

type FunctionNode = | TSESTree.ArrowFunctionExpression
  | TSESTree.FunctionDeclaration
  | TSESTree.FunctionExpression;

Generated by Syntax Scribe