Skip to content

⬅️ Back to Table of Contents

📄 await-thenable

📊 Analysis Summary

Metric Count
🔧 Functions 6
📦 Imports 14
📑 Type Aliases 1

📚 Table of Contents

🛠️ File Location:

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

📤 Default Export

export default createRule<[], MessageId>({ ... })
Property Value
name 'await-thenable'
meta.type 'problem'
meta.docs.description 'Disallow awaiting a value that is not a Thenable'
meta.docs.recommended 'recommended'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.await 'Unexpected await of a non-Promise (non-"Thenable") value.'
meta.messages.awaitUsingOfNonAsyncDisposable 'Unexpected await using of a value that is not async disposable.'
meta.messages.convertToOrdinaryFor 'Convert to an ordinary for...of loop.'
meta.messages.forAwaitOfNonAsyncIterable 'Unexpected for await...of of a value that is not async iterable.'
meta.messages.invalidPromiseAggregatorInput 'Unexpected iterable of non-Promise (non-"Thenable") values passed to promise aggregator.'
meta.messages.removeAwait 'Remove unnecessary await.'
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
Awaitable ../util
createRule ../util
getConstrainedTypeAtLocation ../util
getFixOrSuggest ../util
getParserServices ../util
isAwaitKeyword ../util
isTypeAnyType ../util
needsToBeAwaited ../util
nullThrows ../util
NullThrowsReasons ../util
getForStatementHeadLoc ../util/getForStatementHeadLoc
isPromiseAggregatorMethod ../util/isPromiseAggregatorMethod

Functions

create(context: any): { AwaitExpression(node: any): void; CallExpression(node: TS…

Parameters:

  • context any

Returns: { AwaitExpression(node: any): void; CallExpression(node: TSESTree.CallExpression): void; 'ForOfStatement[await=true]'(node: TSESTree.ForOfStatement): void; 'VariableDeclaration[kind="await using"]'(node: TSESTree.VariableDeclaration): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • services.getTypeAtLocation
  • services.esTreeNodeToTSNodeMap.get
  • needsToBeAwaited (from ../util)
  • context.report
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • NullThrowsReasons.MissingToken
  • fixer.remove
  • isPromiseAggregatorMethod (from ../util/isPromiseAggregatorMethod)
  • node.arguments.at
  • getConstrainedTypeAtLocation (from ../util)
  • isAlwaysNonAwaitableType
  • isInvalidPromiseAggregatorInput
  • isTypeAnyType (from ../util)
  • tsutils .unionConstituents(type) .some
  • tsutils.getWellKnownSymbolPropertyOfType
  • getForStatementHeadLoc (from ../util/getForStatementHeadLoc)
  • tsutils .unionConstituents(type) .some
  • getFixOrSuggest (from ../util)

Internal Comments:

// Note that this suggestion causes broken code for sync iterables
// of promises, since the loop variable is not awaited.
// let the user figure out what to do if there's
// await using a = b, c = d, e = f;
// it's rare and not worth the complexity to handle.

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

    return {
      AwaitExpression(node): void {
        const awaitArgumentEsNode = node.argument;
        const awaitArgumentType =
          services.getTypeAtLocation(awaitArgumentEsNode);
        const awaitArgumentTsNode =
          services.esTreeNodeToTSNodeMap.get(awaitArgumentEsNode);

        const certainty = needsToBeAwaited(
          checker,
          awaitArgumentTsNode,
          awaitArgumentType,
        );

        if (certainty === Awaitable.Never) {
          context.report({
            node,
            messageId: 'await',
            suggest: [
              {
                messageId: 'removeAwait',
                fix(fixer): TSESLint.RuleFix {
                  const awaitKeyword = nullThrows(
                    context.sourceCode.getFirstToken(node, isAwaitKeyword),
                    NullThrowsReasons.MissingToken('await', 'await expression'),
                  );

                  return fixer.remove(awaitKeyword);
                },
              },
            ],
          });
        }
      },

      CallExpression(node: TSESTree.CallExpression): void {
        if (!isPromiseAggregatorMethod(context, services, node)) {
          return;
        }

        const argument = node.arguments.at(0);

        if (argument == null) {
          return;
        }

        if (argument.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
          for (const element of argument.elements) {
            if (element == null) {
              continue;
            }

            const type = getConstrainedTypeAtLocation(services, element);
            const tsNode = services.esTreeNodeToTSNodeMap.get(element);

            if (isAlwaysNonAwaitableType(type, tsNode, checker)) {
              context.report({
                node: element,
                messageId: 'invalidPromiseAggregatorInput',
              });
            }
          }

          return;
        }

        const type = getConstrainedTypeAtLocation(services, argument);

        if (
          isInvalidPromiseAggregatorInput(
            checker,
            services.esTreeNodeToTSNodeMap.get(argument),
            type,
          )
        ) {
          context.report({
            node: argument,
            messageId: 'invalidPromiseAggregatorInput',
          });
        }
      },

      'ForOfStatement[await=true]'(node: TSESTree.ForOfStatement): void {
        const type = services.getTypeAtLocation(node.right);
        if (isTypeAnyType(type)) {
          return;
        }

        const hasAsyncIteratorSymbol = tsutils
          .unionConstituents(type)
          .some(
            typePart =>
              tsutils.getWellKnownSymbolPropertyOfType(
                typePart,
                'asyncIterator',
                checker,
              ) != null,
          );

        if (!hasAsyncIteratorSymbol) {
          context.report({
            loc: getForStatementHeadLoc(context.sourceCode, node),
            messageId: 'forAwaitOfNonAsyncIterable',
            suggest: [
              // Note that this suggestion causes broken code for sync iterables
              // of promises, since the loop variable is not awaited.
              {
                messageId: 'convertToOrdinaryFor',
                fix(fixer): TSESLint.RuleFix {
                  const awaitToken = nullThrows(
                    context.sourceCode.getFirstToken(node, isAwaitKeyword),
                    NullThrowsReasons.MissingToken('await', 'for await loop'),
                  );
                  return fixer.remove(awaitToken);
                },
              },
            ],
          });
        }
      },

      'VariableDeclaration[kind="await using"]'(
        node: TSESTree.VariableDeclaration,
      ): void {
        for (const declarator of node.declarations) {
          const init = declarator.init;
          if (init == null) {
            continue;
          }
          const type = services.getTypeAtLocation(init);
          if (isTypeAnyType(type)) {
            continue;
          }

          const hasAsyncDisposeSymbol = tsutils
            .unionConstituents(type)
            .some(
              typePart =>
                tsutils.getWellKnownSymbolPropertyOfType(
                  typePart,
                  'asyncDispose',
                  checker,
                ) != null,
            );

          if (!hasAsyncDisposeSymbol) {
            context.report({
              node: init,
              messageId: 'awaitUsingOfNonAsyncDisposable',
              // let the user figure out what to do if there's
              // await using a = b, c = d, e = f;
              // it's rare and not worth the complexity to handle.
              ...getFixOrSuggest({
                fixOrSuggest:
                  node.declarations.length === 1 ? 'suggest' : 'none',

                suggestion: {
                  messageId: 'removeAwait',
                  fix(fixer): TSESLint.RuleFix {
                    const awaitToken = nullThrows(
                      context.sourceCode.getFirstToken(node, isAwaitKeyword),
                      NullThrowsReasons.MissingToken('await', 'await using'),
                    );
                    return fixer.remove(awaitToken);
                  },
                },
              }),
            });
          }
        }
      },
    };
  }

isInvalidPromiseAggregatorInput(checker: ts.TypeChecker, node: ts.Node, type: ts.Type): boolean

Parameters:

  • checker ts.TypeChecker
  • node ts.Node
  • type ts.Type

Returns: boolean

Calls:

  • isIterable
  • tsutils.unionConstituents
  • getValueTypesOfArrayLike
  • containsNonAwaitableType

Internal Comments:

// non array/tuple/iterable types already show up as a type error

Code
function isInvalidPromiseAggregatorInput(
  checker: ts.TypeChecker,
  node: ts.Node,
  type: ts.Type,
): boolean {
  // non array/tuple/iterable types already show up as a type error
  if (!isIterable(type, checker)) {
    return false;
  }

  for (const part of tsutils.unionConstituents(type)) {
    const valueTypes = getValueTypesOfArrayLike(part, checker);

    if (valueTypes != null) {
      for (const typeArgument of valueTypes) {
        if (containsNonAwaitableType(typeArgument, node, checker)) {
          return true;
        }
      }
    }
  }

  return false;
}

getValueTypesOfArrayLike(type: ts.Type, checker: ts.TypeChecker): readonly ts.Type[] | null

Parameters:

  • type ts.Type
  • checker ts.TypeChecker

Returns: readonly ts.Type[] | null

Calls:

  • checker.isTupleType
  • checker.getTypeArguments
  • checker.isArrayLikeType
  • nullThrows (from ../util)
  • type.getNumberIndexType
  • tsutils.isTypeReference
  • checker.getTypeArguments(type).slice

Internal Comments:

// `Iterable<...>`

Code
function getValueTypesOfArrayLike(
  type: ts.Type,
  checker: ts.TypeChecker,
): readonly ts.Type[] | null {
  if (checker.isTupleType(type)) {
    return checker.getTypeArguments(type);
  }

  if (checker.isArrayLikeType(type)) {
    return [
      nullThrows(
        type.getNumberIndexType(),
        'number index type should exist on an array-like',
      ),
    ];
  }

  // `Iterable<...>`
  if (tsutils.isTypeReference(type)) {
    return checker.getTypeArguments(type).slice(0, 1);
  }

  return null;
}

isAlwaysNonAwaitableType(type: ts.Type, node: ts.Node, checker: ts.TypeChecker): boolean

Parameters:

  • type ts.Type
  • node ts.Node
  • checker ts.TypeChecker

Returns: boolean

Calls:

  • tsutils .unionConstituents(type) .every
  • needsToBeAwaited (from ../util)
Code
function isAlwaysNonAwaitableType(
  type: ts.Type,
  node: ts.Node,
  checker: ts.TypeChecker,
): boolean {
  return tsutils
    .unionConstituents(type)
    .every(
      typeArgumentPart =>
        needsToBeAwaited(checker, node, typeArgumentPart) === Awaitable.Never,
    );
}

containsNonAwaitableType(type: ts.Type, node: ts.Node, checker: ts.TypeChecker): boolean

Parameters:

  • type ts.Type
  • node ts.Node
  • checker ts.TypeChecker

Returns: boolean

Calls:

  • tsutils .unionConstituents(type) .some
  • needsToBeAwaited (from ../util)
Code
function containsNonAwaitableType(
  type: ts.Type,
  node: ts.Node,
  checker: ts.TypeChecker,
): boolean {
  return tsutils
    .unionConstituents(type)
    .some(
      typeArgumentPart =>
        needsToBeAwaited(checker, node, typeArgumentPart) === Awaitable.Never,
    );
}

isIterable(type: ts.Type, checker: ts.TypeChecker): boolean

Parameters:

  • type ts.Type
  • checker ts.TypeChecker

Returns: boolean

Calls:

  • tsutils .unionConstituents(type) .every
  • tsutils.getWellKnownSymbolPropertyOfType
Code
function isIterable(type: ts.Type, checker: ts.TypeChecker): boolean {
  return tsutils
    .unionConstituents(type)
    .every(
      part =>
        !!tsutils.getWellKnownSymbolPropertyOfType(part, 'iterator', checker),
    );
}

Type Aliases

MessageId

type MessageId = | 'await'
  | 'awaitUsingOfNonAsyncDisposable'
  | 'convertToOrdinaryFor'
  | 'forAwaitOfNonAsyncIterable'
  | 'invalidPromiseAggregatorInput'
  | 'removeAwait';

Generated by Syntax Scribe