Skip to content

⬅️ Back to Table of Contents

📄 strict-boolean-expressions

📊 Analysis Summary

Metric Count
🔧 Functions 17
📦 Imports 14
📑 Type Aliases 5

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/strict-boolean-expressions.ts

📤 Default Export

export default createRule<Options, MessageId>({ ... })
Property Value
name 'strict-boolean-expressions'
meta.type 'suggestion'
meta.docs.description 'Disallow certain types in boolean expressions'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.conditionErrorAny 'Unexpected any value in {{context}}. ' + 'An explicit comparison or type conversion is required.'
meta.messages.conditionErrorNullableBoolean 'Unexpected nullable boolean value in {{context}}. ' + 'Please handle the nullish case explicitly.'
meta.messages.conditionErrorNullableEnum 'Unexpected nullable enum value in {{context}}. ' + 'Please handle the nullish/zero/NaN cases explicitly.'
meta.messages.conditionErrorNullableNumber 'Unexpected nullable number value in {{context}}. ' + 'Please handle the nullish/zero/NaN cases explicitly.'
meta.messages.conditionErrorNullableObject 'Unexpected nullable object value in {{context}}. ' + 'An explicit null check is required.'
meta.messages.conditionErrorNullableString 'Unexpected nullable string value in {{context}}. ' + 'Please handle the nullish/empty cases explicitly.'
meta.messages.conditionErrorNullish 'Unexpected nullish value in conditional. ' + 'The condition is always false.'
meta.messages.conditionErrorNumber 'Unexpected number value in {{context}}. ' + 'An explicit zero/NaN check is required.'
meta.messages.conditionErrorObject 'Unexpected object value in {{context}}. ' + 'The condition is always true.'
meta.messages.conditionErrorOther 'Unexpected value in conditional. ' + 'A boolean expression is required.'
meta.messages.conditionErrorString 'Unexpected string value in {{context}}. ' + 'An explicit empty string check is required.'
meta.messages.conditionFixCastBoolean 'Explicitly convert value to a boolean (Boolean(value))'
meta.messages.conditionFixCompareArrayLengthNonzero "Change condition to check array's length (value.length > 0)"
meta.messages.conditionFixCompareArrayLengthZero "Change condition to check array's length (value.length === 0)"
meta.messages.conditionFixCompareEmptyString 'Change condition to check for empty string (value !== "")'
meta.messages.conditionFixCompareFalse 'Change condition to check if false (value === false)'
meta.messages.conditionFixCompareNaN 'Change condition to check for NaN (!Number.isNaN(value))'
meta.messages.conditionFixCompareNullish 'Change condition to check for null/undefined (value != null)'
meta.messages.conditionFixCompareStringLength "Change condition to check string's length (value.length !== 0)"
meta.messages.conditionFixCompareTrue 'Change condition to check if true (value === true)'
meta.messages.conditionFixCompareZero 'Change condition to check for 0 (value !== 0)'
meta.messages.conditionFixDefaultEmptyString 'Explicitly treat nullish value the same as an empty string (value ?? "")'
meta.messages.conditionFixDefaultFalse 'Explicitly treat nullish value the same as false (value ?? false)'
meta.messages.conditionFixDefaultZero 'Explicitly treat nullish value the same as 0 (value ?? 0)'
meta.messages.explicitBooleanReturnType 'Add an explicit boolean return type annotation.'
meta.messages.noStrictNullCheck 'This rule requires the strictNullChecks compiler option to be turned on to function correctly.'
meta.messages.predicateCannotBeAsync "Predicate function should not be 'async'; expected a boolean return type."
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowAny: { type: 'boolean', description: 'Whether to ...
defaultOptions [ { allowAny: false, allowNullableBoolean: false, allowNullableEnum: false, allowNullableNumber: false, allowNullable...

Entry point: create — documented under Functions.


📦 Imports

Name Source
ParserServicesWithTypeInformation @typescript-eslint/utils
TSESTree @typescript-eslint/utils
ReportSuggestionArray @typescript-eslint/utils/ts-eslint
AST_NODE_TYPES @typescript-eslint/utils
ASTUtils @typescript-eslint/utils
createRule ../util
getConstrainedTypeAtLocation ../util
getParserServices ../util
getWrappingFixer ../util
isArrayMethodCallWithPredicate ../util
isParenlessArrowFunction ../util
isTypeArrayTypeOrUnionOfArrayTypes ../util
nullThrows ../util
findTruthinessAssertedArgument ../util/assertionFunctionUtils

Functions

create(context: any, [options]: any): { CallExpression: (node: TSESTree.CallExpression) => void; …

Parameters:

  • context any
  • [options] any

Returns: { CallExpression: (node: TSESTree.CallExpression) => void; ConditionalExpression: (node: any) => void; DoWhileStatement: (node: any) => void; ForStatement: (node: any) => void; IfStatement: (node: any) => void; 'LogicalExpression[operator!="??"]': (node: TSESTree.LogicalExpression, isCondition?: boolean) => void; 'UnaryExpression[operator="!"]': (node: TSESTree.UnaryExpression) => void; WhileStatement: (node: any) => void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • services.program.getCompilerOptions
  • tsutils.isStrictCompilerOptionEnabled
  • context.report
  • traverseNode
  • findTruthinessAssertedArgument (from ../util/assertionFunctionUtils)
  • isArrayMethodCallWithPredicate (from ../util)
  • node.arguments.at
  • checkArrayMethodCallPredicate
  • ASTUtils.isFunction
  • services .getTypeAtLocation(predicateNode) .getCallSignatures() .map
  • signature.getReturnType
  • tsutils.isTypeParameter
  • checker.getBaseConstraintOfType
  • returnTypes.flatMap
  • tsutils.unionConstituents
  • inspectVariantTypes
  • determineReportType
  • suggestions.push
  • getSuggestionsForConditionError
  • isParenlessArrowFunction (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • context.sourceCode.getTokenAfter
  • traversedNodes.has
  • traversedNodes.add
  • traverseLogicalExpression
  • checkNode
  • wantedTypes.every
  • types.has
  • is
  • getWrappingFixer (from ../util)
  • isLogicalNegationExpression
  • isArrayLengthExpression
  • getConstrainedTypeAtLocation (from ../util)
  • types.some
  • tsutils.isTypeFlagSet
  • variantTypes.add
  • types.filter
  • tsutils.isTrueLiteralType
  • strings.every
  • type.isStringLiteral
  • numbers.every
  • type.isNumberLiteral

Internal Comments:

/**
     * Inspects condition of a test expression. (`if`, `while`, `for`, etc.)
     */
/**
     * Inspects the argument of a unary logical expression (`!`).
     */
/**
     * Inspects the arguments of a logical expression (`&&`, `||`).
     *
     * If the logical expression is a descendant of a test expression,
     * the `isCondition` flag should be set to true.
     * Otherwise, if the logical expression is there on it's own,
     * it's used for control flow and is not a condition itself.
     */
// left argument is always treated as a condition (x3)
// if the logical expression is used for control flow, (x3)
// then its right argument is used for its side effects only (x3)
/**
     * Dedicated function to check array method predicate calls. Reports predicate
     * arguments that don't return a boolean value.
     */
// custom message for accidental `async` function expressions
/**
     * Inspects any node.
     *
     * If it's a logical expression then it recursively traverses its arguments.
     * If it's any other kind of node then it's type is finally checked against the rule,
     * unless `isCondition` flag is set to false, in which case
     * it's assumed to be used for side effects only and is skipped.
     */
// prevent checking the same node multiple times
// for logical operator, we check its operands
// skip if node is not a condition
// boolean
// boolean is always ok
// never
// never is always okay
// nullish
// condition is always false
// Known edge case: boolean `true` and nullish values are always valid boolean expressions
// nullable boolean
// Known edge case: truthy primitives and nullish values are always valid boolean expressions
// string
// nullable string
// number
// nullable number
// object
// nullable object
// nullable enum
// mixed enums (x2)
// any
// if (!nullableBoolean)
// if (nullableBoolean)
// if (!nullableNumber)
// if (nullableNumber)
// if (!nullableObject)
// if (nullableObject)
// if (!nullableString)
// if (nullableString)
// if (!array.length)
// if (array.length)
// if (!number)
// TODO: we have to compare to 0n if the type is bigint (x2)
// TODO: don't suggest this for bigint because it can't be NaN (x2)
// if (number)
// if (!string)
// if (string)
/**
     * This function does the actual type check on a node.
     * It analyzes the type of a node and checks if it is allowed in a boolean context.
     */
/** The types we care about */
/**
     * Check union variants for the types we care about
     */
// If incoming type is either "true" or "false", there will be one type
// object with intrinsicName set accordingly
// If incoming type is boolean, there will be two type objects with
// intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()

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

    const isStrictNullChecks = tsutils.isStrictCompilerOptionEnabled(
      compilerOptions,
      'strictNullChecks',
    );

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

    const traversedNodes = new Set<TSESTree.Node>();

    return {
      CallExpression: traverseCallExpression,
      ConditionalExpression: traverseTestExpression,
      DoWhileStatement: traverseTestExpression,
      ForStatement: traverseTestExpression,
      IfStatement: traverseTestExpression,
      'LogicalExpression[operator!="??"]': traverseLogicalExpression,
      'UnaryExpression[operator="!"]': traverseUnaryLogicalExpression,
      WhileStatement: traverseTestExpression,
    };

    type TestExpression =
      | TSESTree.ConditionalExpression
      | TSESTree.DoWhileStatement
      | TSESTree.ForStatement
      | TSESTree.IfStatement
      | TSESTree.WhileStatement;

    /**
     * Inspects condition of a test expression. (`if`, `while`, `for`, etc.)
     */
    function traverseTestExpression(node: TestExpression): void {
      if (node.test == null) {
        return;
      }
      traverseNode(node.test, true);
    }

    /**
     * Inspects the argument of a unary logical expression (`!`).
     */
    function traverseUnaryLogicalExpression(
      node: TSESTree.UnaryExpression,
    ): void {
      traverseNode(node.argument, true);
    }

    /**
     * Inspects the arguments of a logical expression (`&&`, `||`).
     *
     * If the logical expression is a descendant of a test expression,
     * the `isCondition` flag should be set to true.
     * Otherwise, if the logical expression is there on it's own,
     * it's used for control flow and is not a condition itself.
     */
    function traverseLogicalExpression(
      node: TSESTree.LogicalExpression,
      isCondition = false,
    ): void {
      // left argument is always treated as a condition
      traverseNode(node.left, true);
      // if the logical expression is used for control flow,
      // then its right argument is used for its side effects only
      traverseNode(node.right, isCondition);
    }

    function traverseCallExpression(node: TSESTree.CallExpression): void {
      const assertedArgument = findTruthinessAssertedArgument(services, node);
      if (assertedArgument != null) {
        traverseNode(assertedArgument, true);
      }
      if (isArrayMethodCallWithPredicate(context, services, node)) {
        const predicate = node.arguments.at(0);

        if (predicate) {
          checkArrayMethodCallPredicate(predicate);
        }
      }
    }

    /**
     * Dedicated function to check array method predicate calls. Reports predicate
     * arguments that don't return a boolean value.
     */
    function checkArrayMethodCallPredicate(
      predicateNode: TSESTree.CallExpressionArgument,
    ): void {
      const isFunctionExpression = ASTUtils.isFunction(predicateNode);

      // custom message for accidental `async` function expressions
      if (isFunctionExpression && predicateNode.async) {
        return context.report({
          node: predicateNode,
          messageId: 'predicateCannotBeAsync',
        });
      }

      const returnTypes = services
        .getTypeAtLocation(predicateNode)
        .getCallSignatures()
        .map(signature => {
          const type = signature.getReturnType();

          if (tsutils.isTypeParameter(type)) {
            return checker.getBaseConstraintOfType(type) ?? type;
          }

          return type;
        });
      const flattenTypes = [
        ...new Set(
          returnTypes.flatMap(type => tsutils.unionConstituents(type)),
        ),
      ];
      const types = inspectVariantTypes(flattenTypes);
      const reportType = determineReportType(types);

      if (reportType == null) {
        return;
      }

      const suggestions: ReportSuggestionArray<MessageId> = [];
      if (
        isFunctionExpression &&
        predicateNode.body.type !== AST_NODE_TYPES.BlockStatement
      ) {
        suggestions.push(
          ...getSuggestionsForConditionError(predicateNode.body, reportType),
        );
      }

      if (isFunctionExpression && !predicateNode.returnType) {
        suggestions.push({
          messageId: 'explicitBooleanReturnType',
          fix: fixer => {
            if (
              predicateNode.type === AST_NODE_TYPES.ArrowFunctionExpression &&
              isParenlessArrowFunction(predicateNode, context.sourceCode)
            ) {
              return [
                fixer.insertTextBefore(predicateNode.params[0], '('),
                fixer.insertTextAfter(predicateNode.params[0], '): boolean'),
              ];
            }

            if (predicateNode.params.length === 0) {
              const closingBracket = nullThrows(
                context.sourceCode.getFirstToken(
                  predicateNode,
                  token => token.value === ')',
                ),
                'function expression has to have a closing parenthesis.',
              );

              return fixer.insertTextAfter(closingBracket, ': boolean');
            }

            const lastClosingParenthesis = nullThrows(
              context.sourceCode.getTokenAfter(
                predicateNode.params[predicateNode.params.length - 1],
                token => token.value === ')',
              ),
              'function expression has to have a closing parenthesis.',
            );

            return fixer.insertTextAfter(lastClosingParenthesis, ': boolean');
          },
        });
      }

      return context.report({
        node: predicateNode,
        messageId: reportType,
        data: {
          context: 'array predicate return type',
        },
        suggest: suggestions,
      });
    }

    /**
     * Inspects any node.
     *
     * If it's a logical expression then it recursively traverses its arguments.
     * If it's any other kind of node then it's type is finally checked against the rule,
     * unless `isCondition` flag is set to false, in which case
     * it's assumed to be used for side effects only and is skipped.
     */
    function traverseNode(
      node: TSESTree.Expression,
      isCondition: boolean,
    ): void {
      // prevent checking the same node multiple times
      if (traversedNodes.has(node)) {
        return;
      }
      traversedNodes.add(node);

      // for logical operator, we check its operands
      if (
        node.type === AST_NODE_TYPES.LogicalExpression &&
        node.operator !== '??'
      ) {
        traverseLogicalExpression(node, isCondition);
        return;
      }

      // skip if node is not a condition
      if (!isCondition) {
        return;
      }

      checkNode(node);
    }

    function determineReportType(
      types: Set<VariantType>,
    ): ConditionErrorMessageId | undefined {
      const is = (...wantedTypes: readonly VariantType[]): boolean =>
        types.size === wantedTypes.length &&
        wantedTypes.every(type => types.has(type));

      // boolean
      if (is('boolean') || is('truthy boolean')) {
        // boolean is always ok
        return undefined;
      }

      // never
      if (is('never')) {
        // never is always okay
        return undefined;
      }

      // nullish
      if (is('nullish')) {
        // condition is always false
        return 'conditionErrorNullish';
      }

      // Known edge case: boolean `true` and nullish values are always valid boolean expressions
      if (is('nullish', 'truthy boolean')) {
        return;
      }

      // nullable boolean
      if (is('nullish', 'boolean')) {
        return !options.allowNullableBoolean
          ? 'conditionErrorNullableBoolean'
          : undefined;
      }

      // Known edge case: truthy primitives and nullish values are always valid boolean expressions
      if (
        (options.allowNumber && is('nullish', 'truthy number')) ||
        (options.allowString && is('nullish', 'truthy string'))
      ) {
        return;
      }

      // string
      if (is('string') || is('truthy string')) {
        return !options.allowString ? 'conditionErrorString' : undefined;
      }

      // nullable string
      if (is('nullish', 'string')) {
        return !options.allowNullableString
          ? 'conditionErrorNullableString'
          : undefined;
      }

      // number
      if (is('number') || is('truthy number')) {
        return !options.allowNumber ? 'conditionErrorNumber' : undefined;
      }

      // nullable number
      if (is('nullish', 'number')) {
        return !options.allowNullableNumber
          ? 'conditionErrorNullableNumber'
          : undefined;
      }

      // object
      if (is('object')) {
        return 'conditionErrorObject';
      }

      // nullable object
      if (is('nullish', 'object')) {
        return !options.allowNullableObject
          ? 'conditionErrorNullableObject'
          : undefined;
      }

      // nullable enum
      if (
        is('nullish', 'number', 'enum') ||
        is('nullish', 'string', 'enum') ||
        is('nullish', 'truthy number', 'enum') ||
        is('nullish', 'truthy string', 'enum') ||
        // mixed enums
        is('nullish', 'truthy number', 'truthy string', 'enum') ||
        is('nullish', 'truthy number', 'string', 'enum') ||
        is('nullish', 'truthy string', 'number', 'enum') ||
        is('nullish', 'number', 'string', 'enum')
      ) {
        return !options.allowNullableEnum
          ? 'conditionErrorNullableEnum'
          : undefined;
      }

      // any
      if (is('any')) {
        return !options.allowAny ? 'conditionErrorAny' : undefined;
      }

      return 'conditionErrorOther';
    }

    function getSuggestionsForConditionError(
      node: TSESTree.Expression,
      conditionError: ConditionErrorMessageId,
    ): ReportSuggestionArray<MessageId> {
      switch (conditionError) {
        case 'conditionErrorAny':
          return [
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];
        case 'conditionErrorNullableBoolean':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableBoolean)
            return [
              {
                messageId: 'conditionFixDefaultFalse',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} ?? false`,
                }),
              },
              {
                messageId: 'conditionFixCompareFalse',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} === false`,
                }),
              },
            ];
          }
          // if (nullableBoolean)
          return [
            {
              messageId: 'conditionFixDefaultFalse',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} ?? false`,
              }),
            },
            {
              messageId: 'conditionFixCompareTrue',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} === true`,
              }),
            },
          ];

        case 'conditionErrorNullableEnum':
          if (isLogicalNegationExpression(node.parent)) {
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
            ];
          }
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
          ];

        case 'conditionErrorNullableNumber':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableNumber)
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
              {
                messageId: 'conditionFixDefaultZero',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} ?? 0`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (nullableNumber)
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
            {
              messageId: 'conditionFixDefaultZero',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} ?? 0`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorNullableObject':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableObject)
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
            ];
          }
          // if (nullableObject)
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
          ];

        case 'conditionErrorNullableString':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableString)
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
              {
                messageId: 'conditionFixDefaultEmptyString',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} ?? ""`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (nullableString)
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
            {
              messageId: 'conditionFixDefaultEmptyString',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} ?? ""`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorNumber':
          if (isArrayLengthExpression(node, checker, services)) {
            if (isLogicalNegationExpression(node.parent)) {
              // if (!array.length)
              return [
                {
                  messageId: 'conditionFixCompareArrayLengthZero',
                  fix: getWrappingFixer({
                    node: node.parent,
                    innerNode: node,
                    sourceCode: context.sourceCode,
                    wrap: code => `${code} === 0`,
                  }),
                },
              ];
            }
            // if (array.length)
            return [
              {
                messageId: 'conditionFixCompareArrayLengthNonzero',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} > 0`,
                }),
              },
            ];
          }
          if (isLogicalNegationExpression(node.parent)) {
            // if (!number)
            return [
              {
                messageId: 'conditionFixCompareZero',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  // TODO: we have to compare to 0n if the type is bigint
                  wrap: code => `${code} === 0`,
                }),
              },
              {
                // TODO: don't suggest this for bigint because it can't be NaN
                messageId: 'conditionFixCompareNaN',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `Number.isNaN(${code})`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (number)
          return [
            {
              messageId: 'conditionFixCompareZero',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} !== 0`,
              }),
            },
            {
              messageId: 'conditionFixCompareNaN',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `!Number.isNaN(${code})`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorString':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!string)
            return [
              {
                messageId: 'conditionFixCompareStringLength',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code}.length === 0`,
                }),
              },
              {
                messageId: 'conditionFixCompareEmptyString',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} === ""`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (string)
          return [
            {
              messageId: 'conditionFixCompareStringLength',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code}.length > 0`,
              }),
            },
            {
              messageId: 'conditionFixCompareEmptyString',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} !== ""`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorObject':
        case 'conditionErrorNullish':
        case 'conditionErrorOther':
          return [];
        default:
          conditionError satisfies never;
          throw new Error('Unreachable');
      }
    }

    /**
     * This function does the actual type check on a node.
     * It analyzes the type of a node and checks if it is allowed in a boolean context.
     */
    function checkNode(node: TSESTree.Expression): void {
      const type = getConstrainedTypeAtLocation(services, node);
      const types = inspectVariantTypes(tsutils.unionConstituents(type));
      const reportType = determineReportType(types);

      if (reportType != null) {
        context.report({
          node,
          messageId: reportType,
          data: {
            context: 'conditional',
          },
          suggest: getSuggestionsForConditionError(node, reportType),
        });
      }
    }

    /** The types we care about */
    type VariantType =
      | 'any'
      | 'boolean'
      | 'enum'
      | 'never'
      | 'nullish'
      | 'number'
      | 'object'
      | 'string'
      | 'truthy boolean'
      | 'truthy number'
      | 'truthy string';

    /**
     * Check union variants for the types we care about
     */
    function inspectVariantTypes(types: ts.Type[]): Set<VariantType> {
      const variantTypes = new Set<VariantType>();

      if (
        types.some(type =>
          tsutils.isTypeFlagSet(
            type,
            ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike,
          ),
        )
      ) {
        variantTypes.add('nullish');
      }
      const booleans = types.filter(type =>
        tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike),
      );

      // If incoming type is either "true" or "false", there will be one type
      // object with intrinsicName set accordingly
      // If incoming type is boolean, there will be two type objects with
      // intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()
      if (booleans.length === 1) {
        variantTypes.add(
          tsutils.isTrueLiteralType(booleans[0]) ? 'truthy boolean' : 'boolean',
        );
      } else if (booleans.length === 2) {
        variantTypes.add('boolean');
      }

      const strings = types.filter(type =>
        tsutils.isTypeFlagSet(type, ts.TypeFlags.StringLike),
      );

      if (strings.length) {
        if (
          strings.every(type => type.isStringLiteral() && type.value !== '')
        ) {
          variantTypes.add('truthy string');
        } else {
          variantTypes.add('string');
        }
      }

      const numbers = types.filter(type =>
        tsutils.isTypeFlagSet(
          type,
          ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
        ),
      );

      if (numbers.length) {
        if (numbers.every(type => type.isNumberLiteral() && type.value !== 0)) {
          variantTypes.add('truthy number');
        } else {
          variantTypes.add('number');
        }
      }

      if (
        types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike))
      ) {
        variantTypes.add('enum');
      }

      if (
        types.some(
          type =>
            !tsutils.isTypeFlagSet(
              type,
              ts.TypeFlags.Null |
                ts.TypeFlags.Undefined |
                ts.TypeFlags.VoidLike |
                ts.TypeFlags.BooleanLike |
                ts.TypeFlags.StringLike |
                ts.TypeFlags.NumberLike |
                ts.TypeFlags.BigIntLike |
                ts.TypeFlags.TypeParameter |
                ts.TypeFlags.Any |
                ts.TypeFlags.Unknown |
                ts.TypeFlags.Never,
            ),
        )
      ) {
        variantTypes.add(types.some(isBrandedBoolean) ? 'boolean' : 'object');
      }

      if (
        types.some(type =>
          tsutils.isTypeFlagSet(
            type,
            ts.TypeFlags.TypeParameter |
              ts.TypeFlags.Any |
              ts.TypeFlags.Unknown,
          ),
        )
      ) {
        variantTypes.add('any');
      }

      if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Never))) {
        variantTypes.add('never');
      }

      return variantTypes;
    }
  }

isLogicalNegationExpression(node: TSESTree.Node): node is TSESTree.UnaryExpression

Parameters:

  • node TSESTree.Node

Returns: node is TSESTree.UnaryExpression

Code
function isLogicalNegationExpression(
  node: TSESTree.Node,
): node is TSESTree.UnaryExpression {
  return node.type === AST_NODE_TYPES.UnaryExpression && node.operator === '!';
}

isArrayLengthExpression(node: TSESTree.Node, typeChecker: ts.TypeChecker, services: ParserServicesWithTypeInformation): node is TSESTree.MemberExpressionNonComputedName

Parameters:

  • node TSESTree.Node
  • typeChecker ts.TypeChecker
  • services ParserServicesWithTypeInformation

Returns: node is TSESTree.MemberExpressionNonComputedName

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • isTypeArrayTypeOrUnionOfArrayTypes (from ../util)
Code
function isArrayLengthExpression(
  node: TSESTree.Node,
  typeChecker: ts.TypeChecker,
  services: ParserServicesWithTypeInformation,
): node is TSESTree.MemberExpressionNonComputedName {
  if (node.type !== AST_NODE_TYPES.MemberExpression) {
    return false;
  }
  if (node.computed) {
    return false;
  }
  if (node.property.name !== 'length') {
    return false;
  }
  const objectType = getConstrainedTypeAtLocation(services, node.object);
  return isTypeArrayTypeOrUnionOfArrayTypes(objectType, typeChecker);
}

isBrandedBoolean(type: ts.Type): boolean

Verify is the type is a branded boolean (e.g. type Foo = boolean & { __brand: 'Foo' })

Parameters:

  • type any: The type checked
Raw JSDoc
/**
 * Verify is the type is a branded boolean (e.g. `type Foo = boolean & { __brand: 'Foo' }`)
 *
 * @param type The type checked
 */

Calls:

  • type.isIntersection
  • type.types.some
  • isBooleanType
Code
function isBrandedBoolean(type: ts.Type): boolean {
  return (
    type.isIntersection() &&
    type.types.some(childType => isBooleanType(childType))
  );
}

isBooleanType(expressionType: ts.Type): boolean

Parameters:

  • expressionType ts.Type

Returns: boolean

Calls:

  • tsutils.isTypeFlagSet
Code
function isBooleanType(expressionType: ts.Type): boolean {
  return tsutils.isTypeFlagSet(
    expressionType,
    ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral,
  );
}

Internal helpers

Declared inside another function in this file.

traverseTestExpression(node: TestExpression): void

Inspects condition of a test expression. (if, while, for, etc.)

Raw JSDoc
/**
     * Inspects condition of a test expression. (`if`, `while`, `for`, etc.)
     */

Calls:

  • traverseNode
Code
function traverseTestExpression(node: TestExpression): void {
      if (node.test == null) {
        return;
      }
      traverseNode(node.test, true);
    }

traverseUnaryLogicalExpression(node: TSESTree.UnaryExpression): void

Inspects the argument of a unary logical expression (!).

Raw JSDoc
/**
     * Inspects the argument of a unary logical expression (`!`).
     */

Calls:

  • traverseNode
Code
function traverseUnaryLogicalExpression(
      node: TSESTree.UnaryExpression,
    ): void {
      traverseNode(node.argument, true);
    }

traverseLogicalExpression(node: TSESTree.LogicalExpression, isCondition: boolean): void

Inspects the arguments of a logical expression (&&, ||).

If the logical expression is a descendant of a test expression, the isCondition flag should be set to true. Otherwise, if the logical expression is there on it's own, it's used for control flow and is not a condition itself.

Raw JSDoc
/**
     * Inspects the arguments of a logical expression (`&&`, `||`).
     *
     * If the logical expression is a descendant of a test expression,
     * the `isCondition` flag should be set to true.
     * Otherwise, if the logical expression is there on it's own,
     * it's used for control flow and is not a condition itself.
     */

Calls:

  • traverseNode

Internal Comments:

// left argument is always treated as a condition (x3)
// if the logical expression is used for control flow, (x3)
// then its right argument is used for its side effects only (x3)

Code
function traverseLogicalExpression(
      node: TSESTree.LogicalExpression,
      isCondition = false,
    ): void {
      // left argument is always treated as a condition
      traverseNode(node.left, true);
      // if the logical expression is used for control flow,
      // then its right argument is used for its side effects only
      traverseNode(node.right, isCondition);
    }

traverseCallExpression(node: TSESTree.CallExpression): void

Parameters:

  • node TSESTree.CallExpression

Returns: void

Calls:

  • findTruthinessAssertedArgument (from ../util/assertionFunctionUtils)
  • traverseNode
  • isArrayMethodCallWithPredicate (from ../util)
  • node.arguments.at
  • checkArrayMethodCallPredicate
Code
function traverseCallExpression(node: TSESTree.CallExpression): void {
      const assertedArgument = findTruthinessAssertedArgument(services, node);
      if (assertedArgument != null) {
        traverseNode(assertedArgument, true);
      }
      if (isArrayMethodCallWithPredicate(context, services, node)) {
        const predicate = node.arguments.at(0);

        if (predicate) {
          checkArrayMethodCallPredicate(predicate);
        }
      }
    }

checkArrayMethodCallPredicate(predicateNode: TSESTree.CallExpressionArgument): void

Dedicated function to check array method predicate calls. Reports predicate arguments that don't return a boolean value.

Raw JSDoc
/**
     * Dedicated function to check array method predicate calls. Reports predicate
     * arguments that don't return a boolean value.
     */

Calls:

  • ASTUtils.isFunction
  • context.report
  • services .getTypeAtLocation(predicateNode) .getCallSignatures() .map
  • signature.getReturnType
  • tsutils.isTypeParameter
  • checker.getBaseConstraintOfType
  • returnTypes.flatMap
  • tsutils.unionConstituents
  • inspectVariantTypes
  • determineReportType
  • suggestions.push
  • getSuggestionsForConditionError
  • isParenlessArrowFunction (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • context.sourceCode.getTokenAfter

Internal Comments:

// custom message for accidental `async` function expressions

Code
function checkArrayMethodCallPredicate(
      predicateNode: TSESTree.CallExpressionArgument,
    ): void {
      const isFunctionExpression = ASTUtils.isFunction(predicateNode);

      // custom message for accidental `async` function expressions
      if (isFunctionExpression && predicateNode.async) {
        return context.report({
          node: predicateNode,
          messageId: 'predicateCannotBeAsync',
        });
      }

      const returnTypes = services
        .getTypeAtLocation(predicateNode)
        .getCallSignatures()
        .map(signature => {
          const type = signature.getReturnType();

          if (tsutils.isTypeParameter(type)) {
            return checker.getBaseConstraintOfType(type) ?? type;
          }

          return type;
        });
      const flattenTypes = [
        ...new Set(
          returnTypes.flatMap(type => tsutils.unionConstituents(type)),
        ),
      ];
      const types = inspectVariantTypes(flattenTypes);
      const reportType = determineReportType(types);

      if (reportType == null) {
        return;
      }

      const suggestions: ReportSuggestionArray<MessageId> = [];
      if (
        isFunctionExpression &&
        predicateNode.body.type !== AST_NODE_TYPES.BlockStatement
      ) {
        suggestions.push(
          ...getSuggestionsForConditionError(predicateNode.body, reportType),
        );
      }

      if (isFunctionExpression && !predicateNode.returnType) {
        suggestions.push({
          messageId: 'explicitBooleanReturnType',
          fix: fixer => {
            if (
              predicateNode.type === AST_NODE_TYPES.ArrowFunctionExpression &&
              isParenlessArrowFunction(predicateNode, context.sourceCode)
            ) {
              return [
                fixer.insertTextBefore(predicateNode.params[0], '('),
                fixer.insertTextAfter(predicateNode.params[0], '): boolean'),
              ];
            }

            if (predicateNode.params.length === 0) {
              const closingBracket = nullThrows(
                context.sourceCode.getFirstToken(
                  predicateNode,
                  token => token.value === ')',
                ),
                'function expression has to have a closing parenthesis.',
              );

              return fixer.insertTextAfter(closingBracket, ': boolean');
            }

            const lastClosingParenthesis = nullThrows(
              context.sourceCode.getTokenAfter(
                predicateNode.params[predicateNode.params.length - 1],
                token => token.value === ')',
              ),
              'function expression has to have a closing parenthesis.',
            );

            return fixer.insertTextAfter(lastClosingParenthesis, ': boolean');
          },
        });
      }

      return context.report({
        node: predicateNode,
        messageId: reportType,
        data: {
          context: 'array predicate return type',
        },
        suggest: suggestions,
      });
    }

fix(fixer: any): any

Parameters:

  • fixer any

Returns: any

Calls:

  • isParenlessArrowFunction (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • nullThrows (from ../util)
  • context.sourceCode.getFirstToken
  • context.sourceCode.getTokenAfter
Code
fixer => {
            if (
              predicateNode.type === AST_NODE_TYPES.ArrowFunctionExpression &&
              isParenlessArrowFunction(predicateNode, context.sourceCode)
            ) {
              return [
                fixer.insertTextBefore(predicateNode.params[0], '('),
                fixer.insertTextAfter(predicateNode.params[0], '): boolean'),
              ];
            }

            if (predicateNode.params.length === 0) {
              const closingBracket = nullThrows(
                context.sourceCode.getFirstToken(
                  predicateNode,
                  token => token.value === ')',
                ),
                'function expression has to have a closing parenthesis.',
              );

              return fixer.insertTextAfter(closingBracket, ': boolean');
            }

            const lastClosingParenthesis = nullThrows(
              context.sourceCode.getTokenAfter(
                predicateNode.params[predicateNode.params.length - 1],
                token => token.value === ')',
              ),
              'function expression has to have a closing parenthesis.',
            );

            return fixer.insertTextAfter(lastClosingParenthesis, ': boolean');
          }

traverseNode(node: TSESTree.Expression, isCondition: boolean): void

Inspects any node.

If it's a logical expression then it recursively traverses its arguments. If it's any other kind of node then it's type is finally checked against the rule, unless isCondition flag is set to false, in which case it's assumed to be used for side effects only and is skipped.

Raw JSDoc
/**
     * Inspects any node.
     *
     * If it's a logical expression then it recursively traverses its arguments.
     * If it's any other kind of node then it's type is finally checked against the rule,
     * unless `isCondition` flag is set to false, in which case
     * it's assumed to be used for side effects only and is skipped.
     */

Calls:

  • traversedNodes.has
  • traversedNodes.add
  • traverseLogicalExpression
  • checkNode

Internal Comments:

// prevent checking the same node multiple times
// for logical operator, we check its operands
// skip if node is not a condition

Code
function traverseNode(
      node: TSESTree.Expression,
      isCondition: boolean,
    ): void {
      // prevent checking the same node multiple times
      if (traversedNodes.has(node)) {
        return;
      }
      traversedNodes.add(node);

      // for logical operator, we check its operands
      if (
        node.type === AST_NODE_TYPES.LogicalExpression &&
        node.operator !== '??'
      ) {
        traverseLogicalExpression(node, isCondition);
        return;
      }

      // skip if node is not a condition
      if (!isCondition) {
        return;
      }

      checkNode(node);
    }

determineReportType(types: Set<VariantType>): ConditionErrorMessageId | undefined

Parameters:

  • types Set<VariantType>

Returns: ConditionErrorMessageId | undefined

Calls:

  • wantedTypes.every
  • types.has
  • is

Internal Comments:

// boolean
// boolean is always ok
// never
// never is always okay
// nullish
// condition is always false
// Known edge case: boolean `true` and nullish values are always valid boolean expressions
// nullable boolean
// Known edge case: truthy primitives and nullish values are always valid boolean expressions
// string
// nullable string
// number
// nullable number
// object
// nullable object
// nullable enum
// mixed enums (x2)
// any

Code
function determineReportType(
      types: Set<VariantType>,
    ): ConditionErrorMessageId | undefined {
      const is = (...wantedTypes: readonly VariantType[]): boolean =>
        types.size === wantedTypes.length &&
        wantedTypes.every(type => types.has(type));

      // boolean
      if (is('boolean') || is('truthy boolean')) {
        // boolean is always ok
        return undefined;
      }

      // never
      if (is('never')) {
        // never is always okay
        return undefined;
      }

      // nullish
      if (is('nullish')) {
        // condition is always false
        return 'conditionErrorNullish';
      }

      // Known edge case: boolean `true` and nullish values are always valid boolean expressions
      if (is('nullish', 'truthy boolean')) {
        return;
      }

      // nullable boolean
      if (is('nullish', 'boolean')) {
        return !options.allowNullableBoolean
          ? 'conditionErrorNullableBoolean'
          : undefined;
      }

      // Known edge case: truthy primitives and nullish values are always valid boolean expressions
      if (
        (options.allowNumber && is('nullish', 'truthy number')) ||
        (options.allowString && is('nullish', 'truthy string'))
      ) {
        return;
      }

      // string
      if (is('string') || is('truthy string')) {
        return !options.allowString ? 'conditionErrorString' : undefined;
      }

      // nullable string
      if (is('nullish', 'string')) {
        return !options.allowNullableString
          ? 'conditionErrorNullableString'
          : undefined;
      }

      // number
      if (is('number') || is('truthy number')) {
        return !options.allowNumber ? 'conditionErrorNumber' : undefined;
      }

      // nullable number
      if (is('nullish', 'number')) {
        return !options.allowNullableNumber
          ? 'conditionErrorNullableNumber'
          : undefined;
      }

      // object
      if (is('object')) {
        return 'conditionErrorObject';
      }

      // nullable object
      if (is('nullish', 'object')) {
        return !options.allowNullableObject
          ? 'conditionErrorNullableObject'
          : undefined;
      }

      // nullable enum
      if (
        is('nullish', 'number', 'enum') ||
        is('nullish', 'string', 'enum') ||
        is('nullish', 'truthy number', 'enum') ||
        is('nullish', 'truthy string', 'enum') ||
        // mixed enums
        is('nullish', 'truthy number', 'truthy string', 'enum') ||
        is('nullish', 'truthy number', 'string', 'enum') ||
        is('nullish', 'truthy string', 'number', 'enum') ||
        is('nullish', 'number', 'string', 'enum')
      ) {
        return !options.allowNullableEnum
          ? 'conditionErrorNullableEnum'
          : undefined;
      }

      // any
      if (is('any')) {
        return !options.allowAny ? 'conditionErrorAny' : undefined;
      }

      return 'conditionErrorOther';
    }

is(wantedTypes: readonly VariantType[]): boolean

Parameters:

  • wantedTypes readonly VariantType[]

Returns: boolean

Code
(...wantedTypes: readonly VariantType[]): boolean =>
        types.size === wantedTypes.length &&
        wantedTypes.every(type => types.has(type))

getSuggestionsForConditionError(node: TSESTree.Expression, conditionError: ConditionErrorMessageId): ReportSuggestionArray<MessageId>

Parameters:

  • node TSESTree.Expression
  • conditionError ConditionErrorMessageId

Returns: ReportSuggestionArray<MessageId>

Calls:

  • getWrappingFixer (from ../util)
  • isLogicalNegationExpression
  • isArrayLengthExpression

Internal Comments:

// if (!nullableBoolean)
// if (nullableBoolean)
// if (!nullableNumber)
// if (nullableNumber)
// if (!nullableObject)
// if (nullableObject)
// if (!nullableString)
// if (nullableString)
// if (!array.length)
// if (array.length)
// if (!number)
// TODO: we have to compare to 0n if the type is bigint (x2)
// TODO: don't suggest this for bigint because it can't be NaN (x2)
// if (number)
// if (!string)
// if (string)

Code
function getSuggestionsForConditionError(
      node: TSESTree.Expression,
      conditionError: ConditionErrorMessageId,
    ): ReportSuggestionArray<MessageId> {
      switch (conditionError) {
        case 'conditionErrorAny':
          return [
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];
        case 'conditionErrorNullableBoolean':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableBoolean)
            return [
              {
                messageId: 'conditionFixDefaultFalse',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} ?? false`,
                }),
              },
              {
                messageId: 'conditionFixCompareFalse',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} === false`,
                }),
              },
            ];
          }
          // if (nullableBoolean)
          return [
            {
              messageId: 'conditionFixDefaultFalse',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} ?? false`,
              }),
            },
            {
              messageId: 'conditionFixCompareTrue',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} === true`,
              }),
            },
          ];

        case 'conditionErrorNullableEnum':
          if (isLogicalNegationExpression(node.parent)) {
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
            ];
          }
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
          ];

        case 'conditionErrorNullableNumber':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableNumber)
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
              {
                messageId: 'conditionFixDefaultZero',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} ?? 0`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (nullableNumber)
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
            {
              messageId: 'conditionFixDefaultZero',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} ?? 0`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorNullableObject':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableObject)
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
            ];
          }
          // if (nullableObject)
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
          ];

        case 'conditionErrorNullableString':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!nullableString)
            return [
              {
                messageId: 'conditionFixCompareNullish',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} == null`,
                }),
              },
              {
                messageId: 'conditionFixDefaultEmptyString',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} ?? ""`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (nullableString)
          return [
            {
              messageId: 'conditionFixCompareNullish',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} != null`,
              }),
            },
            {
              messageId: 'conditionFixDefaultEmptyString',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} ?? ""`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorNumber':
          if (isArrayLengthExpression(node, checker, services)) {
            if (isLogicalNegationExpression(node.parent)) {
              // if (!array.length)
              return [
                {
                  messageId: 'conditionFixCompareArrayLengthZero',
                  fix: getWrappingFixer({
                    node: node.parent,
                    innerNode: node,
                    sourceCode: context.sourceCode,
                    wrap: code => `${code} === 0`,
                  }),
                },
              ];
            }
            // if (array.length)
            return [
              {
                messageId: 'conditionFixCompareArrayLengthNonzero',
                fix: getWrappingFixer({
                  node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} > 0`,
                }),
              },
            ];
          }
          if (isLogicalNegationExpression(node.parent)) {
            // if (!number)
            return [
              {
                messageId: 'conditionFixCompareZero',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  // TODO: we have to compare to 0n if the type is bigint
                  wrap: code => `${code} === 0`,
                }),
              },
              {
                // TODO: don't suggest this for bigint because it can't be NaN
                messageId: 'conditionFixCompareNaN',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `Number.isNaN(${code})`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (number)
          return [
            {
              messageId: 'conditionFixCompareZero',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} !== 0`,
              }),
            },
            {
              messageId: 'conditionFixCompareNaN',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `!Number.isNaN(${code})`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorString':
          if (isLogicalNegationExpression(node.parent)) {
            // if (!string)
            return [
              {
                messageId: 'conditionFixCompareStringLength',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code}.length === 0`,
                }),
              },
              {
                messageId: 'conditionFixCompareEmptyString',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `${code} === ""`,
                }),
              },
              {
                messageId: 'conditionFixCastBoolean',
                fix: getWrappingFixer({
                  node: node.parent,
                  innerNode: node,
                  sourceCode: context.sourceCode,
                  wrap: code => `!Boolean(${code})`,
                }),
              },
            ];
          }
          // if (string)
          return [
            {
              messageId: 'conditionFixCompareStringLength',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code}.length > 0`,
              }),
            },
            {
              messageId: 'conditionFixCompareEmptyString',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `${code} !== ""`,
              }),
            },
            {
              messageId: 'conditionFixCastBoolean',
              fix: getWrappingFixer({
                node,
                sourceCode: context.sourceCode,
                wrap: code => `Boolean(${code})`,
              }),
            },
          ];

        case 'conditionErrorObject':
        case 'conditionErrorNullish':
        case 'conditionErrorOther':
          return [];
        default:
          conditionError satisfies never;
          throw new Error('Unreachable');
      }
    }

checkNode(node: TSESTree.Expression): void

This function does the actual type check on a node. It analyzes the type of a node and checks if it is allowed in a boolean context.

Raw JSDoc
/**
     * This function does the actual type check on a node.
     * It analyzes the type of a node and checks if it is allowed in a boolean context.
     */

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • inspectVariantTypes
  • tsutils.unionConstituents
  • determineReportType
  • context.report
  • getSuggestionsForConditionError
Code
function checkNode(node: TSESTree.Expression): void {
      const type = getConstrainedTypeAtLocation(services, node);
      const types = inspectVariantTypes(tsutils.unionConstituents(type));
      const reportType = determineReportType(types);

      if (reportType != null) {
        context.report({
          node,
          messageId: reportType,
          data: {
            context: 'conditional',
          },
          suggest: getSuggestionsForConditionError(node, reportType),
        });
      }
    }

inspectVariantTypes(types: ts.Type[]): Set<VariantType>

Check union variants for the types we care about

Raw JSDoc
/**
     * Check union variants for the types we care about
     */

Calls:

  • types.some
  • tsutils.isTypeFlagSet
  • variantTypes.add
  • types.filter
  • tsutils.isTrueLiteralType
  • strings.every
  • type.isStringLiteral
  • numbers.every
  • type.isNumberLiteral

Internal Comments:

// If incoming type is either "true" or "false", there will be one type
// object with intrinsicName set accordingly
// If incoming type is boolean, there will be two type objects with
// intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()

Code
function inspectVariantTypes(types: ts.Type[]): Set<VariantType> {
      const variantTypes = new Set<VariantType>();

      if (
        types.some(type =>
          tsutils.isTypeFlagSet(
            type,
            ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.VoidLike,
          ),
        )
      ) {
        variantTypes.add('nullish');
      }
      const booleans = types.filter(type =>
        tsutils.isTypeFlagSet(type, ts.TypeFlags.BooleanLike),
      );

      // If incoming type is either "true" or "false", there will be one type
      // object with intrinsicName set accordingly
      // If incoming type is boolean, there will be two type objects with
      // intrinsicName set "true" and "false" each because of ts-api-utils.unionConstituents()
      if (booleans.length === 1) {
        variantTypes.add(
          tsutils.isTrueLiteralType(booleans[0]) ? 'truthy boolean' : 'boolean',
        );
      } else if (booleans.length === 2) {
        variantTypes.add('boolean');
      }

      const strings = types.filter(type =>
        tsutils.isTypeFlagSet(type, ts.TypeFlags.StringLike),
      );

      if (strings.length) {
        if (
          strings.every(type => type.isStringLiteral() && type.value !== '')
        ) {
          variantTypes.add('truthy string');
        } else {
          variantTypes.add('string');
        }
      }

      const numbers = types.filter(type =>
        tsutils.isTypeFlagSet(
          type,
          ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
        ),
      );

      if (numbers.length) {
        if (numbers.every(type => type.isNumberLiteral() && type.value !== 0)) {
          variantTypes.add('truthy number');
        } else {
          variantTypes.add('number');
        }
      }

      if (
        types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.EnumLike))
      ) {
        variantTypes.add('enum');
      }

      if (
        types.some(
          type =>
            !tsutils.isTypeFlagSet(
              type,
              ts.TypeFlags.Null |
                ts.TypeFlags.Undefined |
                ts.TypeFlags.VoidLike |
                ts.TypeFlags.BooleanLike |
                ts.TypeFlags.StringLike |
                ts.TypeFlags.NumberLike |
                ts.TypeFlags.BigIntLike |
                ts.TypeFlags.TypeParameter |
                ts.TypeFlags.Any |
                ts.TypeFlags.Unknown |
                ts.TypeFlags.Never,
            ),
        )
      ) {
        variantTypes.add(types.some(isBrandedBoolean) ? 'boolean' : 'object');
      }

      if (
        types.some(type =>
          tsutils.isTypeFlagSet(
            type,
            ts.TypeFlags.TypeParameter |
              ts.TypeFlags.Any |
              ts.TypeFlags.Unknown,
          ),
        )
      ) {
        variantTypes.add('any');
      }

      if (types.some(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Never))) {
        variantTypes.add('never');
      }

      return variantTypes;
    }

Type Aliases

Options

type Options = [
  {
    allowAny?: boolean;
    allowNullableBoolean?: boolean;
    allowNullableEnum?: boolean;
    allowNullableNumber?: boolean;
    allowNullableObject?: boolean;
    allowNullableString?: boolean;
    allowNumber?: boolean;
    allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
    allowString?: boolean;
  },
];

ConditionErrorMessageId

type ConditionErrorMessageId = | 'conditionErrorAny'
  | 'conditionErrorNullableBoolean'
  | 'conditionErrorNullableEnum'
  | 'conditionErrorNullableNumber'
  | 'conditionErrorNullableObject'
  | 'conditionErrorNullableString'
  | 'conditionErrorNullish'
  | 'conditionErrorNumber'
  | 'conditionErrorObject'
  | 'conditionErrorOther'
  | 'conditionErrorString';

MessageId

type MessageId = | 'conditionFixCastBoolean'
  | 'conditionFixCompareArrayLengthNonzero'
  | 'conditionFixCompareArrayLengthZero'
  | 'conditionFixCompareEmptyString'
  | 'conditionFixCompareFalse'
  | 'conditionFixCompareNaN'
  | 'conditionFixCompareNullish'
  | 'conditionFixCompareStringLength'
  | 'conditionFixCompareTrue'
  | 'conditionFixCompareZero'
  | 'conditionFixDefaultEmptyString'
  | 'conditionFixDefaultFalse'
  | 'conditionFixDefaultZero'
  | 'explicitBooleanReturnType'
  | 'noStrictNullCheck'
  | 'predicateCannotBeAsync'
  | ConditionErrorMessageId;

TestExpression

type TestExpression = | TSESTree.ConditionalExpression
      | TSESTree.DoWhileStatement
      | TSESTree.ForStatement
      | TSESTree.IfStatement
      | TSESTree.WhileStatement;

VariantType

/* The types we care about /

type VariantType = | 'any'
      | 'boolean'
      | 'enum'
      | 'never'
      | 'nullish'
      | 'number'
      | 'object'
      | 'string'
      | 'truthy boolean'
      | 'truthy number'
      | 'truthy string';

Generated by Syntax Scribe