Skip to content

⬅️ Back to Table of Contents

πŸ“„ use-unknown-in-catch-callback-variable

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 8
πŸ“¦ Imports 10
πŸ“Š Variables & Constants 1
πŸ“‘ Type Aliases 1

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/use-unknown-in-catch-callback-variable.ts

πŸ“€ Default Export

export default createRule<[], MessageIds>({ ... })
Property Value
name 'use-unknown-in-catch-callback-variable'
meta.type 'suggestion'
meta.docs.description 'Enforce typing arguments in Promise rejection callbacks as unknown'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.addUnknownRestTypeAnnotationSuggestion 'Add an explicit : [unknown] type annotation to the rejection callback rest variable.'
meta.messages.addUnknownTypeAnnotationSuggestion 'Add an explicit : unknown type annotation to the rejection callback variable.'
meta.messages.useUnknown useUnknownMessageBase
meta.messages.useUnknownArrayDestructuringPattern ${useUnknownMessageBase} The thrown error may not be iterable.
meta.messages.useUnknownObjectDestructuringPattern ${ useUnknownMessageBase } The thrown error may be nullable, or may not have the expected shape.
meta.messages.wrongRestTypeAnnotationSuggestion 'Change existing type annotation to : [unknown].'
meta.messages.wrongTypeAnnotationSuggestion 'Change existing type annotation to : unknown.'
meta.schema []
defaultOptions []

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
ReportDescriptor @typescript-eslint/utils/ts-eslint
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getParserServices ../util
getStaticMemberAccessValue ../util
isParenlessArrowFunction ../util
isRestParameterDeclaration ../util
nullThrows ../util

Variables & Constants

Name Type Kind Value Exported
useUnknownMessageBase "Prefer the safe: unknownfor a{...| const |'Prefer the safe : unknown for a {{method}} callback variable.'`} βœ—

Functions

create(context: any): { CallExpression({ arguments: args, callee }: { arguments: …

Parameters:

  • context any

Returns: { CallExpression({ arguments: args, callee }: { arguments: any; callee: any; }): void; }

Calls:

  • getParserServices (from ../util)
  • program.getTypeChecker
  • tsutils.unionConstituents
  • tsutils.getCallSignaturesOfType
  • callSignature.parameters.at
  • checker.getTypeOfSymbol
  • isRestParameterDeclaration (from ../util)
  • checker.isArrayType
  • checker.getTypeArguments
  • checker.isTupleType
  • tsutils.isIntrinsicUnknownType
  • collectFlaggedNodes
  • nullThrows (from ../util)
  • node.expressions.at
  • esTreeNodeToTSNodeMap.get
  • checker.getTypeAtLocation
  • isFlaggableHandlerType
  • argument.params.at
  • isParenlessArrowFunction (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • fixer.replaceText
  • getStaticMemberAccessValue (from ../util)
  • ( [ { append: '', argIndexToCheck: 0, method: 'catch' }, { append: ' rejection', argIndexToCheck: 1, method: 'then' }, ] satisfies { append: string; argIndexToCheck: number; method: string; }[] ).find
  • args.slice
  • argsToCheck.some
  • tsutils.isThenableType
  • refineReportIfPossible
  • context.report

Internal Comments:

// Ignore any non-function components to the type. Those are not this rule's problem.
// it's not an issue if there's no catch variable at all.
// a rest arg that's not an array or tuple should definitely be flagged.
/**
     * Analyzes the syntax of the catch argument and makes a best effort to pinpoint
     * why it's reporting, and to come up with a suggested fix if possible.
     *
     * This function is explicitly operating under the assumption that the
     * rule _is reporting_, so it is not guaranteed to be sound to call otherwise.
     */
// Function expressions can't have parameter properties; those only exist in constructors. (x2)
// Need to be enough args to check (x2)
// Argument to check, and all arguments before it, must be "ordinary" arguments (i.e. no spread arguments) (x2)
// promise.catch(f), promise.catch(() => {}), promise.catch(<expression>, <<other-args>>) (x2)
// the `some` check above has already excluded `SpreadElement`, so we are safe to assert the same (x2)
// We are now guaranteed to report, but we have a bit of work to do (x2)
// to determine exactly where, and whether we can fix it. (x2)

Code
create(context) {
    const { esTreeNodeToTSNodeMap, program } = getParserServices(context);
    const checker = program.getTypeChecker();

    function isFlaggableHandlerType(type: ts.Type): boolean {
      for (const unionPart of tsutils.unionConstituents(type)) {
        const callSignatures = tsutils.getCallSignaturesOfType(unionPart);
        if (callSignatures.length === 0) {
          // Ignore any non-function components to the type. Those are not this rule's problem.
          continue;
        }

        for (const callSignature of callSignatures) {
          const firstParam = callSignature.parameters.at(0);
          if (!firstParam) {
            // it's not an issue if there's no catch variable at all.
            continue;
          }

          let firstParamType = checker.getTypeOfSymbol(firstParam);

          const decl = firstParam.valueDeclaration;
          if (decl != null && isRestParameterDeclaration(decl)) {
            if (checker.isArrayType(firstParamType)) {
              firstParamType = checker.getTypeArguments(firstParamType)[0];
            } else if (checker.isTupleType(firstParamType)) {
              firstParamType = checker.getTypeArguments(firstParamType)[0];
            } else {
              // a rest arg that's not an array or tuple should definitely be flagged.
              return true;
            }
          }

          if (!tsutils.isIntrinsicUnknownType(firstParamType)) {
            return true;
          }
        }
      }

      return false;
    }

    function collectFlaggedNodes(
      node: Exclude<TSESTree.Node, TSESTree.SpreadElement>,
    ): (TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression)[] {
      switch (node.type) {
        case AST_NODE_TYPES.LogicalExpression:
          return [
            ...collectFlaggedNodes(node.left),
            ...collectFlaggedNodes(node.right),
          ];
        case AST_NODE_TYPES.SequenceExpression:
          return collectFlaggedNodes(
            nullThrows(
              node.expressions.at(-1),
              'sequence expression must have multiple expressions',
            ),
          );
        case AST_NODE_TYPES.ConditionalExpression:
          return [
            ...collectFlaggedNodes(node.consequent),
            ...collectFlaggedNodes(node.alternate),
          ];
        case AST_NODE_TYPES.ArrowFunctionExpression:
        case AST_NODE_TYPES.FunctionExpression:
          {
            const argument = esTreeNodeToTSNodeMap.get(node);
            const typeOfArgument = checker.getTypeAtLocation(argument);
            if (isFlaggableHandlerType(typeOfArgument)) {
              return [node];
            }
          }
          break;
        default:
          break;
      }
      return [];
    }

    /**
     * Analyzes the syntax of the catch argument and makes a best effort to pinpoint
     * why it's reporting, and to come up with a suggested fix if possible.
     *
     * This function is explicitly operating under the assumption that the
     * rule _is reporting_, so it is not guaranteed to be sound to call otherwise.
     */
    function refineReportIfPossible(
      argument: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
    ): Partial<ReportDescriptor<MessageIds>> | undefined {
      const catchVariableOuterWithIncorrectTypes = nullThrows(
        argument.params.at(0),
        'There should have been at least one parameter for the rule to have flagged.',
      );

      // Function expressions can't have parameter properties; those only exist in constructors.
      const catchVariableOuter =
        catchVariableOuterWithIncorrectTypes as Exclude<
          typeof catchVariableOuterWithIncorrectTypes,
          TSESTree.TSParameterProperty
        >;
      const catchVariableInner =
        catchVariableOuter.type === AST_NODE_TYPES.AssignmentPattern
          ? catchVariableOuter.left
          : catchVariableOuter;

      switch (catchVariableInner.type) {
        case AST_NODE_TYPES.Identifier: {
          const catchVariableTypeAnnotation = catchVariableInner.typeAnnotation;
          if (catchVariableTypeAnnotation == null) {
            return {
              node: catchVariableOuter,
              suggest: [
                {
                  messageId: 'addUnknownTypeAnnotationSuggestion',
                  fix: (fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] => {
                    if (
                      argument.type ===
                        AST_NODE_TYPES.ArrowFunctionExpression &&
                      isParenlessArrowFunction(argument, context.sourceCode)
                    ) {
                      return [
                        fixer.insertTextBefore(catchVariableInner, '('),
                        fixer.insertTextAfter(catchVariableInner, ': unknown)'),
                      ];
                    }

                    return [
                      fixer.insertTextAfter(catchVariableInner, ': unknown'),
                    ];
                  },
                },
              ],
            };
          }

          return {
            node: catchVariableOuter,
            suggest: [
              {
                messageId: 'wrongTypeAnnotationSuggestion',
                fix: (fixer: TSESLint.RuleFixer): TSESLint.RuleFix =>
                  fixer.replaceText(catchVariableTypeAnnotation, ': unknown'),
              },
            ],
          };
        }
        case AST_NODE_TYPES.ArrayPattern: {
          return {
            node: catchVariableOuter,
            messageId: 'useUnknownArrayDestructuringPattern',
          };
        }
        case AST_NODE_TYPES.ObjectPattern: {
          return {
            node: catchVariableOuter,
            messageId: 'useUnknownObjectDestructuringPattern',
          };
        }
        case AST_NODE_TYPES.RestElement: {
          const catchVariableTypeAnnotation = catchVariableInner.typeAnnotation;
          if (catchVariableTypeAnnotation == null) {
            return {
              node: catchVariableOuter,
              suggest: [
                {
                  messageId: 'addUnknownRestTypeAnnotationSuggestion',
                  fix: (fixer): TSESLint.RuleFix =>
                    fixer.insertTextAfter(catchVariableInner, ': [unknown]'),
                },
              ],
            };
          }
          return {
            node: catchVariableOuter,
            suggest: [
              {
                messageId: 'wrongRestTypeAnnotationSuggestion',
                fix: (fixer): TSESLint.RuleFix =>
                  fixer.replaceText(catchVariableTypeAnnotation, ': [unknown]'),
              },
            ],
          };
        }
      }
    }

    return {
      CallExpression({ arguments: args, callee }): void {
        if (callee.type !== AST_NODE_TYPES.MemberExpression) {
          return;
        }

        const staticMemberAccessKey = getStaticMemberAccessValue(
          callee,
          context,
        );
        if (!staticMemberAccessKey) {
          return;
        }

        const promiseMethodInfo = (
          [
            { append: '', argIndexToCheck: 0, method: 'catch' },
            { append: ' rejection', argIndexToCheck: 1, method: 'then' },
          ] satisfies {
            append: string;
            argIndexToCheck: number;
            method: string;
          }[]
        ).find(({ method }) => staticMemberAccessKey === method);
        if (!promiseMethodInfo) {
          return;
        }

        // Need to be enough args to check
        const { argIndexToCheck, ...data } = promiseMethodInfo;
        if (args.length < argIndexToCheck + 1) {
          return;
        }

        // Argument to check, and all arguments before it, must be "ordinary" arguments (i.e. no spread arguments)
        // promise.catch(f), promise.catch(() => {}), promise.catch(<expression>, <<other-args>>)
        const argsToCheck = args.slice(0, argIndexToCheck + 1);
        if (
          argsToCheck.some(({ type }) => type === AST_NODE_TYPES.SpreadElement)
        ) {
          return;
        }

        if (
          !tsutils.isThenableType(
            checker,
            esTreeNodeToTSNodeMap.get(callee),
            checker.getTypeAtLocation(esTreeNodeToTSNodeMap.get(callee.object)),
          )
        ) {
          return;
        }

        // the `some` check above has already excluded `SpreadElement`, so we are safe to assert the same
        const argToCheck = argsToCheck[argIndexToCheck] as Exclude<
          TSESTree.Node,
          TSESTree.SpreadElement
        >;

        for (const node of collectFlaggedNodes(argToCheck)) {
          // We are now guaranteed to report, but we have a bit of work to do
          // to determine exactly where, and whether we can fix it.
          const overrides = refineReportIfPossible(node);
          context.report({
            node,
            messageId: 'useUnknown',
            data,
            ...overrides,
          });
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

isFlaggableHandlerType(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • tsutils.unionConstituents
  • tsutils.getCallSignaturesOfType
  • callSignature.parameters.at
  • checker.getTypeOfSymbol
  • isRestParameterDeclaration (from ../util)
  • checker.isArrayType
  • checker.getTypeArguments
  • checker.isTupleType
  • tsutils.isIntrinsicUnknownType

Internal Comments:

// Ignore any non-function components to the type. Those are not this rule's problem.
// it's not an issue if there's no catch variable at all.
// a rest arg that's not an array or tuple should definitely be flagged.

Code
function isFlaggableHandlerType(type: ts.Type): boolean {
      for (const unionPart of tsutils.unionConstituents(type)) {
        const callSignatures = tsutils.getCallSignaturesOfType(unionPart);
        if (callSignatures.length === 0) {
          // Ignore any non-function components to the type. Those are not this rule's problem.
          continue;
        }

        for (const callSignature of callSignatures) {
          const firstParam = callSignature.parameters.at(0);
          if (!firstParam) {
            // it's not an issue if there's no catch variable at all.
            continue;
          }

          let firstParamType = checker.getTypeOfSymbol(firstParam);

          const decl = firstParam.valueDeclaration;
          if (decl != null && isRestParameterDeclaration(decl)) {
            if (checker.isArrayType(firstParamType)) {
              firstParamType = checker.getTypeArguments(firstParamType)[0];
            } else if (checker.isTupleType(firstParamType)) {
              firstParamType = checker.getTypeArguments(firstParamType)[0];
            } else {
              // a rest arg that's not an array or tuple should definitely be flagged.
              return true;
            }
          }

          if (!tsutils.isIntrinsicUnknownType(firstParamType)) {
            return true;
          }
        }
      }

      return false;
    }

collectFlaggedNodes(node: Exclude<TSESTree.Node, TSESTree.SpreadE…): (TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpres…

Parameters:

  • node Exclude<TSESTree.Node, TSESTree.SpreadElement>

Returns: (TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression)[]

Calls:

  • collectFlaggedNodes
  • nullThrows (from ../util)
  • node.expressions.at
  • esTreeNodeToTSNodeMap.get
  • checker.getTypeAtLocation
  • isFlaggableHandlerType
Code
function collectFlaggedNodes(
      node: Exclude<TSESTree.Node, TSESTree.SpreadElement>,
    ): (TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression)[] {
      switch (node.type) {
        case AST_NODE_TYPES.LogicalExpression:
          return [
            ...collectFlaggedNodes(node.left),
            ...collectFlaggedNodes(node.right),
          ];
        case AST_NODE_TYPES.SequenceExpression:
          return collectFlaggedNodes(
            nullThrows(
              node.expressions.at(-1),
              'sequence expression must have multiple expressions',
            ),
          );
        case AST_NODE_TYPES.ConditionalExpression:
          return [
            ...collectFlaggedNodes(node.consequent),
            ...collectFlaggedNodes(node.alternate),
          ];
        case AST_NODE_TYPES.ArrowFunctionExpression:
        case AST_NODE_TYPES.FunctionExpression:
          {
            const argument = esTreeNodeToTSNodeMap.get(node);
            const typeOfArgument = checker.getTypeAtLocation(argument);
            if (isFlaggableHandlerType(typeOfArgument)) {
              return [node];
            }
          }
          break;
        default:
          break;
      }
      return [];
    }

refineReportIfPossible(argument: TSESTree.ArrowFunctionExpression | TSES…): Partial<ReportDescriptor<MessageIds>> | undefined

Analyzes the syntax of the catch argument and makes a best effort to pinpoint why it's reporting, and to come up with a suggested fix if possible.

This function is explicitly operating under the assumption that the rule is reporting, so it is not guaranteed to be sound to call otherwise.

Raw JSDoc
/**
     * Analyzes the syntax of the catch argument and makes a best effort to pinpoint
     * why it's reporting, and to come up with a suggested fix if possible.
     *
     * This function is explicitly operating under the assumption that the
     * rule _is reporting_, so it is not guaranteed to be sound to call otherwise.
     */

Calls:

  • nullThrows (from ../util)
  • argument.params.at
  • isParenlessArrowFunction (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • fixer.replaceText

Internal Comments:

// Function expressions can't have parameter properties; those only exist in constructors. (x2)

Code
function refineReportIfPossible(
      argument: TSESTree.ArrowFunctionExpression | TSESTree.FunctionExpression,
    ): Partial<ReportDescriptor<MessageIds>> | undefined {
      const catchVariableOuterWithIncorrectTypes = nullThrows(
        argument.params.at(0),
        'There should have been at least one parameter for the rule to have flagged.',
      );

      // Function expressions can't have parameter properties; those only exist in constructors.
      const catchVariableOuter =
        catchVariableOuterWithIncorrectTypes as Exclude<
          typeof catchVariableOuterWithIncorrectTypes,
          TSESTree.TSParameterProperty
        >;
      const catchVariableInner =
        catchVariableOuter.type === AST_NODE_TYPES.AssignmentPattern
          ? catchVariableOuter.left
          : catchVariableOuter;

      switch (catchVariableInner.type) {
        case AST_NODE_TYPES.Identifier: {
          const catchVariableTypeAnnotation = catchVariableInner.typeAnnotation;
          if (catchVariableTypeAnnotation == null) {
            return {
              node: catchVariableOuter,
              suggest: [
                {
                  messageId: 'addUnknownTypeAnnotationSuggestion',
                  fix: (fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] => {
                    if (
                      argument.type ===
                        AST_NODE_TYPES.ArrowFunctionExpression &&
                      isParenlessArrowFunction(argument, context.sourceCode)
                    ) {
                      return [
                        fixer.insertTextBefore(catchVariableInner, '('),
                        fixer.insertTextAfter(catchVariableInner, ': unknown)'),
                      ];
                    }

                    return [
                      fixer.insertTextAfter(catchVariableInner, ': unknown'),
                    ];
                  },
                },
              ],
            };
          }

          return {
            node: catchVariableOuter,
            suggest: [
              {
                messageId: 'wrongTypeAnnotationSuggestion',
                fix: (fixer: TSESLint.RuleFixer): TSESLint.RuleFix =>
                  fixer.replaceText(catchVariableTypeAnnotation, ': unknown'),
              },
            ],
          };
        }
        case AST_NODE_TYPES.ArrayPattern: {
          return {
            node: catchVariableOuter,
            messageId: 'useUnknownArrayDestructuringPattern',
          };
        }
        case AST_NODE_TYPES.ObjectPattern: {
          return {
            node: catchVariableOuter,
            messageId: 'useUnknownObjectDestructuringPattern',
          };
        }
        case AST_NODE_TYPES.RestElement: {
          const catchVariableTypeAnnotation = catchVariableInner.typeAnnotation;
          if (catchVariableTypeAnnotation == null) {
            return {
              node: catchVariableOuter,
              suggest: [
                {
                  messageId: 'addUnknownRestTypeAnnotationSuggestion',
                  fix: (fixer): TSESLint.RuleFix =>
                    fixer.insertTextAfter(catchVariableInner, ': [unknown]'),
                },
              ],
            };
          }
          return {
            node: catchVariableOuter,
            suggest: [
              {
                messageId: 'wrongRestTypeAnnotationSuggestion',
                fix: (fixer): TSESLint.RuleFix =>
                  fixer.replaceText(catchVariableTypeAnnotation, ': [unknown]'),
              },
            ],
          };
        }
      }
    }

suggest.fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix[]

Parameters:

  • fixer TSESLint.RuleFixer

Returns: TSESLint.RuleFix[]

Calls:

  • isParenlessArrowFunction (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
Code
(fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] => {
                    if (
                      argument.type ===
                        AST_NODE_TYPES.ArrowFunctionExpression &&
                      isParenlessArrowFunction(argument, context.sourceCode)
                    ) {
                      return [
                        fixer.insertTextBefore(catchVariableInner, '('),
                        fixer.insertTextAfter(catchVariableInner, ': unknown)'),
                      ];
                    }

                    return [
                      fixer.insertTextAfter(catchVariableInner, ': unknown'),
                    ];
                  }

suggest.fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix

Parameters:

  • fixer TSESLint.RuleFixer

Returns: TSESLint.RuleFix

Calls:

  • fixer.replaceText
Code
(fixer: TSESLint.RuleFixer): TSESLint.RuleFix =>
                  fixer.replaceText(catchVariableTypeAnnotation, ': unknown')

suggest.fix(fixer: any): TSESLint.RuleFix

Parameters:

  • fixer any

Returns: TSESLint.RuleFix

Calls:

  • fixer.insertTextAfter
Code
(fixer): TSESLint.RuleFix =>
                    fixer.insertTextAfter(catchVariableInner, ': [unknown]')

suggest.fix(fixer: any): TSESLint.RuleFix

Parameters:

  • fixer any

Returns: TSESLint.RuleFix

Calls:

  • fixer.replaceText
Code
(fixer): TSESLint.RuleFix =>
                  fixer.replaceText(catchVariableTypeAnnotation, ': [unknown]')

Type Aliases

MessageIds

type MessageIds = | 'addUnknownRestTypeAnnotationSuggestion'
  | 'addUnknownTypeAnnotationSuggestion'
  | 'useUnknown'
  | 'useUnknownArrayDestructuringPattern'
  | 'useUnknownObjectDestructuringPattern'
  | 'wrongRestTypeAnnotationSuggestion'
  | 'wrongTypeAnnotationSuggestion';

Generated by Syntax Scribe