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