Skip to content

⬅️ Back to Table of Contents

πŸ“„ no-unnecessary-condition

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 30
πŸ“¦ Imports 22
πŸ“Š Variables & Constants 3
πŸ“‘ Type Aliases 5

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/no-unnecessary-condition.ts

πŸ“€ Default Export

export default createRule<Options, MessageId>({ ... })
Property Value
name 'no-unnecessary-condition'
meta.type 'suggestion'
meta.docs.description 'Disallow conditionals where the type is always truthy or always falsy'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.alwaysFalsy 'Unnecessary conditional, value is always falsy.'
meta.messages.alwaysFalsyFunc 'This callback should return a conditional, but return is always falsy.'
meta.messages.alwaysNullish 'Unnecessary conditional, left-hand side of ?? operator is always null or undefined.'
meta.messages.alwaysTruthy 'Unnecessary conditional, value is always truthy.'
meta.messages.alwaysTruthyFunc 'This callback should return a conditional, but return is always truthy.'
meta.messages.comparisonBetweenLiteralTypes 'Unnecessary conditional, comparison is always {{trueOrFalse}}, since {{left}} {{operator}} {{right}} is {{trueOrFa...
meta.messages.never 'Unnecessary conditional, value is never.'
meta.messages.neverNullish 'Unnecessary conditional, expected left-hand side of ?? operator to be possibly null or undefined.'
meta.messages.neverOptionalChain 'Unnecessary optional chain on a non-nullish value.'
meta.messages.noOverlapBooleanExpression 'Unnecessary conditional, the types have no overlap.'
meta.messages.noStrictNullCheck 'This rule requires the strictNullChecks compiler option to be turned on to function correctly.'
meta.messages.suggestRemoveOptionalChain 'Remove unnecessary optional chain'
meta.messages.typeGuardAlreadyIsType 'Unnecessary conditional, expression already has the type being checked by the {{typeGuardOrAssertionFunction}}.'
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowConstantLoopConditions: { description: 'Whether t...
defaultOptions [ { allowConstantLoopConditions: 'never', allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, checkTypePre...

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
AST_TOKEN_TYPES @typescript-eslint/utils
createRule ../util
getConstrainedTypeAtLocation ../util
getConstraintInfo ../util
getParserServices ../util
getTypeName ../util
getTypeOfPropertyOfName ../util
getValueOfLiteralType ../util
isArrayMethodCallWithPredicate ../util
isIdentifier ../util
isNullableType ../util
isPossiblyFalsy ../util
isPossiblyTruthy ../util
isTypeAnyType ../util
isTypeFlagSet ../util
isTypeUnknownType ../util
nullThrows ../util
NullThrowsReasons ../util
findTruthinessAssertedArgument ../util/assertionFunctionUtils
findTypeGuardAssertedArgument ../util/assertionFunctionUtils

Variables & Constants

Name Type Kind Value Exported
nullishFlag number const ts.TypeFlags.Undefined \| ts.TypeFlags.Null βœ—
BOOL_OPERATORS Set<"==" \| "===" \| "<" \| ">" \| "<... const new Set([ '<', '>', '<=', '>=', '==', '===', '!=', '!==', ] as const) βœ—
constantLoopConditionsAllow... Set<unknown> const new Set<unknown>([ true, false, 1, 0, ]) βœ—

Functions

create(context: any, [ { allowConstantLoopConditions…: any): { AssignmentExpression: (node: TSESTree.AssignmentExpressio…

Parameters:

  • context any
  • [ { allowConstantLoopConditions, allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, checkTypePredicates, }, ] any

Returns: { AssignmentExpression: (node: TSESTree.AssignmentExpression) => void; BinaryExpression(node: any): void; CallExpression: (node: TSESTree.CallExpression) => void; 'CallExpression[optional = true]': (node: TSESTree.CallExpression) => void; ConditionalExpression: (node: any) => void; DoWhileStatement: (node: TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.WhileStatement) => void; ForStatement: (node: TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.WhileStatement) => void; IfStatement: (node: any) => void; LogicalExpression: (node: TSESTree.LogicalExpression) => void; 'MemberExpression[optional = true]': (node: TSESTree.MemberExpression) => void; SwitchCase({ parent, test }: { parent: any; test: any; }): void; WhileStatement: (node: TSESTree.DoWhileStatement | TSESTree.ForStatement | TSESTree.WhileStatement) => void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • services.program.getCompilerOptions
  • tsutils.isStrictCompilerOptionEnabled
  • tsutils.isCompilerOptionEnabled
  • normalizeAllowConstantLoopConditions
  • context.report
  • getConstrainedTypeAtLocation (from ../util)
  • tsutils .unionConstituents(nodeType) .some
  • checker.isArrayType
  • checker.isTupleType
  • nodeIsArrayType
  • nodeIsTupleType
  • tsutils .unionConstituents(type) .some
  • isTypeAnyType (from ../util)
  • isTypeUnknownType (from ../util)
  • isTypeFlagSet (from ../util)
  • services.getTypeAtLocation
  • isNullablePropertyType
  • context.sourceCode.getText
  • objectType .getProperties() .find
  • tsutils.isSymbolFlagSet
  • checkNode
  • isArrayIndexExpression
  • isOnlyUsedForTruthiness
  • isConditionalAlwaysNecessary
  • isPossiblyTruthy (from ../util)
  • isPossiblyFalsy (from ../util)
  • isPossiblyNullish
  • isNullableMemberExpression
  • optionChainContainsOptionArrayIndex
  • isAlwaysNullish
  • toStaticValue
  • booleanComparison
  • checker.typeToString
  • isComparable
  • checkNodeForNullish
  • constantLoopConditionsAllowedLiterals.has
  • tsutils.isTrueLiteralType
  • findTruthinessAssertedArgument (from ../util/assertionFunctionUtils)
  • findTypeGuardAssertedArgument (from ../util/assertionFunctionUtils)
  • tsutils.isTypeFlagSet
  • checker.isTypeAssignableTo
  • typeGuardAssertedArgument.type.isUnion
  • isArrayMethodCallWithPredicate (from ../util)
  • tsutils .getCallSignaturesOfType( getConstrainedTypeAtLocation(services, callback), ) .map
  • sig.getReturnType
  • getConstraintInfo (from ../util)
  • propertyType.isUnion
  • propertyType.types.some
  • propertyType.isNumberLiteral
  • propertyType.isStringLiteral
  • getTypeOfPropertyOfName (from ../util)
  • propertyType.value.toString
  • isNullableType (from ../util)
  • getTypeName (from ../util)
  • checker .getIndexInfosOfType(objType) .some
  • prevType.isUnion
  • isIdentifier (from ../util)
  • prevType.types.some
  • checker.getIndexInfosOfType
  • indexInfo.some
  • type.getCallSignatures
  • signatures.some
  • isMemberExpressionNullableOriginFromObject
  • isCallExpressionNullableOriginFromCallee
  • isOptionableExpression
  • nullThrows (from ../util)
  • context.sourceCode.getTokenAfter
  • NullThrowsReasons.MissingToken
  • fixer.replaceText
  • checkOptionalChain
  • ['&&=', '||='].includes
  • isBoolOperator
  • checkIfBoolExpressionIsNecessaryConditional

Internal Comments:

// https://github.com/typescript-eslint/typescript-eslint/issues/5439 (x2)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x2)
// Is an index signature (x5)
// ...into an array type
// ... or a tuple type
// Exception: literal index into a tuple - will have a sound type (x4)
// Conditional is always necessary if it involves:
//    `any` or `unknown` or a naked type variable
// Get the actual property name, to account for private properties (this.#prop). (x2)
/**
     * Checks if a conditional node is necessary:
     * if the type of the node is always true or always false, it's not necessary.
     */
// Check if the node is Unary Negation expression and handle it
// Since typescript array index signature types don't represent the (x3)
//  possibility of out-of-bounds access, if we're indexing into an array (x3)
//  just skip the check, to avoid false positives (x3)
// Only check the right side when the logical expression is used for
// truthiness, as the left side has already been checked by
// checkLogicalExpressionForUnnecessaryConditionals.
//
// Unless the node is nullish coalescing, as it's common to use patterns like
// `nullBool ?? true` to perform strict boolean checks. If we inspect the right
// here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.
// Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
/**
     * Checks that a binary expression is necessarily conditional, reports otherwise.
     * If both sides of the binary expression are literal values, it's not a necessary condition.
     *
     * NOTE: It's also unnecessary if the types that don't overlap at all
     *    but that case is handled by the Typescript compiler itself.
     *    Known exceptions:
     *      - https://github.com/microsoft/TypeScript/issues/32627
     *      - https://github.com/microsoft/TypeScript/issues/37160 (handled)
     */
// Workaround for https://github.com/microsoft/TypeScript/issues/37160
// Allow comparison to `any`, `unknown` or a naked type parameter. (x3)
// Allow loose comparison to nullish values.
/**
     * Checks that a logical expression contains a boolean, reports otherwise.
     */
// Only checks the left side, since the right side might not be "conditional" at all. (x3)
// The right side will be checked if the LogicalExpression is used in a conditional context (x3)
/**
     * Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
     */
// e.g. `for(;;)`
// Skip `any` β€” it is assignable to everything, producing (x3)
// false positives for meaningful runtime type guards. (x3)
// Only flag if the types are mutually assignable (i.e. equivalent,
// like Narrower ↔ Wider with optional props) or the predicate type
// is a union that the argument is a strict subtype of.  This avoids
// false positives with structural subtypes whose extra members are
// all optional in the *predicate* type (e.g. custom MappedType
// interfaces extending ts.Type).
// If this is something like arr.filter(x => /*condition*/), check `condition`
// Inline defined functions
// Two special cases, where we can directly check the node that's returned:
// () => something
// () => { return something; } (x2)
// Otherwise just do type analysis on the function as a whole. (x2)
// Not a callable function, e.g. `any`
// Predicate is always necessary if it involves `any` or `unknown`
// bail early if both a possibly-truthy and a possibly-falsy have been detected
// Recursively searches an optional chain for an array index expression
//  Has to search the entire chain, because an array index will "infect" the rest of the types
//  Example: (x2)
//  ``` (x4)
//  [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
//    ?.x // type is {y: "z"}
//    ?.y // This access is considered "unnecessary" according to the types
// Checks whether a member expression is nullable or not regardless of it's previous node.
//  // 'bar' is nullable if 'foo' is null.
//  // but this function checks regardless of 'foo' type, so returns 'true'.
//  declare const foo: { bar : { baz: string } } | null
//  foo?.bar;
// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
// Similar to checkLogicalExpressionForUnnecessaryConditionals, since
// a ||= b is equivalent to a || (a = b)
// only check `case ...:`, not `default:`

Code
create(
    context,
    [
      {
        allowConstantLoopConditions,
        allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
        checkTypePredicates,
      },
    ],
  ) {
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();

    const compilerOptions = services.program.getCompilerOptions();
    const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(
      compilerOptions,
      'strictNullChecks',
    );
    const isNoUncheckedIndexedAccess = tsutils.isCompilerOptionEnabled(
      compilerOptions,
      'noUncheckedIndexedAccess',
    );

    const allowConstantLoopConditionsOption =
      normalizeAllowConstantLoopConditions(
        // https://github.com/typescript-eslint/typescript-eslint/issues/5439
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        allowConstantLoopConditions!,
      );

    if (
      !isStrictNullChecks &&
      allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing !== true
    ) {
      context.report({
        loc: {
          start: { column: 0, line: 0 },
          end: { column: 0, line: 0 },
        },
        messageId: 'noStrictNullCheck',
      });
    }

    function nodeIsArrayType(node: TSESTree.Expression): boolean {
      const nodeType = getConstrainedTypeAtLocation(services, node);
      return tsutils
        .unionConstituents(nodeType)
        .some(part => checker.isArrayType(part));
    }

    function nodeIsTupleType(node: TSESTree.Expression): boolean {
      const nodeType = getConstrainedTypeAtLocation(services, node);
      return tsutils
        .unionConstituents(nodeType)
        .some(part => checker.isTupleType(part));
    }

    function isArrayIndexExpression(node: TSESTree.Expression): boolean {
      return (
        // Is an index signature
        node.type === AST_NODE_TYPES.MemberExpression &&
        node.computed &&
        // ...into an array type
        (nodeIsArrayType(node.object) ||
          // ... or a tuple type
          (nodeIsTupleType(node.object) &&
            // Exception: literal index into a tuple - will have a sound type
            node.property.type !== AST_NODE_TYPES.Literal))
      );
    }

    // Conditional is always necessary if it involves:
    //    `any` or `unknown` or a naked type variable
    function isConditionalAlwaysNecessary(type: ts.Type): boolean {
      return tsutils
        .unionConstituents(type)
        .some(
          part =>
            isTypeAnyType(part) ||
            isTypeUnknownType(part) ||
            isTypeFlagSet(part, ts.TypeFlags.TypeVariable),
        );
    }

    function isNullableMemberExpression(
      node: TSESTree.MemberExpression,
    ): boolean {
      const objectType = services.getTypeAtLocation(node.object);
      if (node.computed) {
        const propertyType = services.getTypeAtLocation(node.property);
        return isNullablePropertyType(objectType, propertyType);
      }
      const property = node.property;

      // Get the actual property name, to account for private properties (this.#prop).
      const propertyName = context.sourceCode.getText(property);

      const propertyType = objectType
        .getProperties()
        .find(prop => prop.name === propertyName);

      if (
        propertyType &&
        tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional)
      ) {
        return true;
      }

      return false;
    }

    /**
     * Checks if a conditional node is necessary:
     * if the type of the node is always true or always false, it's not necessary.
     */
    function checkNode(
      expression: TSESTree.Expression,
      isUnaryNotArgument = false,
      node = expression,
    ): void {
      // Check if the node is Unary Negation expression and handle it
      if (
        expression.type === AST_NODE_TYPES.UnaryExpression &&
        expression.operator === '!'
      ) {
        return checkNode(expression.argument, !isUnaryNotArgument, node);
      }

      // Since typescript array index signature types don't represent the
      //  possibility of out-of-bounds access, if we're indexing into an array
      //  just skip the check, to avoid false positives
      if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) {
        return;
      }

      // Only check the right side when the logical expression is used for
      // truthiness, as the left side has already been checked by
      // checkLogicalExpressionForUnnecessaryConditionals.
      //
      // Unless the node is nullish coalescing, as it's common to use patterns like
      // `nullBool ?? true` to perform strict boolean checks. If we inspect the right
      // here, it'll usually be a constant condition on purpose.
      // In this case it's better to inspect the type of the expression as a whole.
      if (
        expression.type === AST_NODE_TYPES.LogicalExpression &&
        expression.operator !== '??'
      ) {
        if (isOnlyUsedForTruthiness(expression)) {
          checkNode(expression.right);
        }
        return;
      }

      const type = getConstrainedTypeAtLocation(services, expression);

      if (isConditionalAlwaysNecessary(type)) {
        return;
      }
      let messageId: MessageId | null = null;

      if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
        messageId = 'never';
      } else if (!isPossiblyTruthy(type)) {
        messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
      } else if (!isPossiblyFalsy(type)) {
        messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
      }

      if (messageId) {
        context.report({ node, messageId });
      }
    }

    function checkNodeForNullish(node: TSESTree.Expression): void {
      const type = getConstrainedTypeAtLocation(services, node);

      // Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
      if (
        isTypeFlagSet(
          type,
          ts.TypeFlags.Any |
            ts.TypeFlags.Unknown |
            ts.TypeFlags.TypeParameter |
            ts.TypeFlags.TypeVariable,
        )
      ) {
        return;
      }

      let messageId: MessageId | null = null;
      if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
        messageId = 'never';
      } else if (
        !isPossiblyNullish(type) &&
        !(
          node.type === AST_NODE_TYPES.MemberExpression &&
          isNullableMemberExpression(node)
        )
      ) {
        // Since typescript array index signature types don't represent the
        //  possibility of out-of-bounds access, if we're indexing into an array
        //  just skip the check, to avoid false positives
        if (
          isNoUncheckedIndexedAccess ||
          (!isArrayIndexExpression(node) &&
            !(
              node.type === AST_NODE_TYPES.ChainExpression &&
              node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
              optionChainContainsOptionArrayIndex(node.expression)
            ))
        ) {
          messageId = 'neverNullish';
        }
      } else if (isAlwaysNullish(type)) {
        messageId = 'alwaysNullish';
      }

      if (messageId) {
        context.report({ node, messageId });
      }
    }

    /**
     * Checks that a binary expression is necessarily conditional, reports otherwise.
     * If both sides of the binary expression are literal values, it's not a necessary condition.
     *
     * NOTE: It's also unnecessary if the types that don't overlap at all
     *    but that case is handled by the Typescript compiler itself.
     *    Known exceptions:
     *      - https://github.com/microsoft/TypeScript/issues/32627
     *      - https://github.com/microsoft/TypeScript/issues/37160 (handled)
     */
    function checkIfBoolExpressionIsNecessaryConditional(
      node: TSESTree.Node,
      left: TSESTree.Node,
      right: TSESTree.Node,
      operator: BoolOperator,
    ): void {
      const leftType = getConstrainedTypeAtLocation(services, left);
      const rightType = getConstrainedTypeAtLocation(services, right);

      const leftStaticValue = toStaticValue(leftType);
      const rightStaticValue = toStaticValue(rightType);

      if (leftStaticValue != null && rightStaticValue != null) {
        const conditionIsTrue = booleanComparison(
          leftStaticValue.value,
          operator,
          rightStaticValue.value,
        );

        context.report({
          node,
          messageId: 'comparisonBetweenLiteralTypes',
          data: {
            left: checker.typeToString(leftType),
            operator,
            right: checker.typeToString(rightType),
            trueOrFalse: conditionIsTrue ? 'true' : 'false',
          },
        });
        return;
      }

      // Workaround for https://github.com/microsoft/TypeScript/issues/37160
      if (isStrictNullChecks) {
        const UNDEFINED = ts.TypeFlags.Undefined;
        const NULL = ts.TypeFlags.Null;
        const VOID = ts.TypeFlags.Void;
        const isComparable = (type: ts.Type, flag: ts.TypeFlags): boolean => {
          // Allow comparison to `any`, `unknown` or a naked type parameter.
          flag |=
            ts.TypeFlags.Any |
            ts.TypeFlags.Unknown |
            ts.TypeFlags.TypeParameter |
            ts.TypeFlags.TypeVariable;

          // Allow loose comparison to nullish values.
          if (operator === '==' || operator === '!=') {
            flag |= NULL | UNDEFINED | VOID;
          }

          return isTypeFlagSet(type, flag);
        };

        if (
          (leftType.flags === UNDEFINED &&
            !isComparable(rightType, UNDEFINED | VOID)) ||
          (rightType.flags === UNDEFINED &&
            !isComparable(leftType, UNDEFINED | VOID)) ||
          (leftType.flags === NULL && !isComparable(rightType, NULL)) ||
          (rightType.flags === NULL && !isComparable(leftType, NULL))
        ) {
          context.report({ node, messageId: 'noOverlapBooleanExpression' });
          return;
        }
      }
    }

    /**
     * Checks that a logical expression contains a boolean, reports otherwise.
     */
    function checkLogicalExpressionForUnnecessaryConditionals(
      node: TSESTree.LogicalExpression,
    ): void {
      if (node.operator === '??') {
        checkNodeForNullish(node.left);
        return;
      }
      // Only checks the left side, since the right side might not be "conditional" at all.
      // The right side will be checked if the LogicalExpression is used in a conditional context
      checkNode(node.left);
    }

    /**
     * Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
     */
    function checkIfLoopIsNecessaryConditional(
      node:
        | TSESTree.DoWhileStatement
        | TSESTree.ForStatement
        | TSESTree.WhileStatement,
    ): void {
      if (node.test == null) {
        // e.g. `for(;;)`
        return;
      }

      if (
        allowConstantLoopConditionsOption === 'only-allowed-literals' &&
        node.test.type === AST_NODE_TYPES.Literal &&
        constantLoopConditionsAllowedLiterals.has(node.test.value)
      ) {
        return;
      }

      if (
        allowConstantLoopConditionsOption === 'always' &&
        tsutils.isTrueLiteralType(
          getConstrainedTypeAtLocation(services, node.test),
        )
      ) {
        return;
      }

      checkNode(node.test);
    }

    function checkCallExpression(node: TSESTree.CallExpression): void {
      if (checkTypePredicates) {
        const truthinessAssertedArgument = findTruthinessAssertedArgument(
          services,
          node,
        );
        if (truthinessAssertedArgument != null) {
          checkNode(truthinessAssertedArgument);
        }

        const typeGuardAssertedArgument = findTypeGuardAssertedArgument(
          services,
          node,
        );
        if (typeGuardAssertedArgument != null) {
          const typeOfArgument = getConstrainedTypeAtLocation(
            services,
            typeGuardAssertedArgument.argument,
          );
          if (
            // Skip `any` β€” it is assignable to everything, producing
            // false positives for meaningful runtime type guards.
            !tsutils.isTypeFlagSet(
              typeOfArgument,
              ts.TypeFlags.Any | ts.TypeFlags.Unknown,
            ) &&
            checker.isTypeAssignableTo(
              typeOfArgument,
              typeGuardAssertedArgument.type,
            ) &&
            // Only flag if the types are mutually assignable (i.e. equivalent,
            // like Narrower ↔ Wider with optional props) or the predicate type
            // is a union that the argument is a strict subtype of.  This avoids
            // false positives with structural subtypes whose extra members are
            // all optional in the *predicate* type (e.g. custom MappedType
            // interfaces extending ts.Type).
            (checker.isTypeAssignableTo(
              typeGuardAssertedArgument.type,
              typeOfArgument,
            ) ||
              typeGuardAssertedArgument.type.isUnion())
          ) {
            context.report({
              node: typeGuardAssertedArgument.argument,
              messageId: 'typeGuardAlreadyIsType',
              data: {
                typeGuardOrAssertionFunction: typeGuardAssertedArgument.asserts
                  ? 'assertion function'
                  : 'type guard',
              },
            });
          }
        }
      }

      // If this is something like arr.filter(x => /*condition*/), check `condition`
      if (
        isArrayMethodCallWithPredicate(context, services, node) &&
        node.arguments.length
      ) {
        const callback = node.arguments[0];
        // Inline defined functions
        if (
          callback.type === AST_NODE_TYPES.ArrowFunctionExpression ||
          callback.type === AST_NODE_TYPES.FunctionExpression
        ) {
          // Two special cases, where we can directly check the node that's returned:
          // () => something
          if (callback.body.type !== AST_NODE_TYPES.BlockStatement) {
            return checkNode(callback.body);
          }
          // () => { return something; }
          const callbackBody = callback.body.body;
          if (
            callbackBody.length === 1 &&
            callbackBody[0].type === AST_NODE_TYPES.ReturnStatement &&
            callbackBody[0].argument
          ) {
            return checkNode(callbackBody[0].argument);
          }
          // Potential enhancement: could use code-path analysis to check
          //   any function with a single return statement
          // (Value to complexity ratio is dubious however)
        }
        // Otherwise just do type analysis on the function as a whole.
        const returnTypes = tsutils
          .getCallSignaturesOfType(
            getConstrainedTypeAtLocation(services, callback),
          )
          .map(sig => sig.getReturnType());

        if (returnTypes.length === 0) {
          // Not a callable function, e.g. `any`
          return;
        }

        let hasFalsyReturnTypes = false;
        let hasTruthyReturnTypes = false;

        for (const type of returnTypes) {
          const { constraintType } = getConstraintInfo(checker, type);
          // Predicate is always necessary if it involves `any` or `unknown`
          if (
            !constraintType ||
            isTypeAnyType(constraintType) ||
            isTypeUnknownType(constraintType)
          ) {
            return;
          }

          if (isPossiblyFalsy(constraintType)) {
            hasFalsyReturnTypes = true;
          }

          if (isPossiblyTruthy(constraintType)) {
            hasTruthyReturnTypes = true;
          }

          // bail early if both a possibly-truthy and a possibly-falsy have been detected
          if (hasFalsyReturnTypes && hasTruthyReturnTypes) {
            return;
          }
        }

        if (!hasFalsyReturnTypes) {
          return context.report({
            node: callback,
            messageId: 'alwaysTruthyFunc',
          });
        }

        if (!hasTruthyReturnTypes) {
          return context.report({
            node: callback,
            messageId: 'alwaysFalsyFunc',
          });
        }
      }
    }

    // Recursively searches an optional chain for an array index expression
    //  Has to search the entire chain, because an array index will "infect" the rest of the types
    //  Example:
    //  ```
    //  [{x: {y: "z"} }][n] // type is {x: {y: "z"}}
    //    ?.x // type is {y: "z"}
    //    ?.y // This access is considered "unnecessary" according to the types
    //  ```
    function optionChainContainsOptionArrayIndex(
      node: TSESTree.CallExpression | TSESTree.MemberExpression,
    ): boolean {
      const lhsNode =
        node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
      if (node.optional && isArrayIndexExpression(lhsNode)) {
        return true;
      }
      if (
        lhsNode.type === AST_NODE_TYPES.MemberExpression ||
        lhsNode.type === AST_NODE_TYPES.CallExpression
      ) {
        return optionChainContainsOptionArrayIndex(lhsNode);
      }
      return false;
    }

    function isNullablePropertyType(
      objType: ts.Type,
      propertyType: ts.Type,
    ): boolean {
      if (propertyType.isUnion()) {
        return propertyType.types.some(type =>
          isNullablePropertyType(objType, type),
        );
      }
      if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
        const propType = getTypeOfPropertyOfName(
          checker,
          objType,
          propertyType.value.toString(),
        );
        if (propType) {
          return isNullableType(propType);
        }
      }
      const typeName = getTypeName(checker, propertyType);
      return checker
        .getIndexInfosOfType(objType)
        .some(info => getTypeName(checker, info.keyType) === typeName);
    }

    // Checks whether a member expression is nullable or not regardless of it's previous node.
    //  Example:
    //  ```
    //  // 'bar' is nullable if 'foo' is null.
    //  // but this function checks regardless of 'foo' type, so returns 'true'.
    //  declare const foo: { bar : { baz: string } } | null
    //  foo?.bar;
    //  ```
    function isMemberExpressionNullableOriginFromObject(
      node: TSESTree.MemberExpression,
    ): boolean {
      const prevType = getConstrainedTypeAtLocation(services, node.object);
      const property = node.property;
      if (prevType.isUnion() && isIdentifier(property)) {
        const isOwnNullable = prevType.types.some(type => {
          if (node.computed) {
            const propertyType = getConstrainedTypeAtLocation(
              services,
              node.property,
            );
            return isNullablePropertyType(type, propertyType);
          }
          const propType = getTypeOfPropertyOfName(
            checker,
            type,
            property.name,
          );

          if (propType) {
            return isNullableType(propType);
          }
          const indexInfo = checker.getIndexInfosOfType(type);

          return indexInfo.some(info => {
            const isStringTypeName =
              getTypeName(checker, info.keyType) === 'string';

            return (
              isStringTypeName &&
              (isNoUncheckedIndexedAccess || isNullableType(info.type))
            );
          });
        });
        return !isOwnNullable && isNullableType(prevType);
      }
      return false;
    }

    function isCallExpressionNullableOriginFromCallee(
      node: TSESTree.CallExpression,
    ): boolean {
      const prevType = getConstrainedTypeAtLocation(services, node.callee);

      if (prevType.isUnion()) {
        const isOwnNullable = prevType.types.some(type => {
          const signatures = type.getCallSignatures();
          return signatures.some(sig => isNullableType(sig.getReturnType()));
        });
        return !isOwnNullable && isNullableType(prevType);
      }

      return false;
    }

    function isOptionableExpression(node: TSESTree.Expression): boolean {
      const type = getConstrainedTypeAtLocation(services, node);
      const isOwnNullable =
        node.type === AST_NODE_TYPES.MemberExpression
          ? !isMemberExpressionNullableOriginFromObject(node)
          : node.type === AST_NODE_TYPES.CallExpression
            ? !isCallExpressionNullableOriginFromCallee(node)
            : true;

      return (
        isConditionalAlwaysNecessary(type) ||
        (isOwnNullable && isNullableType(type))
      );
    }

    function checkOptionalChain(
      node: TSESTree.CallExpression | TSESTree.MemberExpression,
      beforeOperator: TSESTree.Node,
      fix: '' | '.',
    ): void {
      // We only care if this step in the chain is optional. If just descend
      // from an optional chain, then that's fine.
      if (!node.optional) {
        return;
      }

      // Since typescript array index signature types don't represent the
      //  possibility of out-of-bounds access, if we're indexing into an array
      //  just skip the check, to avoid false positives
      if (
        !isNoUncheckedIndexedAccess &&
        optionChainContainsOptionArrayIndex(node)
      ) {
        return;
      }

      const nodeToCheck =
        node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;

      if (isOptionableExpression(nodeToCheck)) {
        return;
      }

      const questionDotOperator = nullThrows(
        context.sourceCode.getTokenAfter(
          beforeOperator,
          token =>
            token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.',
        ),
        NullThrowsReasons.MissingToken('operator', node.type),
      );

      context.report({
        loc: questionDotOperator.loc,
        node,
        messageId: 'neverOptionalChain',
        suggest: [
          {
            messageId: 'suggestRemoveOptionalChain',
            fix(fixer) {
              return fixer.replaceText(questionDotOperator, fix);
            },
          },
        ],
      });
    }

    function checkOptionalMemberExpression(
      node: TSESTree.MemberExpression,
    ): void {
      checkOptionalChain(node, node.object, node.computed ? '' : '.');
    }

    function checkOptionalCallExpression(node: TSESTree.CallExpression): void {
      checkOptionalChain(node, node.callee, '');
    }

    function checkAssignmentExpression(
      node: TSESTree.AssignmentExpression,
    ): void {
      // Similar to checkLogicalExpressionForUnnecessaryConditionals, since
      // a ||= b is equivalent to a || (a = b)
      if (['&&=', '||='].includes(node.operator)) {
        checkNode(node.left);
      } else if (node.operator === '??=') {
        checkNodeForNullish(node.left);
      }
    }

    return {
      AssignmentExpression: checkAssignmentExpression,
      BinaryExpression(node): void {
        const { operator } = node;
        if (isBoolOperator(operator)) {
          checkIfBoolExpressionIsNecessaryConditional(
            node,
            node.left,
            node.right,
            operator,
          );
        }
      },
      CallExpression: checkCallExpression,
      'CallExpression[optional = true]': checkOptionalCallExpression,
      ConditionalExpression: (node): void => checkNode(node.test),
      DoWhileStatement: checkIfLoopIsNecessaryConditional,
      ForStatement: checkIfLoopIsNecessaryConditional,
      IfStatement: (node): void => checkNode(node.test),
      LogicalExpression: checkLogicalExpressionForUnnecessaryConditionals,
      'MemberExpression[optional = true]': checkOptionalMemberExpression,
      SwitchCase({ parent, test }): void {
        // only check `case ...:`, not `default:`
        if (test) {
          checkIfBoolExpressionIsNecessaryConditional(
            test,
            parent.discriminant,
            test,
            '===',
          );
        }
      },
      WhileStatement: checkIfLoopIsNecessaryConditional,
    };
  }

isNullishType(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • tsutils.isTypeFlagSet
Code
function isNullishType(type: ts.Type): boolean {
  return tsutils.isTypeFlagSet(type, nullishFlag);
}

isAlwaysNullish(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • tsutils.unionConstituents(type).every
Code
function isAlwaysNullish(type: ts.Type): boolean {
  return tsutils.unionConstituents(type).every(isNullishType);
}

isPossiblyNullish(type: ts.Type): boolean

Note that this differs from in that it doesn't consider any or unknown to be nullable.

Raw JSDoc
/**
 * Note that this differs from {@link isNullableType} in that it doesn't consider
 * `any` or `unknown` to be nullable.
 */

Calls:

  • tsutils .unionConstituents(type) .some
  • isNullishType
  • isTypeFlagSet (from ../util)
Code
function isPossiblyNullish(type: ts.Type): boolean {
  return tsutils
    .unionConstituents(type)
    .some(t => isNullishType(t) || isTypeFlagSet(t, ts.TypeFlags.Void));
}

toStaticValue(type: ts.Type): { value: bigint | boolean | number | string | null | undefi…

Parameters:

  • type ts.Type

Returns: { value: bigint | boolean | number | string | null | undefined } | undefined

Calls:

  • tsutils.isBooleanLiteralType
  • tsutils.isTrueLiteralType
  • type.isLiteral
  • getValueOfLiteralType (from ../util)

Internal Comments:

// type.isLiteral() only covers numbers/bigints and strings, hence the rest of the branches.

Code
function toStaticValue(
  type: ts.Type,
):
  { value: bigint | boolean | number | string | null | undefined } | undefined {
  // type.isLiteral() only covers numbers/bigints and strings, hence the rest of the branches.
  if (tsutils.isBooleanLiteralType(type)) {
    return { value: tsutils.isTrueLiteralType(type) };
  }
  if (type.flags === ts.TypeFlags.Undefined) {
    return { value: undefined };
  }
  if (type.flags === ts.TypeFlags.Null) {
    return { value: null };
  }
  if (type.isLiteral()) {
    return { value: getValueOfLiteralType(type) };
  }

  return undefined;
}

isBoolOperator(operator: string): operator is BoolOperator

Parameters:

  • operator string

Returns: operator is BoolOperator

Calls:

  • (BOOL_OPERATORS as Set<string>).has
Code
function isBoolOperator(operator: string): operator is BoolOperator {
  return (BOOL_OPERATORS as Set<string>).has(operator);
}

booleanComparison(left: unknown, operator: BoolOperator, right: unknown): boolean

Parameters:

  • left unknown
  • operator BoolOperator
  • right unknown

Returns: boolean

Internal Comments:

// eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality (x2)
// @ts-expect-error: we don't care if the comparison seems unintentional. (x4)

Code
function booleanComparison(
  left: unknown,
  operator: BoolOperator,
  right: unknown,
): boolean {
  switch (operator) {
    case '!=':
      // eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality
      return left != right;
    case '!==':
      return left !== right;
    case '<':
      // @ts-expect-error: we don't care if the comparison seems unintentional.
      return left < right;
    case '<=':
      // @ts-expect-error: we don't care if the comparison seems unintentional.
      return left <= right;
    case '==':
      // eslint-disable-next-line eqeqeq -- intentionally comparing with loose equality
      return left == right;
    case '===':
      return left === right;
    case '>':
      // @ts-expect-error: we don't care if the comparison seems unintentional.
      return left > right;
    case '>=':
      // @ts-expect-error: we don't care if the comparison seems unintentional.
      return left >= right;
  }
}

isOnlyUsedForTruthiness(node: TSESTree.Expression): boolean

Parameters:

  • node TSESTree.Expression

Returns: boolean

Calls:

  • isOnlyUsedForTruthiness
Code
function isOnlyUsedForTruthiness(node: TSESTree.Expression): boolean {
  const parent = node.parent;

  switch (parent.type) {
    case AST_NODE_TYPES.ConditionalExpression:
    case AST_NODE_TYPES.DoWhileStatement:
    case AST_NODE_TYPES.ForStatement:
    case AST_NODE_TYPES.IfStatement:
    case AST_NODE_TYPES.WhileStatement:
      return parent.test === node;

    case AST_NODE_TYPES.LogicalExpression:
      return (
        (parent.operator === '&&' && parent.left === node) ||
        isOnlyUsedForTruthiness(parent)
      );

    case AST_NODE_TYPES.UnaryExpression:
      return parent.operator === '!';

    default:
      return false;
  }
}

normalizeAllowConstantLoopConditions(allowConstantLoopConditions: AllowConstantLoopConditions | LegacyAll…): AllowConstantLoopConditions

Parameters:

  • allowConstantLoopConditions AllowConstantLoopConditions | LegacyAllowConstantLoopConditions

Returns: AllowConstantLoopConditions

Code
function normalizeAllowConstantLoopConditions(
  allowConstantLoopConditions:
    AllowConstantLoopConditions | LegacyAllowConstantLoopConditions,
): AllowConstantLoopConditions {
  if (allowConstantLoopConditions === true) {
    return 'always';
  }

  if (allowConstantLoopConditions === false) {
    return 'never';
  }

  return allowConstantLoopConditions;
}

Internal helpers

Declared inside another function in this file.

nodeIsArrayType(node: TSESTree.Expression): boolean

Parameters:

  • node TSESTree.Expression

Returns: boolean

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • tsutils .unionConstituents(nodeType) .some
  • checker.isArrayType
Code
function nodeIsArrayType(node: TSESTree.Expression): boolean {
      const nodeType = getConstrainedTypeAtLocation(services, node);
      return tsutils
        .unionConstituents(nodeType)
        .some(part => checker.isArrayType(part));
    }

nodeIsTupleType(node: TSESTree.Expression): boolean

Parameters:

  • node TSESTree.Expression

Returns: boolean

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • tsutils .unionConstituents(nodeType) .some
  • checker.isTupleType
Code
function nodeIsTupleType(node: TSESTree.Expression): boolean {
      const nodeType = getConstrainedTypeAtLocation(services, node);
      return tsutils
        .unionConstituents(nodeType)
        .some(part => checker.isTupleType(part));
    }

isArrayIndexExpression(node: TSESTree.Expression): boolean

Parameters:

  • node TSESTree.Expression

Returns: boolean

Calls:

  • nodeIsArrayType
  • nodeIsTupleType

Internal Comments:

// Is an index signature (x5)
// ...into an array type
// ... or a tuple type
// Exception: literal index into a tuple - will have a sound type (x4)

Code
function isArrayIndexExpression(node: TSESTree.Expression): boolean {
      return (
        // Is an index signature
        node.type === AST_NODE_TYPES.MemberExpression &&
        node.computed &&
        // ...into an array type
        (nodeIsArrayType(node.object) ||
          // ... or a tuple type
          (nodeIsTupleType(node.object) &&
            // Exception: literal index into a tuple - will have a sound type
            node.property.type !== AST_NODE_TYPES.Literal))
      );
    }

isConditionalAlwaysNecessary(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • tsutils .unionConstituents(type) .some
  • isTypeAnyType (from ../util)
  • isTypeUnknownType (from ../util)
  • isTypeFlagSet (from ../util)
Code
function isConditionalAlwaysNecessary(type: ts.Type): boolean {
      return tsutils
        .unionConstituents(type)
        .some(
          part =>
            isTypeAnyType(part) ||
            isTypeUnknownType(part) ||
            isTypeFlagSet(part, ts.TypeFlags.TypeVariable),
        );
    }

isNullableMemberExpression(node: TSESTree.MemberExpression): boolean

Parameters:

  • node TSESTree.MemberExpression

Returns: boolean

Calls:

  • services.getTypeAtLocation
  • isNullablePropertyType
  • context.sourceCode.getText
  • objectType .getProperties() .find
  • tsutils.isSymbolFlagSet

Internal Comments:

// Get the actual property name, to account for private properties (this.#prop). (x2)

Code
function isNullableMemberExpression(
      node: TSESTree.MemberExpression,
    ): boolean {
      const objectType = services.getTypeAtLocation(node.object);
      if (node.computed) {
        const propertyType = services.getTypeAtLocation(node.property);
        return isNullablePropertyType(objectType, propertyType);
      }
      const property = node.property;

      // Get the actual property name, to account for private properties (this.#prop).
      const propertyName = context.sourceCode.getText(property);

      const propertyType = objectType
        .getProperties()
        .find(prop => prop.name === propertyName);

      if (
        propertyType &&
        tsutils.isSymbolFlagSet(propertyType, ts.SymbolFlags.Optional)
      ) {
        return true;
      }

      return false;
    }

checkNode(expression: TSESTree.Expression, isUnaryNotArgument: boolean, node: TSESTree.Expression): void

Checks if a conditional node is necessary: if the type of the node is always true or always false, it's not necessary.

Raw JSDoc
/**
     * Checks if a conditional node is necessary:
     * if the type of the node is always true or always false, it's not necessary.
     */

Calls:

  • checkNode
  • isArrayIndexExpression
  • isOnlyUsedForTruthiness
  • getConstrainedTypeAtLocation (from ../util)
  • isConditionalAlwaysNecessary
  • isTypeFlagSet (from ../util)
  • isPossiblyTruthy (from ../util)
  • isPossiblyFalsy (from ../util)
  • context.report

Internal Comments:

// Check if the node is Unary Negation expression and handle it
// Since typescript array index signature types don't represent the
//  possibility of out-of-bounds access, if we're indexing into an array
//  just skip the check, to avoid false positives
// Only check the right side when the logical expression is used for
// truthiness, as the left side has already been checked by
// checkLogicalExpressionForUnnecessaryConditionals.
//
// Unless the node is nullish coalescing, as it's common to use patterns like
// `nullBool ?? true` to perform strict boolean checks. If we inspect the right
// here, it'll usually be a constant condition on purpose.
// In this case it's better to inspect the type of the expression as a whole.

Code
function checkNode(
      expression: TSESTree.Expression,
      isUnaryNotArgument = false,
      node = expression,
    ): void {
      // Check if the node is Unary Negation expression and handle it
      if (
        expression.type === AST_NODE_TYPES.UnaryExpression &&
        expression.operator === '!'
      ) {
        return checkNode(expression.argument, !isUnaryNotArgument, node);
      }

      // Since typescript array index signature types don't represent the
      //  possibility of out-of-bounds access, if we're indexing into an array
      //  just skip the check, to avoid false positives
      if (!isNoUncheckedIndexedAccess && isArrayIndexExpression(expression)) {
        return;
      }

      // Only check the right side when the logical expression is used for
      // truthiness, as the left side has already been checked by
      // checkLogicalExpressionForUnnecessaryConditionals.
      //
      // Unless the node is nullish coalescing, as it's common to use patterns like
      // `nullBool ?? true` to perform strict boolean checks. If we inspect the right
      // here, it'll usually be a constant condition on purpose.
      // In this case it's better to inspect the type of the expression as a whole.
      if (
        expression.type === AST_NODE_TYPES.LogicalExpression &&
        expression.operator !== '??'
      ) {
        if (isOnlyUsedForTruthiness(expression)) {
          checkNode(expression.right);
        }
        return;
      }

      const type = getConstrainedTypeAtLocation(services, expression);

      if (isConditionalAlwaysNecessary(type)) {
        return;
      }
      let messageId: MessageId | null = null;

      if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
        messageId = 'never';
      } else if (!isPossiblyTruthy(type)) {
        messageId = !isUnaryNotArgument ? 'alwaysFalsy' : 'alwaysTruthy';
      } else if (!isPossiblyFalsy(type)) {
        messageId = !isUnaryNotArgument ? 'alwaysTruthy' : 'alwaysFalsy';
      }

      if (messageId) {
        context.report({ node, messageId });
      }
    }

checkNodeForNullish(node: TSESTree.Expression): void

Parameters:

  • node TSESTree.Expression

Returns: void

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • isTypeFlagSet (from ../util)
  • isPossiblyNullish
  • isNullableMemberExpression
  • isArrayIndexExpression
  • optionChainContainsOptionArrayIndex
  • isAlwaysNullish
  • context.report

Internal Comments:

// Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
// Since typescript array index signature types don't represent the
//  possibility of out-of-bounds access, if we're indexing into an array
//  just skip the check, to avoid false positives

Code
function checkNodeForNullish(node: TSESTree.Expression): void {
      const type = getConstrainedTypeAtLocation(services, node);

      // Conditional is always necessary if it involves `any`, `unknown` or a naked type parameter
      if (
        isTypeFlagSet(
          type,
          ts.TypeFlags.Any |
            ts.TypeFlags.Unknown |
            ts.TypeFlags.TypeParameter |
            ts.TypeFlags.TypeVariable,
        )
      ) {
        return;
      }

      let messageId: MessageId | null = null;
      if (isTypeFlagSet(type, ts.TypeFlags.Never)) {
        messageId = 'never';
      } else if (
        !isPossiblyNullish(type) &&
        !(
          node.type === AST_NODE_TYPES.MemberExpression &&
          isNullableMemberExpression(node)
        )
      ) {
        // Since typescript array index signature types don't represent the
        //  possibility of out-of-bounds access, if we're indexing into an array
        //  just skip the check, to avoid false positives
        if (
          isNoUncheckedIndexedAccess ||
          (!isArrayIndexExpression(node) &&
            !(
              node.type === AST_NODE_TYPES.ChainExpression &&
              node.expression.type !== AST_NODE_TYPES.TSNonNullExpression &&
              optionChainContainsOptionArrayIndex(node.expression)
            ))
        ) {
          messageId = 'neverNullish';
        }
      } else if (isAlwaysNullish(type)) {
        messageId = 'alwaysNullish';
      }

      if (messageId) {
        context.report({ node, messageId });
      }
    }

checkIfBoolExpressionIsNecessaryConditional(node: TSESTree.Node, left: TSESTree.Node, right: TSESTree.Node, operator: BoolOperator): void

Checks that a binary expression is necessarily conditional, reports otherwise. If both sides of the binary expression are literal values, it's not a necessary condition.

NOTE: It's also unnecessary if the types that don't overlap at all but that case is handled by the Typescript compiler itself. Known exceptions: - https://github.com/microsoft/TypeScript/issues/32627 - https://github.com/microsoft/TypeScript/issues/37160 (handled)

Raw JSDoc
/**
     * Checks that a binary expression is necessarily conditional, reports otherwise.
     * If both sides of the binary expression are literal values, it's not a necessary condition.
     *
     * NOTE: It's also unnecessary if the types that don't overlap at all
     *    but that case is handled by the Typescript compiler itself.
     *    Known exceptions:
     *      - https://github.com/microsoft/TypeScript/issues/32627
     *      - https://github.com/microsoft/TypeScript/issues/37160 (handled)
     */

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • toStaticValue
  • booleanComparison
  • context.report
  • checker.typeToString
  • isTypeFlagSet (from ../util)
  • isComparable

Internal Comments:

// Workaround for https://github.com/microsoft/TypeScript/issues/37160
// Allow comparison to `any`, `unknown` or a naked type parameter. (x3)
// Allow loose comparison to nullish values.

Code
function checkIfBoolExpressionIsNecessaryConditional(
      node: TSESTree.Node,
      left: TSESTree.Node,
      right: TSESTree.Node,
      operator: BoolOperator,
    ): void {
      const leftType = getConstrainedTypeAtLocation(services, left);
      const rightType = getConstrainedTypeAtLocation(services, right);

      const leftStaticValue = toStaticValue(leftType);
      const rightStaticValue = toStaticValue(rightType);

      if (leftStaticValue != null && rightStaticValue != null) {
        const conditionIsTrue = booleanComparison(
          leftStaticValue.value,
          operator,
          rightStaticValue.value,
        );

        context.report({
          node,
          messageId: 'comparisonBetweenLiteralTypes',
          data: {
            left: checker.typeToString(leftType),
            operator,
            right: checker.typeToString(rightType),
            trueOrFalse: conditionIsTrue ? 'true' : 'false',
          },
        });
        return;
      }

      // Workaround for https://github.com/microsoft/TypeScript/issues/37160
      if (isStrictNullChecks) {
        const UNDEFINED = ts.TypeFlags.Undefined;
        const NULL = ts.TypeFlags.Null;
        const VOID = ts.TypeFlags.Void;
        const isComparable = (type: ts.Type, flag: ts.TypeFlags): boolean => {
          // Allow comparison to `any`, `unknown` or a naked type parameter.
          flag |=
            ts.TypeFlags.Any |
            ts.TypeFlags.Unknown |
            ts.TypeFlags.TypeParameter |
            ts.TypeFlags.TypeVariable;

          // Allow loose comparison to nullish values.
          if (operator === '==' || operator === '!=') {
            flag |= NULL | UNDEFINED | VOID;
          }

          return isTypeFlagSet(type, flag);
        };

        if (
          (leftType.flags === UNDEFINED &&
            !isComparable(rightType, UNDEFINED | VOID)) ||
          (rightType.flags === UNDEFINED &&
            !isComparable(leftType, UNDEFINED | VOID)) ||
          (leftType.flags === NULL && !isComparable(rightType, NULL)) ||
          (rightType.flags === NULL && !isComparable(leftType, NULL))
        ) {
          context.report({ node, messageId: 'noOverlapBooleanExpression' });
          return;
        }
      }
    }

isComparable(type: ts.Type, flag: ts.TypeFlags): boolean

Parameters:

  • type ts.Type
  • flag ts.TypeFlags

Returns: boolean

Calls:

  • isTypeFlagSet (from ../util)

Internal Comments:

// Allow comparison to `any`, `unknown` or a naked type parameter. (x3)
// Allow loose comparison to nullish values.

Code
(type: ts.Type, flag: ts.TypeFlags): boolean => {
          // Allow comparison to `any`, `unknown` or a naked type parameter.
          flag |=
            ts.TypeFlags.Any |
            ts.TypeFlags.Unknown |
            ts.TypeFlags.TypeParameter |
            ts.TypeFlags.TypeVariable;

          // Allow loose comparison to nullish values.
          if (operator === '==' || operator === '!=') {
            flag |= NULL | UNDEFINED | VOID;
          }

          return isTypeFlagSet(type, flag);
        }

checkLogicalExpressionForUnnecessaryConditionals(node: TSESTree.LogicalExpression): void

Checks that a logical expression contains a boolean, reports otherwise.

Raw JSDoc
/**
     * Checks that a logical expression contains a boolean, reports otherwise.
     */

Calls:

  • checkNodeForNullish
  • checkNode

Internal Comments:

// Only checks the left side, since the right side might not be "conditional" at all. (x3)
// The right side will be checked if the LogicalExpression is used in a conditional context (x3)

Code
function checkLogicalExpressionForUnnecessaryConditionals(
      node: TSESTree.LogicalExpression,
    ): void {
      if (node.operator === '??') {
        checkNodeForNullish(node.left);
        return;
      }
      // Only checks the left side, since the right side might not be "conditional" at all.
      // The right side will be checked if the LogicalExpression is used in a conditional context
      checkNode(node.left);
    }

checkIfLoopIsNecessaryConditional(node: | TSESTree.DoWhileStatement | TSESTree.…): void

Checks that a testable expression of a loop is necessarily conditional, reports otherwise.

Raw JSDoc
/**
     * Checks that a testable expression of a loop is necessarily conditional, reports otherwise.
     */

Calls:

  • constantLoopConditionsAllowedLiterals.has
  • tsutils.isTrueLiteralType
  • getConstrainedTypeAtLocation (from ../util)
  • checkNode

Internal Comments:

// e.g. `for(;;)`

Code
function checkIfLoopIsNecessaryConditional(
      node:
        | TSESTree.DoWhileStatement
        | TSESTree.ForStatement
        | TSESTree.WhileStatement,
    ): void {
      if (node.test == null) {
        // e.g. `for(;;)`
        return;
      }

      if (
        allowConstantLoopConditionsOption === 'only-allowed-literals' &&
        node.test.type === AST_NODE_TYPES.Literal &&
        constantLoopConditionsAllowedLiterals.has(node.test.value)
      ) {
        return;
      }

      if (
        allowConstantLoopConditionsOption === 'always' &&
        tsutils.isTrueLiteralType(
          getConstrainedTypeAtLocation(services, node.test),
        )
      ) {
        return;
      }

      checkNode(node.test);
    }

checkCallExpression(node: TSESTree.CallExpression): void

Parameters:

  • node TSESTree.CallExpression

Returns: void

Calls:

  • findTruthinessAssertedArgument (from ../util/assertionFunctionUtils)
  • checkNode
  • findTypeGuardAssertedArgument (from ../util/assertionFunctionUtils)
  • getConstrainedTypeAtLocation (from ../util)
  • tsutils.isTypeFlagSet
  • checker.isTypeAssignableTo
  • typeGuardAssertedArgument.type.isUnion
  • context.report
  • isArrayMethodCallWithPredicate (from ../util)
  • tsutils .getCallSignaturesOfType( getConstrainedTypeAtLocation(services, callback), ) .map
  • sig.getReturnType
  • getConstraintInfo (from ../util)
  • isTypeAnyType (from ../util)
  • isTypeUnknownType (from ../util)
  • isPossiblyFalsy (from ../util)
  • isPossiblyTruthy (from ../util)

Internal Comments:

// Skip `any` β€” it is assignable to everything, producing (x3)
// false positives for meaningful runtime type guards. (x3)
// Only flag if the types are mutually assignable (i.e. equivalent,
// like Narrower ↔ Wider with optional props) or the predicate type
// is a union that the argument is a strict subtype of.  This avoids
// false positives with structural subtypes whose extra members are
// all optional in the *predicate* type (e.g. custom MappedType
// interfaces extending ts.Type).
// If this is something like arr.filter(x => /*condition*/), check `condition`
// Inline defined functions
// Two special cases, where we can directly check the node that's returned:
// () => something
// () => { return something; } (x2)
// Otherwise just do type analysis on the function as a whole. (x2)
// Not a callable function, e.g. `any`
// Predicate is always necessary if it involves `any` or `unknown`
// bail early if both a possibly-truthy and a possibly-falsy have been detected

Code
function checkCallExpression(node: TSESTree.CallExpression): void {
      if (checkTypePredicates) {
        const truthinessAssertedArgument = findTruthinessAssertedArgument(
          services,
          node,
        );
        if (truthinessAssertedArgument != null) {
          checkNode(truthinessAssertedArgument);
        }

        const typeGuardAssertedArgument = findTypeGuardAssertedArgument(
          services,
          node,
        );
        if (typeGuardAssertedArgument != null) {
          const typeOfArgument = getConstrainedTypeAtLocation(
            services,
            typeGuardAssertedArgument.argument,
          );
          if (
            // Skip `any` β€” it is assignable to everything, producing
            // false positives for meaningful runtime type guards.
            !tsutils.isTypeFlagSet(
              typeOfArgument,
              ts.TypeFlags.Any | ts.TypeFlags.Unknown,
            ) &&
            checker.isTypeAssignableTo(
              typeOfArgument,
              typeGuardAssertedArgument.type,
            ) &&
            // Only flag if the types are mutually assignable (i.e. equivalent,
            // like Narrower ↔ Wider with optional props) or the predicate type
            // is a union that the argument is a strict subtype of.  This avoids
            // false positives with structural subtypes whose extra members are
            // all optional in the *predicate* type (e.g. custom MappedType
            // interfaces extending ts.Type).
            (checker.isTypeAssignableTo(
              typeGuardAssertedArgument.type,
              typeOfArgument,
            ) ||
              typeGuardAssertedArgument.type.isUnion())
          ) {
            context.report({
              node: typeGuardAssertedArgument.argument,
              messageId: 'typeGuardAlreadyIsType',
              data: {
                typeGuardOrAssertionFunction: typeGuardAssertedArgument.asserts
                  ? 'assertion function'
                  : 'type guard',
              },
            });
          }
        }
      }

      // If this is something like arr.filter(x => /*condition*/), check `condition`
      if (
        isArrayMethodCallWithPredicate(context, services, node) &&
        node.arguments.length
      ) {
        const callback = node.arguments[0];
        // Inline defined functions
        if (
          callback.type === AST_NODE_TYPES.ArrowFunctionExpression ||
          callback.type === AST_NODE_TYPES.FunctionExpression
        ) {
          // Two special cases, where we can directly check the node that's returned:
          // () => something
          if (callback.body.type !== AST_NODE_TYPES.BlockStatement) {
            return checkNode(callback.body);
          }
          // () => { return something; }
          const callbackBody = callback.body.body;
          if (
            callbackBody.length === 1 &&
            callbackBody[0].type === AST_NODE_TYPES.ReturnStatement &&
            callbackBody[0].argument
          ) {
            return checkNode(callbackBody[0].argument);
          }
          // Potential enhancement: could use code-path analysis to check
          //   any function with a single return statement
          // (Value to complexity ratio is dubious however)
        }
        // Otherwise just do type analysis on the function as a whole.
        const returnTypes = tsutils
          .getCallSignaturesOfType(
            getConstrainedTypeAtLocation(services, callback),
          )
          .map(sig => sig.getReturnType());

        if (returnTypes.length === 0) {
          // Not a callable function, e.g. `any`
          return;
        }

        let hasFalsyReturnTypes = false;
        let hasTruthyReturnTypes = false;

        for (const type of returnTypes) {
          const { constraintType } = getConstraintInfo(checker, type);
          // Predicate is always necessary if it involves `any` or `unknown`
          if (
            !constraintType ||
            isTypeAnyType(constraintType) ||
            isTypeUnknownType(constraintType)
          ) {
            return;
          }

          if (isPossiblyFalsy(constraintType)) {
            hasFalsyReturnTypes = true;
          }

          if (isPossiblyTruthy(constraintType)) {
            hasTruthyReturnTypes = true;
          }

          // bail early if both a possibly-truthy and a possibly-falsy have been detected
          if (hasFalsyReturnTypes && hasTruthyReturnTypes) {
            return;
          }
        }

        if (!hasFalsyReturnTypes) {
          return context.report({
            node: callback,
            messageId: 'alwaysTruthyFunc',
          });
        }

        if (!hasTruthyReturnTypes) {
          return context.report({
            node: callback,
            messageId: 'alwaysFalsyFunc',
          });
        }
      }
    }

optionChainContainsOptionArrayIndex(node: TSESTree.CallExpression | TSESTree.Memb…): boolean

Parameters:

  • node TSESTree.CallExpression | TSESTree.MemberExpression

Returns: boolean

Calls:

  • isArrayIndexExpression
  • optionChainContainsOptionArrayIndex
Code
function optionChainContainsOptionArrayIndex(
      node: TSESTree.CallExpression | TSESTree.MemberExpression,
    ): boolean {
      const lhsNode =
        node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;
      if (node.optional && isArrayIndexExpression(lhsNode)) {
        return true;
      }
      if (
        lhsNode.type === AST_NODE_TYPES.MemberExpression ||
        lhsNode.type === AST_NODE_TYPES.CallExpression
      ) {
        return optionChainContainsOptionArrayIndex(lhsNode);
      }
      return false;
    }

isNullablePropertyType(objType: ts.Type, propertyType: ts.Type): boolean

Parameters:

  • objType ts.Type
  • propertyType ts.Type

Returns: boolean

Calls:

  • propertyType.isUnion
  • propertyType.types.some
  • isNullablePropertyType
  • propertyType.isNumberLiteral
  • propertyType.isStringLiteral
  • getTypeOfPropertyOfName (from ../util)
  • propertyType.value.toString
  • isNullableType (from ../util)
  • getTypeName (from ../util)
  • checker .getIndexInfosOfType(objType) .some
Code
function isNullablePropertyType(
      objType: ts.Type,
      propertyType: ts.Type,
    ): boolean {
      if (propertyType.isUnion()) {
        return propertyType.types.some(type =>
          isNullablePropertyType(objType, type),
        );
      }
      if (propertyType.isNumberLiteral() || propertyType.isStringLiteral()) {
        const propType = getTypeOfPropertyOfName(
          checker,
          objType,
          propertyType.value.toString(),
        );
        if (propType) {
          return isNullableType(propType);
        }
      }
      const typeName = getTypeName(checker, propertyType);
      return checker
        .getIndexInfosOfType(objType)
        .some(info => getTypeName(checker, info.keyType) === typeName);
    }

isMemberExpressionNullableOriginFromObject(node: TSESTree.MemberExpression): boolean

Parameters:

  • node TSESTree.MemberExpression

Returns: boolean

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • prevType.isUnion
  • isIdentifier (from ../util)
  • prevType.types.some
  • isNullablePropertyType
  • getTypeOfPropertyOfName (from ../util)
  • isNullableType (from ../util)
  • checker.getIndexInfosOfType
  • indexInfo.some
  • getTypeName (from ../util)
Code
function isMemberExpressionNullableOriginFromObject(
      node: TSESTree.MemberExpression,
    ): boolean {
      const prevType = getConstrainedTypeAtLocation(services, node.object);
      const property = node.property;
      if (prevType.isUnion() && isIdentifier(property)) {
        const isOwnNullable = prevType.types.some(type => {
          if (node.computed) {
            const propertyType = getConstrainedTypeAtLocation(
              services,
              node.property,
            );
            return isNullablePropertyType(type, propertyType);
          }
          const propType = getTypeOfPropertyOfName(
            checker,
            type,
            property.name,
          );

          if (propType) {
            return isNullableType(propType);
          }
          const indexInfo = checker.getIndexInfosOfType(type);

          return indexInfo.some(info => {
            const isStringTypeName =
              getTypeName(checker, info.keyType) === 'string';

            return (
              isStringTypeName &&
              (isNoUncheckedIndexedAccess || isNullableType(info.type))
            );
          });
        });
        return !isOwnNullable && isNullableType(prevType);
      }
      return false;
    }

isCallExpressionNullableOriginFromCallee(node: TSESTree.CallExpression): boolean

Parameters:

  • node TSESTree.CallExpression

Returns: boolean

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • prevType.isUnion
  • prevType.types.some
  • type.getCallSignatures
  • signatures.some
  • isNullableType (from ../util)
  • sig.getReturnType
Code
function isCallExpressionNullableOriginFromCallee(
      node: TSESTree.CallExpression,
    ): boolean {
      const prevType = getConstrainedTypeAtLocation(services, node.callee);

      if (prevType.isUnion()) {
        const isOwnNullable = prevType.types.some(type => {
          const signatures = type.getCallSignatures();
          return signatures.some(sig => isNullableType(sig.getReturnType()));
        });
        return !isOwnNullable && isNullableType(prevType);
      }

      return false;
    }

isOptionableExpression(node: TSESTree.Expression): boolean

Parameters:

  • node TSESTree.Expression

Returns: boolean

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • isMemberExpressionNullableOriginFromObject
  • isCallExpressionNullableOriginFromCallee
  • isConditionalAlwaysNecessary
  • isNullableType (from ../util)
Code
function isOptionableExpression(node: TSESTree.Expression): boolean {
      const type = getConstrainedTypeAtLocation(services, node);
      const isOwnNullable =
        node.type === AST_NODE_TYPES.MemberExpression
          ? !isMemberExpressionNullableOriginFromObject(node)
          : node.type === AST_NODE_TYPES.CallExpression
            ? !isCallExpressionNullableOriginFromCallee(node)
            : true;

      return (
        isConditionalAlwaysNecessary(type) ||
        (isOwnNullable && isNullableType(type))
      );
    }

checkOptionalChain(node: TSESTree.CallExpression | TSESTree.Memb…, beforeOperator: TSESTree.Node, fix: '' | '.'): void

Parameters:

  • node TSESTree.CallExpression | TSESTree.MemberExpression
  • beforeOperator TSESTree.Node
  • fix '' | '.'

Returns: void

Calls:

  • optionChainContainsOptionArrayIndex
  • isOptionableExpression
  • nullThrows (from ../util)
  • context.sourceCode.getTokenAfter
  • NullThrowsReasons.MissingToken
  • context.report
  • fixer.replaceText

Internal Comments:

// We only care if this step in the chain is optional. If just descend
// from an optional chain, then that's fine.
// Since typescript array index signature types don't represent the
//  possibility of out-of-bounds access, if we're indexing into an array
//  just skip the check, to avoid false positives

Code
function checkOptionalChain(
      node: TSESTree.CallExpression | TSESTree.MemberExpression,
      beforeOperator: TSESTree.Node,
      fix: '' | '.',
    ): void {
      // We only care if this step in the chain is optional. If just descend
      // from an optional chain, then that's fine.
      if (!node.optional) {
        return;
      }

      // Since typescript array index signature types don't represent the
      //  possibility of out-of-bounds access, if we're indexing into an array
      //  just skip the check, to avoid false positives
      if (
        !isNoUncheckedIndexedAccess &&
        optionChainContainsOptionArrayIndex(node)
      ) {
        return;
      }

      const nodeToCheck =
        node.type === AST_NODE_TYPES.CallExpression ? node.callee : node.object;

      if (isOptionableExpression(nodeToCheck)) {
        return;
      }

      const questionDotOperator = nullThrows(
        context.sourceCode.getTokenAfter(
          beforeOperator,
          token =>
            token.type === AST_TOKEN_TYPES.Punctuator && token.value === '?.',
        ),
        NullThrowsReasons.MissingToken('operator', node.type),
      );

      context.report({
        loc: questionDotOperator.loc,
        node,
        messageId: 'neverOptionalChain',
        suggest: [
          {
            messageId: 'suggestRemoveOptionalChain',
            fix(fixer) {
              return fixer.replaceText(questionDotOperator, fix);
            },
          },
        ],
      });
    }

checkOptionalMemberExpression(node: TSESTree.MemberExpression): void

Parameters:

  • node TSESTree.MemberExpression

Returns: void

Calls:

  • checkOptionalChain
Code
function checkOptionalMemberExpression(
      node: TSESTree.MemberExpression,
    ): void {
      checkOptionalChain(node, node.object, node.computed ? '' : '.');
    }

checkOptionalCallExpression(node: TSESTree.CallExpression): void

Parameters:

  • node TSESTree.CallExpression

Returns: void

Calls:

  • checkOptionalChain
Code
function checkOptionalCallExpression(node: TSESTree.CallExpression): void {
      checkOptionalChain(node, node.callee, '');
    }

checkAssignmentExpression(node: TSESTree.AssignmentExpression): void

Parameters:

  • node TSESTree.AssignmentExpression

Returns: void

Calls:

  • ['&&=', '||='].includes
  • checkNode
  • checkNodeForNullish

Internal Comments:

// Similar to checkLogicalExpressionForUnnecessaryConditionals, since
// a ||= b is equivalent to a || (a = b)

Code
function checkAssignmentExpression(
      node: TSESTree.AssignmentExpression,
    ): void {
      // Similar to checkLogicalExpressionForUnnecessaryConditionals, since
      // a ||= b is equivalent to a || (a = b)
      if (['&&=', '||='].includes(node.operator)) {
        checkNode(node.left);
      } else if (node.operator === '??=') {
        checkNodeForNullish(node.left);
      }
    }

Type Aliases

BoolOperator

type BoolOperator = typeof BOOL_OPERATORS extends Set<infer T> ? T : never;

LegacyAllowConstantLoopConditions

type LegacyAllowConstantLoopConditions = boolean;

AllowConstantLoopConditions

type AllowConstantLoopConditions = 'always' | 'never' | 'only-allowed-literals';

Options

type Options = [
  {
    allowConstantLoopConditions?:
      AllowConstantLoopConditions | LegacyAllowConstantLoopConditions;
    allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
    checkTypePredicates?: boolean;
  },
];

MessageId

type MessageId = | 'alwaysFalsy'
  | 'alwaysFalsyFunc'
  | 'alwaysNullish'
  | 'alwaysTruthy'
  | 'alwaysTruthyFunc'
  | 'comparisonBetweenLiteralTypes'
  | 'never'
  | 'neverNullish'
  | 'neverOptionalChain'
  | 'noOverlapBooleanExpression'
  | 'noStrictNullCheck'
  | 'suggestRemoveOptionalChain'
  | 'typeGuardAlreadyIsType';

Generated by Syntax Scribe