Skip to content

⬅️ Back to Table of Contents

📄 promise-function-async

📊 Analysis Summary

Metric Count
🔧 Functions 3
📦 Imports 10
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/promise-function-async.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'promise-function-async'
meta.type 'suggestion'
meta.docs.description 'Require any function or method that returns a Promise to be marked async'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.missingAsync 'Functions that return promises must be async.'
meta.messages.missingAsyncHybridReturn 'Functions that return promises must be async. Consider adding an explicit return type annotation if the function is ...
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowAny: { type: 'boolean', description: 'Whether to ...
defaultOptions [ { allowAny: true, allowedPromiseNames: [], checkArrowFunctions: true, checkFunctionDeclarations: true, checkFunctio...

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
AST_TOKEN_TYPES @typescript-eslint/utils
containsAllTypesByName ../util
createRule ../util
getFunctionHeadLoc ../util
getParserServices ../util
isTypeFlagSet ../util
nullThrows ../util
NullThrowsReasons ../util

Functions

create(context: any, [ { allowAny, allowedPromiseNam…: any): { 'FunctionExpression[async = false]'(node: TSESTree.Functi…

Parameters:

  • context any
  • [ { allowAny, allowedPromiseNames, checkArrowFunctions, checkFunctionDeclarations, checkFunctionExpressions, checkMethodDeclarations, }, ] any

Returns: { 'FunctionExpression[async = false]'(node: TSESTree.FunctionExpression): void; 'FunctionDeclaration[async = false]'(node: TSESTree.FunctionDeclaration): void; 'ArrowFunctionExpression[async = false]'(node: TSESTree.ArrowFunctionExpression): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • services.getTypeAtLocation(node).getCallSignatures
  • signatures.map
  • checker.getReturnTypeOfSignature
  • returnTypes.some
  • isTypeFlagSet (from ../util)
  • context.report
  • getFunctionHeadLoc (from ../util)
  • returnTypes.every
  • containsAllTypesByName (from ../util)
  • type.isUnion
  • type.types.every
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • NullThrowsReasons.MissingToken
  • context.sourceCode.getTokenAfter
  • context.sourceCode.isSpaceBetween
  • context.sourceCode.getTokenBefore
  • fixer.insertTextBefore
  • validateNode

Internal Comments:

// https://github.com/typescript-eslint/typescript-eslint/issues/5439
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
// Abstract method can't be async
// Getters and setters can't be async
// Report without auto fixer because the return type is unknown
// require all potential return types to be promise/any/unknown (x3)
// If no return type is explicitly set, we check if any parts of the return type match a Promise (instead of requiring all to match). (x3)
// this function is a class method or object function property shorthand (x2)
// the token to put `async` before (x2)
// if there are decorators then skip past them
// if current token is a keyword like `static` or `public`, or the `override` modifier, then skip it
// check if there is a space between key and previous token (x2)

Code
create(
    context,
    [
      {
        allowAny,
        allowedPromiseNames,
        checkArrowFunctions,
        checkFunctionDeclarations,
        checkFunctionExpressions,
        checkMethodDeclarations,
      },
    ],
  ) {
    const allAllowedPromiseNames = new Set([
      'Promise',
      // https://github.com/typescript-eslint/typescript-eslint/issues/5439
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      ...allowedPromiseNames!,
    ]);
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();

    function validateNode(
      node:
        | TSESTree.ArrowFunctionExpression
        | TSESTree.FunctionDeclaration
        | TSESTree.FunctionExpression,
    ): void {
      if (node.parent.type === AST_NODE_TYPES.TSAbstractMethodDefinition) {
        // Abstract method can't be async
        return;
      }

      if (
        (node.parent.type === AST_NODE_TYPES.Property ||
          node.parent.type === AST_NODE_TYPES.MethodDefinition) &&
        (node.parent.kind === 'get' || node.parent.kind === 'set')
      ) {
        // Getters and setters can't be async
        return;
      }

      const signatures = services.getTypeAtLocation(node).getCallSignatures();
      if (!signatures.length) {
        return;
      }

      const returnTypes = signatures.map(signature =>
        checker.getReturnTypeOfSignature(signature),
      );

      if (
        !allowAny &&
        returnTypes.some(type =>
          isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown),
        )
      ) {
        // Report without auto fixer because the return type is unknown
        return context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: 'missingAsync',
        });
      }

      if (
        // require all potential return types to be promise/any/unknown
        returnTypes.every(type =>
          containsAllTypesByName(
            type,
            true,
            allAllowedPromiseNames,
            // If no return type is explicitly set, we check if any parts of the return type match a Promise (instead of requiring all to match).
            node.returnType == null,
          ),
        )
      ) {
        const isHybridReturnType = returnTypes.some(
          type =>
            type.isUnion() &&
            !type.types.every(part =>
              containsAllTypesByName(part, true, allAllowedPromiseNames),
            ),
        );

        context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: isHybridReturnType
            ? 'missingAsyncHybridReturn'
            : 'missingAsync',
          fix: fixer => {
            if (
              node.parent.type === AST_NODE_TYPES.MethodDefinition ||
              (node.parent.type === AST_NODE_TYPES.Property &&
                node.parent.method)
            ) {
              // this function is a class method or object function property shorthand
              const method = node.parent;

              // the token to put `async` before
              let keyToken = nullThrows(
                context.sourceCode.getFirstToken(method),
                NullThrowsReasons.MissingToken('key token', 'method'),
              );

              // if there are decorators then skip past them
              if (
                method.type === AST_NODE_TYPES.MethodDefinition &&
                method.decorators.length
              ) {
                const lastDecorator =
                  method.decorators[method.decorators.length - 1];
                keyToken = nullThrows(
                  context.sourceCode.getTokenAfter(lastDecorator),
                  NullThrowsReasons.MissingToken('key token', 'last decorator'),
                );
              }

              // if current token is a keyword like `static` or `public`, or the `override` modifier, then skip it
              while (
                (keyToken.type === AST_TOKEN_TYPES.Keyword ||
                  (keyToken.type === AST_TOKEN_TYPES.Identifier &&
                    keyToken.value === 'override')) &&
                keyToken.range[0] < method.key.range[0]
              ) {
                keyToken = nullThrows(
                  context.sourceCode.getTokenAfter(keyToken),
                  NullThrowsReasons.MissingToken('token', 'modifier keyword'),
                );
              }

              // check if there is a space between key and previous token
              const insertSpace = !context.sourceCode.isSpaceBetween(
                nullThrows(
                  context.sourceCode.getTokenBefore(keyToken),
                  NullThrowsReasons.MissingToken('token', 'keyword'),
                ),
                keyToken,
              );

              let code = 'async ';
              if (insertSpace) {
                code = ` ${code}`;
              }
              return fixer.insertTextBefore(keyToken, code);
            }

            return fixer.insertTextBefore(node, 'async ');
          },
        });
      }
    }

    return {
      ...(checkArrowFunctions && {
        'ArrowFunctionExpression[async = false]'(
          node: TSESTree.ArrowFunctionExpression,
        ): void {
          validateNode(node);
        },
      }),
      ...(checkFunctionDeclarations && {
        'FunctionDeclaration[async = false]'(
          node: TSESTree.FunctionDeclaration,
        ): void {
          validateNode(node);
        },
      }),
      'FunctionExpression[async = false]'(
        node: TSESTree.FunctionExpression,
      ): void {
        if (
          node.parent.type === AST_NODE_TYPES.MethodDefinition &&
          node.parent.kind === 'method'
        ) {
          if (checkMethodDeclarations) {
            validateNode(node);
          }
          return;
        }
        if (checkFunctionExpressions) {
          validateNode(node);
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

validateNode(node: | TSESTree.ArrowFunctionExpression | TS…): void

Parameters:

  • node | TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression

Returns: void

Calls:

  • services.getTypeAtLocation(node).getCallSignatures
  • signatures.map
  • checker.getReturnTypeOfSignature
  • returnTypes.some
  • isTypeFlagSet (from ../util)
  • context.report
  • getFunctionHeadLoc (from ../util)
  • returnTypes.every
  • containsAllTypesByName (from ../util)
  • type.isUnion
  • type.types.every
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • NullThrowsReasons.MissingToken
  • context.sourceCode.getTokenAfter
  • context.sourceCode.isSpaceBetween
  • context.sourceCode.getTokenBefore
  • fixer.insertTextBefore

Internal Comments:

// Abstract method can't be async
// Getters and setters can't be async
// Report without auto fixer because the return type is unknown
// require all potential return types to be promise/any/unknown (x3)
// If no return type is explicitly set, we check if any parts of the return type match a Promise (instead of requiring all to match). (x3)
// this function is a class method or object function property shorthand (x2)
// the token to put `async` before (x2)
// if there are decorators then skip past them
// if current token is a keyword like `static` or `public`, or the `override` modifier, then skip it
// check if there is a space between key and previous token (x2)

Code
function validateNode(
      node:
        | TSESTree.ArrowFunctionExpression
        | TSESTree.FunctionDeclaration
        | TSESTree.FunctionExpression,
    ): void {
      if (node.parent.type === AST_NODE_TYPES.TSAbstractMethodDefinition) {
        // Abstract method can't be async
        return;
      }

      if (
        (node.parent.type === AST_NODE_TYPES.Property ||
          node.parent.type === AST_NODE_TYPES.MethodDefinition) &&
        (node.parent.kind === 'get' || node.parent.kind === 'set')
      ) {
        // Getters and setters can't be async
        return;
      }

      const signatures = services.getTypeAtLocation(node).getCallSignatures();
      if (!signatures.length) {
        return;
      }

      const returnTypes = signatures.map(signature =>
        checker.getReturnTypeOfSignature(signature),
      );

      if (
        !allowAny &&
        returnTypes.some(type =>
          isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown),
        )
      ) {
        // Report without auto fixer because the return type is unknown
        return context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: 'missingAsync',
        });
      }

      if (
        // require all potential return types to be promise/any/unknown
        returnTypes.every(type =>
          containsAllTypesByName(
            type,
            true,
            allAllowedPromiseNames,
            // If no return type is explicitly set, we check if any parts of the return type match a Promise (instead of requiring all to match).
            node.returnType == null,
          ),
        )
      ) {
        const isHybridReturnType = returnTypes.some(
          type =>
            type.isUnion() &&
            !type.types.every(part =>
              containsAllTypesByName(part, true, allAllowedPromiseNames),
            ),
        );

        context.report({
          loc: getFunctionHeadLoc(node, context.sourceCode),
          node,
          messageId: isHybridReturnType
            ? 'missingAsyncHybridReturn'
            : 'missingAsync',
          fix: fixer => {
            if (
              node.parent.type === AST_NODE_TYPES.MethodDefinition ||
              (node.parent.type === AST_NODE_TYPES.Property &&
                node.parent.method)
            ) {
              // this function is a class method or object function property shorthand
              const method = node.parent;

              // the token to put `async` before
              let keyToken = nullThrows(
                context.sourceCode.getFirstToken(method),
                NullThrowsReasons.MissingToken('key token', 'method'),
              );

              // if there are decorators then skip past them
              if (
                method.type === AST_NODE_TYPES.MethodDefinition &&
                method.decorators.length
              ) {
                const lastDecorator =
                  method.decorators[method.decorators.length - 1];
                keyToken = nullThrows(
                  context.sourceCode.getTokenAfter(lastDecorator),
                  NullThrowsReasons.MissingToken('key token', 'last decorator'),
                );
              }

              // if current token is a keyword like `static` or `public`, or the `override` modifier, then skip it
              while (
                (keyToken.type === AST_TOKEN_TYPES.Keyword ||
                  (keyToken.type === AST_TOKEN_TYPES.Identifier &&
                    keyToken.value === 'override')) &&
                keyToken.range[0] < method.key.range[0]
              ) {
                keyToken = nullThrows(
                  context.sourceCode.getTokenAfter(keyToken),
                  NullThrowsReasons.MissingToken('token', 'modifier keyword'),
                );
              }

              // check if there is a space between key and previous token
              const insertSpace = !context.sourceCode.isSpaceBetween(
                nullThrows(
                  context.sourceCode.getTokenBefore(keyToken),
                  NullThrowsReasons.MissingToken('token', 'keyword'),
                ),
                keyToken,
              );

              let code = 'async ';
              if (insertSpace) {
                code = ` ${code}`;
              }
              return fixer.insertTextBefore(keyToken, code);
            }

            return fixer.insertTextBefore(node, 'async ');
          },
        });
      }
    }

fix(fixer: any): any

Parameters:

  • fixer any

Returns: any

Calls:

  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • NullThrowsReasons.MissingToken
  • context.sourceCode.getTokenAfter
  • context.sourceCode.isSpaceBetween
  • context.sourceCode.getTokenBefore
  • fixer.insertTextBefore

Internal Comments:

// this function is a class method or object function property shorthand (x2)
// the token to put `async` before (x2)
// if there are decorators then skip past them
// if current token is a keyword like `static` or `public`, or the `override` modifier, then skip it
// check if there is a space between key and previous token (x2)

Code
fixer => {
            if (
              node.parent.type === AST_NODE_TYPES.MethodDefinition ||
              (node.parent.type === AST_NODE_TYPES.Property &&
                node.parent.method)
            ) {
              // this function is a class method or object function property shorthand
              const method = node.parent;

              // the token to put `async` before
              let keyToken = nullThrows(
                context.sourceCode.getFirstToken(method),
                NullThrowsReasons.MissingToken('key token', 'method'),
              );

              // if there are decorators then skip past them
              if (
                method.type === AST_NODE_TYPES.MethodDefinition &&
                method.decorators.length
              ) {
                const lastDecorator =
                  method.decorators[method.decorators.length - 1];
                keyToken = nullThrows(
                  context.sourceCode.getTokenAfter(lastDecorator),
                  NullThrowsReasons.MissingToken('key token', 'last decorator'),
                );
              }

              // if current token is a keyword like `static` or `public`, or the `override` modifier, then skip it
              while (
                (keyToken.type === AST_TOKEN_TYPES.Keyword ||
                  (keyToken.type === AST_TOKEN_TYPES.Identifier &&
                    keyToken.value === 'override')) &&
                keyToken.range[0] < method.key.range[0]
              ) {
                keyToken = nullThrows(
                  context.sourceCode.getTokenAfter(keyToken),
                  NullThrowsReasons.MissingToken('token', 'modifier keyword'),
                );
              }

              // check if there is a space between key and previous token
              const insertSpace = !context.sourceCode.isSpaceBetween(
                nullThrows(
                  context.sourceCode.getTokenBefore(keyToken),
                  NullThrowsReasons.MissingToken('token', 'keyword'),
                ),
                keyToken,
              );

              let code = 'async ';
              if (insertSpace) {
                code = ` ${code}`;
              }
              return fixer.insertTextBefore(keyToken, code);
            }

            return fixer.insertTextBefore(node, 'async ');
          }

Type Aliases

Options

type Options = [
  {
    allowAny?: boolean;
    allowedPromiseNames?: string[];
    checkArrowFunctions?: boolean;
    checkFunctionDeclarations?: boolean;
    checkFunctionExpressions?: boolean;
    checkMethodDeclarations?: boolean;
  },
];

MessageIds

type MessageIds = 'missingAsync' | 'missingAsyncHybridReturn';

Generated by Syntax Scribe