Skip to content

⬅️ Back to Table of Contents

📄 no-floating-promises

📊 Analysis Summary

Metric Count
🔧 Functions 12
📦 Imports 18
📊 Variables & Constants 5
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/no-floating-promises.ts

📤 Default Export

export default createRule<Options, MessageId>({ ... })
Property Value
name 'no-floating-promises'
meta.type 'problem'
meta.docs.description 'Require Promise-like statements to be handled appropriately'
meta.docs.recommended 'recommended'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.floating messageBase
meta.messages.floatingFixAwait 'Add await operator.'
meta.messages.floatingFixVoid 'Add void operator to ignore.'
meta.messages.floatingPromiseArray messagePromiseArray
meta.messages.floatingPromiseArrayVoid messagePromiseArrayVoid
meta.messages.floatingUselessRejectionHandler ${messageBase} ${messageRejectionHandler}
meta.messages.floatingUselessRejectionHandlerVoid ${messageBaseVoid} ${messageRejectionHandler}
meta.messages.floatingVoid messageBaseVoid
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowForKnownSafeCalls: { ...readonlynessOptionsSchema...
defaultOptions [ { allowForKnownSafeCalls: readonlynessOptionsDefaults.allow, allowForKnownSafePromises: readonlynessOptionsDefaults...

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
TypeOrValueSpecifier ../util
createRule ../util
getOperatorPrecedenceForNode ../util
getParserServices ../util
isBuiltinSymbolLike ../util
isParenthesized ../util
OperatorPrecedence ../util
readonlynessOptionsDefaults ../util
readonlynessOptionsSchema ../util
skipChainExpression ../util
typeMatchesSomeSpecifier ../util
valueMatchesSomeSpecifier ../util
parseCatchCall ../util/promiseUtils
parseFinallyCall ../util/promiseUtils
parseThenCall ../util/promiseUtils

Variables & Constants

Name Type Kind Value Exported
messageBase "Promises must be awaited, end with a... const 'Promises must be awaited, end with a call to .catch, or end with a call to ....
messageBaseVoid string const 'Promises must be awaited, end with a call to .catch, end with a call to .the...
messageRejectionHandler "A rejection handler that is not a fu... const 'A rejection handler that is not a function will be ignored.'
messagePromiseArray "An array of Promises may be unintent... const "An array of Promises may be unintentional. Consider handling the promises' f...
messagePromiseArrayVoid string const "An array of Promises may be unintentional. Consider handling the promises' f...

Functions

create(context: any, [options]: any): { ArrowFunctionExpression(node: any): void; ExpressionState…

Parameters:

  • context any
  • [options] any

Returns: { ArrowFunctionExpression(node: any): void; ExpressionStatement(node: any): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • checkNode
  • isAsyncIife
  • skipChainExpression (from ../util)
  • isKnownSafePromiseCall
  • isUnhandledPromise
  • context.report
  • isParenthesized (from ../util)
  • getOperatorPrecedenceForNode (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfterRange
  • addAwait
  • fixer.replaceTextRange
  • services.getTypeAtLocation
  • valueMatchesSomeSpecifier (from ../util)
  • typeMatchesSomeSpecifier (from ../util)
  • services.program .getTypeChecker() .getTypeAtLocation( services.esTreeNodeToTSNodeMap.get(rejectionHandler), ) .getCallSignatures
  • node.expressions .map(item => isUnhandledPromise(checker, item)) .find
  • services.esTreeNodeToTSNodeMap.get
  • isPromiseArray
  • isPromiseLike
  • parseCatchCall (from ../util/promiseUtils)
  • parseThenCall (from ../util/promiseUtils)
  • isValidRejectionHandler
  • parseFinallyCall (from ../util/promiseUtils)
  • getTypeAtLocation
  • tsutils .unionConstituents(type) .map
  • checker.getApparentType
  • checker.isArrayType
  • checker.getTypeArguments
  • checker.isTupleType
  • checker.getTypeAtLocation
  • tsutils.unionConstituents
  • typeParts.some
  • isBuiltinSymbolLike (from ../util)
  • ty.getProperty
  • checker.getTypeOfSymbolAtLocation
  • hasMatchingSignature
  • isFunctionParam

Internal Comments:

// TODO: #5439 (x2)
/* eslint-disable @typescript-eslint/no-non-null-assertion */ (x2)
/* eslint-enable @typescript-eslint/no-non-null-assertion */
// First, check expressions whose resulting types may not be promise-like
// Any child in a comma expression could return a potentially unhandled
// promise, so we check them all regardless of whether the final returned
// value is promise-like.
// Similarly, a `void` expression always returns undefined, so we need to
// see what's inside it without checking the type of the overall expression.
// Check the type. At this point it can't be unhandled if it isn't a promise
// or array thereof.
// await expression addresses promises, but not promise arrays.
// you would think this wouldn't be strictly necessary, since we're
// anyway checking the type of the expression, but, unfortunately TS
// reports the result of `await (promise as Promise<number> & number)`
// as `Promise<number> & number` instead of `number`.
// If the outer expression is a call, a `.catch()` or `.then()` with (x2)
// rejection handler handles the promise. (x2)
// All other cases are unhandled.
// We must be getting the promise-like value from one of the branches of the (x2)
// ternary. Check them directly. (x2)
// Anything else is unhandled.
// The highest priority is to allow anything allowlisted
// Otherwise, we always consider the built-in Promise to be Promise-like... (x2)
// ...and only check all Thenables if explicitly told to
// Modified from tsutils.isThenable() to only consider thenables which can be
// rejected/caught via a second parameter. Original source (MIT licensed):
//
//   https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125

Code
create(context, [options]) {
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();
    const { checkThenables } = options;

    // TODO: #5439
    /* eslint-disable @typescript-eslint/no-non-null-assertion */
    const allowForKnownSafePromises = options.allowForKnownSafePromises!;
    const allowForKnownSafeCalls = options.allowForKnownSafeCalls!;
    /* eslint-enable @typescript-eslint/no-non-null-assertion */

    return {
      ArrowFunctionExpression(node): void {
        if (node.body.type === AST_NODE_TYPES.UnaryExpression) {
          checkNode(node.body, node.body);
        }
      },

      ExpressionStatement(node): void {
        if (options.ignoreIIFE && isAsyncIife(node)) {
          return;
        }

        const expression = skipChainExpression(node.expression);

        checkNode(node, expression);
      },
    };

    function checkNode(
      node: TSESTree.Expression | TSESTree.ExpressionStatement,
      expression: TSESTree.Expression,
    ): void {
      if (isKnownSafePromiseCall(expression)) {
        return;
      }

      const { isUnhandled, nonFunctionHandler, promiseArray } =
        isUnhandledPromise(checker, expression);

      if (isUnhandled) {
        if (promiseArray) {
          context.report({
            node,
            messageId: options.ignoreVoid
              ? 'floatingPromiseArrayVoid'
              : 'floatingPromiseArray',
          });
        } else if (options.ignoreVoid) {
          context.report({
            node,
            messageId: nonFunctionHandler
              ? 'floatingUselessRejectionHandlerVoid'
              : 'floatingVoid',
            suggest: [
              {
                messageId: 'floatingFixVoid',
                fix(fixer): TSESLint.RuleFix | TSESLint.RuleFix[] {
                  if (
                    isParenthesized(expression, context.sourceCode) ||
                    getOperatorPrecedenceForNode(expression) >
                      OperatorPrecedence.Unary
                  ) {
                    return fixer.insertTextBefore(node, 'void ');
                  }
                  return [
                    fixer.insertTextBefore(node, 'void ('),
                    fixer.insertTextAfterRange(
                      [expression.range[1], expression.range[1]],
                      ')',
                    ),
                  ];
                },
              },
              {
                messageId: 'floatingFixAwait',
                fix: (fixer): TSESLint.RuleFix | TSESLint.RuleFix[] =>
                  addAwait(fixer, expression, node),
              },
            ],
          });
        } else {
          context.report({
            node,
            messageId: nonFunctionHandler
              ? 'floatingUselessRejectionHandler'
              : 'floating',
            suggest: [
              {
                messageId: 'floatingFixAwait',
                fix: (fixer): TSESLint.RuleFix | TSESLint.RuleFix[] =>
                  addAwait(fixer, expression, node),
              },
            ],
          });
        }
      }
    }

    function addAwait(
      fixer: TSESLint.RuleFixer,
      expression: TSESTree.Expression,
      node: TSESTree.Expression | TSESTree.ExpressionStatement,
    ): TSESLint.RuleFix | TSESLint.RuleFix[] {
      if (
        expression.type === AST_NODE_TYPES.UnaryExpression &&
        expression.operator === 'void'
      ) {
        return fixer.replaceTextRange(
          [expression.range[0], expression.range[0] + 4],
          'await',
        );
      }
      if (
        isParenthesized(expression, context.sourceCode) ||
        getOperatorPrecedenceForNode(expression) > OperatorPrecedence.Unary
      ) {
        return fixer.insertTextBefore(node, 'await ');
      }
      return [
        fixer.insertTextBefore(node, 'await ('),
        fixer.insertTextAfterRange(
          [expression.range[1], expression.range[1]],
          ')',
        ),
      ];
    }

    function isKnownSafePromiseCall(node: TSESTree.Node): boolean {
      if (node.type !== AST_NODE_TYPES.CallExpression) {
        return false;
      }

      const type = services.getTypeAtLocation(node.callee);

      if (
        valueMatchesSomeSpecifier(
          node.callee,
          allowForKnownSafeCalls,
          services.program,
          type,
        )
      ) {
        return true;
      }

      return typeMatchesSomeSpecifier(
        type,
        allowForKnownSafeCalls,
        services.program,
      );
    }

    function isAsyncIife(node: TSESTree.ExpressionStatement): boolean {
      if (node.expression.type !== AST_NODE_TYPES.CallExpression) {
        return false;
      }

      return (
        node.expression.callee.type ===
          AST_NODE_TYPES.ArrowFunctionExpression ||
        node.expression.callee.type === AST_NODE_TYPES.FunctionExpression
      );
    }

    function isValidRejectionHandler(rejectionHandler: TSESTree.Node): boolean {
      return (
        services.program
          .getTypeChecker()
          .getTypeAtLocation(
            services.esTreeNodeToTSNodeMap.get(rejectionHandler),
          )
          .getCallSignatures().length > 0
      );
    }

    function isUnhandledPromise(
      checker: ts.TypeChecker,
      node: TSESTree.Node,
    ): {
      isUnhandled: boolean;
      nonFunctionHandler?: boolean;
      promiseArray?: boolean;
    } {
      if (node.type === AST_NODE_TYPES.AssignmentExpression) {
        return { isUnhandled: false };
      }

      // First, check expressions whose resulting types may not be promise-like
      if (node.type === AST_NODE_TYPES.SequenceExpression) {
        // Any child in a comma expression could return a potentially unhandled
        // promise, so we check them all regardless of whether the final returned
        // value is promise-like.
        return (
          node.expressions
            .map(item => isUnhandledPromise(checker, item))
            .find(result => result.isUnhandled) ?? { isUnhandled: false }
        );
      }

      if (
        !options.ignoreVoid &&
        node.type === AST_NODE_TYPES.UnaryExpression &&
        node.operator === 'void'
      ) {
        // Similarly, a `void` expression always returns undefined, so we need to
        // see what's inside it without checking the type of the overall expression.
        return isUnhandledPromise(checker, node.argument);
      }

      const tsNode = services.esTreeNodeToTSNodeMap.get(node);

      // Check the type. At this point it can't be unhandled if it isn't a promise
      // or array thereof.

      if (isPromiseArray(tsNode)) {
        return { isUnhandled: true, promiseArray: true };
      }

      // await expression addresses promises, but not promise arrays.
      if (node.type === AST_NODE_TYPES.AwaitExpression) {
        // you would think this wouldn't be strictly necessary, since we're
        // anyway checking the type of the expression, but, unfortunately TS
        // reports the result of `await (promise as Promise<number> & number)`
        // as `Promise<number> & number` instead of `number`.
        return { isUnhandled: false };
      }

      if (!isPromiseLike(tsNode)) {
        return { isUnhandled: false };
      }

      if (node.type === AST_NODE_TYPES.CallExpression) {
        // If the outer expression is a call, a `.catch()` or `.then()` with
        // rejection handler handles the promise.

        const promiseHandlingMethodCall =
          parseCatchCall(node, context) ?? parseThenCall(node, context);
        if (promiseHandlingMethodCall != null) {
          const onRejected = promiseHandlingMethodCall.onRejected;
          if (onRejected != null) {
            if (isValidRejectionHandler(onRejected)) {
              return { isUnhandled: false };
            }
            return { isUnhandled: true, nonFunctionHandler: true };
          }
          return { isUnhandled: true };
        }

        const promiseFinallyCall = parseFinallyCall(node, context);

        if (promiseFinallyCall != null) {
          return isUnhandledPromise(checker, promiseFinallyCall.object);
        }

        // All other cases are unhandled.
        return { isUnhandled: true };
      }

      if (node.type === AST_NODE_TYPES.ConditionalExpression) {
        // We must be getting the promise-like value from one of the branches of the
        // ternary. Check them directly.
        const alternateResult = isUnhandledPromise(checker, node.alternate);
        if (alternateResult.isUnhandled) {
          return alternateResult;
        }
        return isUnhandledPromise(checker, node.consequent);
      }

      if (node.type === AST_NODE_TYPES.LogicalExpression) {
        const leftResult = isUnhandledPromise(checker, node.left);
        if (leftResult.isUnhandled) {
          return leftResult;
        }
        return isUnhandledPromise(checker, node.right);
      }

      // Anything else is unhandled.
      return { isUnhandled: true };
    }

    function isPromiseArray(node: ts.Node): boolean {
      const type = getTypeAtLocation(checker, node);

      if (type == null) {
        return false;
      }

      for (const ty of tsutils
        .unionConstituents(type)
        .map(t => checker.getApparentType(t))) {
        if (checker.isArrayType(ty)) {
          const arrayType = checker.getTypeArguments(ty)[0];
          if (isPromiseLike(node, arrayType)) {
            return true;
          }
        }

        if (checker.isTupleType(ty)) {
          for (const tupleElementType of checker.getTypeArguments(ty)) {
            if (isPromiseLike(node, tupleElementType)) {
              return true;
            }
          }
        }
      }
      return false;
    }

    function isPromiseLike(node: ts.Node, type?: ts.Type): boolean {
      type ??= checker.getTypeAtLocation(node);

      // The highest priority is to allow anything allowlisted
      if (
        typeMatchesSomeSpecifier(
          type,
          allowForKnownSafePromises,
          services.program,
        )
      ) {
        return false;
      }

      // Otherwise, we always consider the built-in Promise to be Promise-like...
      const typeParts = tsutils.unionConstituents(
        checker.getApparentType(type),
      );
      if (
        typeParts.some(typePart =>
          isBuiltinSymbolLike(services.program, typePart, 'Promise'),
        )
      ) {
        return true;
      }

      // ...and only check all Thenables if explicitly told to
      if (!checkThenables) {
        return false;
      }

      // Modified from tsutils.isThenable() to only consider thenables which can be
      // rejected/caught via a second parameter. Original source (MIT licensed):
      //
      //   https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125
      for (const ty of typeParts) {
        const then = ty.getProperty('then');
        if (then == null) {
          continue;
        }

        const thenType = checker.getTypeOfSymbolAtLocation(then, node);
        if (
          hasMatchingSignature(
            thenType,
            signature =>
              signature.parameters.length >= 2 &&
              isFunctionParam(checker, signature.parameters[0], node) &&
              isFunctionParam(checker, signature.parameters[1], node),
          )
        ) {
          return true;
        }
      }
      return false;
    }
  }

hasMatchingSignature(type: ts.Type, matcher: (signature: ts.Signature) => boolean): boolean

Parameters:

  • type ts.Type
  • matcher (signature: ts.Signature) => boolean

Returns: boolean

Calls:

  • tsutils.unionConstituents
  • t.getCallSignatures().some
Code
function hasMatchingSignature(
  type: ts.Type,
  matcher: (signature: ts.Signature) => boolean,
): boolean {
  for (const t of tsutils.unionConstituents(type)) {
    if (t.getCallSignatures().some(matcher)) {
      return true;
    }
  }

  return false;
}

isFunctionParam(checker: ts.TypeChecker, param: ts.Symbol, node: ts.Node): boolean

Parameters:

  • checker ts.TypeChecker
  • param ts.Symbol
  • node ts.Node

Returns: boolean

Calls:

  • checker.getApparentType
  • checker.getTypeOfSymbolAtLocation
  • tsutils.unionConstituents
  • t.getCallSignatures
Code
function isFunctionParam(
  checker: ts.TypeChecker,
  param: ts.Symbol,
  node: ts.Node,
): boolean {
  const type: ts.Type | undefined = checker.getApparentType(
    checker.getTypeOfSymbolAtLocation(param, node),
  );
  for (const t of tsutils.unionConstituents(type)) {
    if (t.getCallSignatures().length !== 0) {
      return true;
    }
  }
  return false;
}

getTypeAtLocation(checker: ts.TypeChecker, node: ts.Node): ts.Type | null

Parameters:

  • checker ts.TypeChecker
  • node ts.Node

Returns: ts.Type | null

Calls:

  • checker.getTypeAtLocation

Internal Comments:

// Workaround for https://github.com/typescript-eslint/typescript-eslint/issues/11947

Code
function getTypeAtLocation(
  checker: ts.TypeChecker,
  node: ts.Node,
): ts.Type | null {
  try {
    return checker.getTypeAtLocation(node);
  } catch {
    // Workaround for https://github.com/typescript-eslint/typescript-eslint/issues/11947
    return null;
  }
}

Internal helpers

Declared inside another function in this file.

checkNode(node: TSESTree.Expression | TSESTree.Expressi…, expression: TSESTree.Expression): void

Parameters:

  • node TSESTree.Expression | TSESTree.ExpressionStatement
  • expression TSESTree.Expression

Returns: void

Calls:

  • isKnownSafePromiseCall
  • isUnhandledPromise
  • context.report
  • isParenthesized (from ../util)
  • getOperatorPrecedenceForNode (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfterRange
  • addAwait
Code
function checkNode(
      node: TSESTree.Expression | TSESTree.ExpressionStatement,
      expression: TSESTree.Expression,
    ): void {
      if (isKnownSafePromiseCall(expression)) {
        return;
      }

      const { isUnhandled, nonFunctionHandler, promiseArray } =
        isUnhandledPromise(checker, expression);

      if (isUnhandled) {
        if (promiseArray) {
          context.report({
            node,
            messageId: options.ignoreVoid
              ? 'floatingPromiseArrayVoid'
              : 'floatingPromiseArray',
          });
        } else if (options.ignoreVoid) {
          context.report({
            node,
            messageId: nonFunctionHandler
              ? 'floatingUselessRejectionHandlerVoid'
              : 'floatingVoid',
            suggest: [
              {
                messageId: 'floatingFixVoid',
                fix(fixer): TSESLint.RuleFix | TSESLint.RuleFix[] {
                  if (
                    isParenthesized(expression, context.sourceCode) ||
                    getOperatorPrecedenceForNode(expression) >
                      OperatorPrecedence.Unary
                  ) {
                    return fixer.insertTextBefore(node, 'void ');
                  }
                  return [
                    fixer.insertTextBefore(node, 'void ('),
                    fixer.insertTextAfterRange(
                      [expression.range[1], expression.range[1]],
                      ')',
                    ),
                  ];
                },
              },
              {
                messageId: 'floatingFixAwait',
                fix: (fixer): TSESLint.RuleFix | TSESLint.RuleFix[] =>
                  addAwait(fixer, expression, node),
              },
            ],
          });
        } else {
          context.report({
            node,
            messageId: nonFunctionHandler
              ? 'floatingUselessRejectionHandler'
              : 'floating',
            suggest: [
              {
                messageId: 'floatingFixAwait',
                fix: (fixer): TSESLint.RuleFix | TSESLint.RuleFix[] =>
                  addAwait(fixer, expression, node),
              },
            ],
          });
        }
      }
    }

addAwait(fixer: TSESLint.RuleFixer, expression: TSESTree.Expression, node: TSESTree.Expression | TSESTree.Expressi…): TSESLint.RuleFix | TSESLint.RuleFix[]

Parameters:

  • fixer TSESLint.RuleFixer
  • expression TSESTree.Expression
  • node TSESTree.Expression | TSESTree.ExpressionStatement

Returns: TSESLint.RuleFix | TSESLint.RuleFix[]

Calls:

  • fixer.replaceTextRange
  • isParenthesized (from ../util)
  • getOperatorPrecedenceForNode (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfterRange
Code
function addAwait(
      fixer: TSESLint.RuleFixer,
      expression: TSESTree.Expression,
      node: TSESTree.Expression | TSESTree.ExpressionStatement,
    ): TSESLint.RuleFix | TSESLint.RuleFix[] {
      if (
        expression.type === AST_NODE_TYPES.UnaryExpression &&
        expression.operator === 'void'
      ) {
        return fixer.replaceTextRange(
          [expression.range[0], expression.range[0] + 4],
          'await',
        );
      }
      if (
        isParenthesized(expression, context.sourceCode) ||
        getOperatorPrecedenceForNode(expression) > OperatorPrecedence.Unary
      ) {
        return fixer.insertTextBefore(node, 'await ');
      }
      return [
        fixer.insertTextBefore(node, 'await ('),
        fixer.insertTextAfterRange(
          [expression.range[1], expression.range[1]],
          ')',
        ),
      ];
    }

isKnownSafePromiseCall(node: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node

Returns: boolean

Calls:

  • services.getTypeAtLocation
  • valueMatchesSomeSpecifier (from ../util)
  • typeMatchesSomeSpecifier (from ../util)
Code
function isKnownSafePromiseCall(node: TSESTree.Node): boolean {
      if (node.type !== AST_NODE_TYPES.CallExpression) {
        return false;
      }

      const type = services.getTypeAtLocation(node.callee);

      if (
        valueMatchesSomeSpecifier(
          node.callee,
          allowForKnownSafeCalls,
          services.program,
          type,
        )
      ) {
        return true;
      }

      return typeMatchesSomeSpecifier(
        type,
        allowForKnownSafeCalls,
        services.program,
      );
    }

isAsyncIife(node: TSESTree.ExpressionStatement): boolean

Parameters:

  • node TSESTree.ExpressionStatement

Returns: boolean

Code
function isAsyncIife(node: TSESTree.ExpressionStatement): boolean {
      if (node.expression.type !== AST_NODE_TYPES.CallExpression) {
        return false;
      }

      return (
        node.expression.callee.type ===
          AST_NODE_TYPES.ArrowFunctionExpression ||
        node.expression.callee.type === AST_NODE_TYPES.FunctionExpression
      );
    }

isValidRejectionHandler(rejectionHandler: TSESTree.Node): boolean

Parameters:

  • rejectionHandler TSESTree.Node

Returns: boolean

Calls:

  • services.program .getTypeChecker() .getTypeAtLocation( services.esTreeNodeToTSNodeMap.get(rejectionHandler), ) .getCallSignatures
Code
function isValidRejectionHandler(rejectionHandler: TSESTree.Node): boolean {
      return (
        services.program
          .getTypeChecker()
          .getTypeAtLocation(
            services.esTreeNodeToTSNodeMap.get(rejectionHandler),
          )
          .getCallSignatures().length > 0
      );
    }

isUnhandledPromise(checker: ts.TypeChecker, node: TSESTree.Node): { isUnhandled: boolean; nonFunctionHandler?: boolean; promi…

Parameters:

  • checker ts.TypeChecker
  • node TSESTree.Node

Returns: { isUnhandled: boolean; nonFunctionHandler?: boolean; promiseArray?: boolean; }

Calls:

  • node.expressions .map(item => isUnhandledPromise(checker, item)) .find
  • isUnhandledPromise
  • services.esTreeNodeToTSNodeMap.get
  • isPromiseArray
  • isPromiseLike
  • parseCatchCall (from ../util/promiseUtils)
  • parseThenCall (from ../util/promiseUtils)
  • isValidRejectionHandler
  • parseFinallyCall (from ../util/promiseUtils)

Internal Comments:

// First, check expressions whose resulting types may not be promise-like
// Any child in a comma expression could return a potentially unhandled
// promise, so we check them all regardless of whether the final returned
// value is promise-like.
// Similarly, a `void` expression always returns undefined, so we need to
// see what's inside it without checking the type of the overall expression.
// Check the type. At this point it can't be unhandled if it isn't a promise
// or array thereof.
// await expression addresses promises, but not promise arrays.
// you would think this wouldn't be strictly necessary, since we're
// anyway checking the type of the expression, but, unfortunately TS
// reports the result of `await (promise as Promise<number> & number)`
// as `Promise<number> & number` instead of `number`.
// If the outer expression is a call, a `.catch()` or `.then()` with (x2)
// rejection handler handles the promise. (x2)
// All other cases are unhandled.
// We must be getting the promise-like value from one of the branches of the (x2)
// ternary. Check them directly. (x2)
// Anything else is unhandled.

Code
function isUnhandledPromise(
      checker: ts.TypeChecker,
      node: TSESTree.Node,
    ): {
      isUnhandled: boolean;
      nonFunctionHandler?: boolean;
      promiseArray?: boolean;
    } {
      if (node.type === AST_NODE_TYPES.AssignmentExpression) {
        return { isUnhandled: false };
      }

      // First, check expressions whose resulting types may not be promise-like
      if (node.type === AST_NODE_TYPES.SequenceExpression) {
        // Any child in a comma expression could return a potentially unhandled
        // promise, so we check them all regardless of whether the final returned
        // value is promise-like.
        return (
          node.expressions
            .map(item => isUnhandledPromise(checker, item))
            .find(result => result.isUnhandled) ?? { isUnhandled: false }
        );
      }

      if (
        !options.ignoreVoid &&
        node.type === AST_NODE_TYPES.UnaryExpression &&
        node.operator === 'void'
      ) {
        // Similarly, a `void` expression always returns undefined, so we need to
        // see what's inside it without checking the type of the overall expression.
        return isUnhandledPromise(checker, node.argument);
      }

      const tsNode = services.esTreeNodeToTSNodeMap.get(node);

      // Check the type. At this point it can't be unhandled if it isn't a promise
      // or array thereof.

      if (isPromiseArray(tsNode)) {
        return { isUnhandled: true, promiseArray: true };
      }

      // await expression addresses promises, but not promise arrays.
      if (node.type === AST_NODE_TYPES.AwaitExpression) {
        // you would think this wouldn't be strictly necessary, since we're
        // anyway checking the type of the expression, but, unfortunately TS
        // reports the result of `await (promise as Promise<number> & number)`
        // as `Promise<number> & number` instead of `number`.
        return { isUnhandled: false };
      }

      if (!isPromiseLike(tsNode)) {
        return { isUnhandled: false };
      }

      if (node.type === AST_NODE_TYPES.CallExpression) {
        // If the outer expression is a call, a `.catch()` or `.then()` with
        // rejection handler handles the promise.

        const promiseHandlingMethodCall =
          parseCatchCall(node, context) ?? parseThenCall(node, context);
        if (promiseHandlingMethodCall != null) {
          const onRejected = promiseHandlingMethodCall.onRejected;
          if (onRejected != null) {
            if (isValidRejectionHandler(onRejected)) {
              return { isUnhandled: false };
            }
            return { isUnhandled: true, nonFunctionHandler: true };
          }
          return { isUnhandled: true };
        }

        const promiseFinallyCall = parseFinallyCall(node, context);

        if (promiseFinallyCall != null) {
          return isUnhandledPromise(checker, promiseFinallyCall.object);
        }

        // All other cases are unhandled.
        return { isUnhandled: true };
      }

      if (node.type === AST_NODE_TYPES.ConditionalExpression) {
        // We must be getting the promise-like value from one of the branches of the
        // ternary. Check them directly.
        const alternateResult = isUnhandledPromise(checker, node.alternate);
        if (alternateResult.isUnhandled) {
          return alternateResult;
        }
        return isUnhandledPromise(checker, node.consequent);
      }

      if (node.type === AST_NODE_TYPES.LogicalExpression) {
        const leftResult = isUnhandledPromise(checker, node.left);
        if (leftResult.isUnhandled) {
          return leftResult;
        }
        return isUnhandledPromise(checker, node.right);
      }

      // Anything else is unhandled.
      return { isUnhandled: true };
    }

isPromiseArray(node: ts.Node): boolean

Parameters:

  • node ts.Node

Returns: boolean

Calls:

  • getTypeAtLocation
  • tsutils .unionConstituents(type) .map
  • checker.getApparentType
  • checker.isArrayType
  • checker.getTypeArguments
  • isPromiseLike
  • checker.isTupleType
Code
function isPromiseArray(node: ts.Node): boolean {
      const type = getTypeAtLocation(checker, node);

      if (type == null) {
        return false;
      }

      for (const ty of tsutils
        .unionConstituents(type)
        .map(t => checker.getApparentType(t))) {
        if (checker.isArrayType(ty)) {
          const arrayType = checker.getTypeArguments(ty)[0];
          if (isPromiseLike(node, arrayType)) {
            return true;
          }
        }

        if (checker.isTupleType(ty)) {
          for (const tupleElementType of checker.getTypeArguments(ty)) {
            if (isPromiseLike(node, tupleElementType)) {
              return true;
            }
          }
        }
      }
      return false;
    }

isPromiseLike(node: ts.Node, type: ts.Type): boolean

Parameters:

  • node ts.Node
  • type ts.Type

Returns: boolean

Calls:

  • checker.getTypeAtLocation
  • typeMatchesSomeSpecifier (from ../util)
  • tsutils.unionConstituents
  • checker.getApparentType
  • typeParts.some
  • isBuiltinSymbolLike (from ../util)
  • ty.getProperty
  • checker.getTypeOfSymbolAtLocation
  • hasMatchingSignature
  • isFunctionParam

Internal Comments:

// The highest priority is to allow anything allowlisted
// Otherwise, we always consider the built-in Promise to be Promise-like... (x2)
// ...and only check all Thenables if explicitly told to
// Modified from tsutils.isThenable() to only consider thenables which can be
// rejected/caught via a second parameter. Original source (MIT licensed):
//
//   https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125

Code
function isPromiseLike(node: ts.Node, type?: ts.Type): boolean {
      type ??= checker.getTypeAtLocation(node);

      // The highest priority is to allow anything allowlisted
      if (
        typeMatchesSomeSpecifier(
          type,
          allowForKnownSafePromises,
          services.program,
        )
      ) {
        return false;
      }

      // Otherwise, we always consider the built-in Promise to be Promise-like...
      const typeParts = tsutils.unionConstituents(
        checker.getApparentType(type),
      );
      if (
        typeParts.some(typePart =>
          isBuiltinSymbolLike(services.program, typePart, 'Promise'),
        )
      ) {
        return true;
      }

      // ...and only check all Thenables if explicitly told to
      if (!checkThenables) {
        return false;
      }

      // Modified from tsutils.isThenable() to only consider thenables which can be
      // rejected/caught via a second parameter. Original source (MIT licensed):
      //
      //   https://github.com/ajafff/tsutils/blob/49d0d31050b44b81e918eae4fbaf1dfe7b7286af/util/type.ts#L95-L125
      for (const ty of typeParts) {
        const then = ty.getProperty('then');
        if (then == null) {
          continue;
        }

        const thenType = checker.getTypeOfSymbolAtLocation(then, node);
        if (
          hasMatchingSignature(
            thenType,
            signature =>
              signature.parameters.length >= 2 &&
              isFunctionParam(checker, signature.parameters[0], node) &&
              isFunctionParam(checker, signature.parameters[1], node),
          )
        ) {
          return true;
        }
      }
      return false;
    }

Type Aliases

Options

type Options = [
  {
    allowForKnownSafeCalls?: TypeOrValueSpecifier[];
    allowForKnownSafePromises?: TypeOrValueSpecifier[];
    checkThenables?: boolean;
    ignoreIIFE?: boolean;
    ignoreVoid?: boolean;
  },
];

MessageId

type MessageId = | 'floating'
  | 'floatingFixAwait'
  | 'floatingFixVoid'
  | 'floatingPromiseArray'
  | 'floatingPromiseArrayVoid'
  | 'floatingUselessRejectionHandler'
  | 'floatingUselessRejectionHandlerVoid'
  | 'floatingVoid';

Generated by Syntax Scribe