Skip to content

⬅️ Back to Table of Contents

📄 no-unnecessary-boolean-literal-compare

📊 Analysis Summary

Metric Count
🔧 Functions 9
📦 Imports 8
📐 Interfaces 3
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/no-unnecessary-boolean-literal-compare.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'no-unnecessary-boolean-literal-compare'
meta.type 'suggestion'
meta.docs.description 'Disallow unnecessary equality comparisons against boolean literals'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.messages.comparingNullableToFalse 'This expression unnecessarily compares a nullable boolean value to false instead of using the ?? operator to provide...
meta.messages.comparingNullableToTrueDirect 'This expression unnecessarily compares a nullable boolean value to true instead of using it directly.'
meta.messages.comparingNullableToTrueNegated 'This expression unnecessarily compares a nullable boolean value to true instead of negating it.'
meta.messages.direct 'This expression unnecessarily compares a boolean value to a boolean instead of using it directly.'
meta.messages.negated 'This expression unnecessarily compares a boolean value to a boolean instead of negating it.'
meta.messages.noStrictNullCheck 'This rule requires the strictNullChecks compiler option to be turned on to function correctly.'
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowComparingNullableBooleansToFalse: { type: 'boolea...
defaultOptions [ { allowComparingNullableBooleansToFalse: true, allowComparingNullableBooleansToTrue: true, allowRuleToRunWithoutStr...

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getConstraintInfo ../util
getParserServices ../util
isConditionalTest ../util
isStrongPrecedenceNode ../util
isWeakPrecedenceParent ../util

Functions

create(context: any, [options]: any): { BinaryExpression(node: any): void; }

Parameters:

  • context any
  • [options] any

Returns: { BinaryExpression(node: any): void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • services.program.getCompilerOptions
  • tsutils.isStrictCompilerOptionEnabled
  • context.report
  • deconstructComparison
  • getConstraintInfo (from ../util)
  • services.getTypeAtLocation
  • isBooleanType
  • isNullableBoolean
  • tsutils.isTypeFlagSet
  • expressionType.isUnion
  • types.filter
  • nonNullishTypes.every
  • getEqualsKind
  • getBooleanComparison
  • nodeIsUnaryNegation
  • booleanXor
  • context.sourceCode.getText
  • isStrongPrecedenceNode (from ../util)
  • isConditionalTest (from ../util)
  • parenthesize
  • isWeakPrecedenceParent (from ../util)
  • fixer.replaceText

Internal Comments:

/**
     * checks if the expressionType is a union that
     *   1) contains at least one nullish type (null or undefined)
     *   2) contains at least once boolean type (true or false or boolean)
     *   3) does not contain any types besides nullish and boolean types
     */
// Whether the truth table of the overall expression being replaced (x2)
// is negated, _ignoring the nullish cases_. (x2)
// we'll build up the replacement text from the compared expression outwards. (x2)
// In maybeNullish === false, nullish values have the same truth table
// as `true`.

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',
      });
    }

    function getBooleanComparison(
      node: TSESTree.BinaryExpression,
    ): BooleanComparisonWithTypeInformation | undefined {
      const comparison = deconstructComparison(node);
      if (!comparison) {
        return undefined;
      }

      const { constraintType, isTypeParameter } = getConstraintInfo(
        checker,
        services.getTypeAtLocation(comparison.expression),
      );

      if (isTypeParameter && constraintType == null) {
        return undefined;
      }

      if (isBooleanType(constraintType)) {
        return {
          ...comparison,
          expressionIsNullableBoolean: false,
        };
      }

      if (isNullableBoolean(constraintType)) {
        return {
          ...comparison,
          expressionIsNullableBoolean: true,
        };
      }

      return undefined;
    }

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

    /**
     * checks if the expressionType is a union that
     *   1) contains at least one nullish type (null or undefined)
     *   2) contains at least once boolean type (true or false or boolean)
     *   3) does not contain any types besides nullish and boolean types
     */
    function isNullableBoolean(expressionType: ts.Type): boolean {
      if (!expressionType.isUnion()) {
        return false;
      }

      const { types } = expressionType;

      const nonNullishTypes = types.filter(
        type =>
          !tsutils.isTypeFlagSet(
            type,
            ts.TypeFlags.Undefined | ts.TypeFlags.Null,
          ),
      );

      const hasNonNullishType = nonNullishTypes.length > 0;
      if (!hasNonNullishType) {
        return false;
      }

      const hasNullableType = nonNullishTypes.length < types.length;
      if (!hasNullableType) {
        return false;
      }

      const allNonNullishTypesAreBoolean = nonNullishTypes.every(isBooleanType);
      if (!allNonNullishTypesAreBoolean) {
        return false;
      }

      return true;
    }

    function deconstructComparison(
      node: TSESTree.BinaryExpression,
    ): BooleanComparison | undefined {
      const comparisonType = getEqualsKind(node.operator);
      if (!comparisonType) {
        return undefined;
      }

      for (const [against, expression] of [
        [node.right, node.left],
        [node.left, node.right],
      ]) {
        if (
          against.type !== AST_NODE_TYPES.Literal ||
          typeof against.value !== 'boolean'
        ) {
          continue;
        }

        const booleanLiteral = against.value ? 'true' : 'false';
        const negated = !comparisonType.isPositive;

        return {
          booleanLiteral,
          expression,
          negated,
        };
      }

      return undefined;
    }

    function nodeIsUnaryNegation(node: TSESTree.Node): boolean {
      return (
        node.type === AST_NODE_TYPES.UnaryExpression && node.operator === '!'
      );
    }

    return {
      BinaryExpression(node): void {
        const comparison = getBooleanComparison(node);
        if (comparison == null) {
          return;
        }

        if (comparison.expressionIsNullableBoolean) {
          if (
            comparison.booleanLiteral === 'true' &&
            options.allowComparingNullableBooleansToTrue
          ) {
            return;
          }
          if (
            comparison.booleanLiteral === 'false' &&
            options.allowComparingNullableBooleansToFalse
          ) {
            return;
          }
        }

        context.report({
          node,
          messageId: comparison.expressionIsNullableBoolean
            ? comparison.booleanLiteral === 'true'
              ? comparison.negated
                ? 'comparingNullableToTrueNegated'
                : 'comparingNullableToTrueDirect'
              : 'comparingNullableToFalse'
            : comparison.negated
              ? 'negated'
              : 'direct',
          fix(fixer) {
            const isWrappedInUnaryNegation = nodeIsUnaryNegation(node.parent);
            const mutatedNode = isWrappedInUnaryNegation ? node.parent : node;

            // Whether the truth table of the overall expression being replaced
            // is negated, _ignoring the nullish cases_.
            const isOverallNegated = booleanXor(
              isWrappedInUnaryNegation,
              comparison.negated,
              comparison.booleanLiteral === 'false',
            );

            // we'll build up the replacement text from the compared expression outwards.
            let replacementText = context.sourceCode.getText(
              comparison.expression,
            );
            let mayNeedParentheses = !isStrongPrecedenceNode(
              comparison.expression,
            );

            const fixWouldReturnExpressionDirectly =
              !isOverallNegated && comparison.expressionIsNullableBoolean;

            if (
              fixWouldReturnExpressionDirectly &&
              !isConditionalTest(mutatedNode)
            ) {
              if (mayNeedParentheses) {
                replacementText = parenthesize(replacementText);
              }
              replacementText = `${replacementText} ?? false`;
              mayNeedParentheses = true;
            } else {
              // In maybeNullish === false, nullish values have the same truth table
              // as `true`.
              if (
                comparison.expressionIsNullableBoolean &&
                comparison.booleanLiteral === 'false'
              ) {
                if (mayNeedParentheses) {
                  replacementText = parenthesize(replacementText);
                }
                replacementText = `${replacementText} ?? true`;
                mayNeedParentheses = true;
              }

              if (isOverallNegated) {
                if (mayNeedParentheses) {
                  replacementText = parenthesize(replacementText);
                }
                replacementText = `!${replacementText}`;
                mayNeedParentheses = false;
              }
            }

            if (mayNeedParentheses && isWeakPrecedenceParent(mutatedNode)) {
              replacementText = parenthesize(replacementText);
            }

            return fixer.replaceText(mutatedNode, replacementText);
          },
        });
      },
    };
  }

getEqualsKind(operator: string): EqualsKind | undefined

Parameters:

  • operator string

Returns: EqualsKind | undefined

Code
function getEqualsKind(operator: string): EqualsKind | undefined {
  switch (operator) {
    case '!=':
      return {
        isPositive: false,
        isStrict: false,
      };

    case '!==':
      return {
        isPositive: false,
        isStrict: true,
      };

    case '==':
      return {
        isPositive: true,
        isStrict: false,
      };

    case '===':
      return {
        isPositive: true,
        isStrict: true,
      };

    default:
      return undefined;
  }
}

booleanXor(arg0: boolean, args: boolean[]): boolean

Parameters:

  • arg0 boolean
  • args boolean[]

Returns: boolean

Calls:

  • args.reduce
  • Boolean

Internal Comments:

// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion

Code
function booleanXor(arg0: boolean, ...args: boolean[]) {
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
  return args.reduce((acc, curr) => acc !== Boolean(curr), Boolean(arg0));
}

parenthesize(text: string): string

Parameters:

  • text string

Returns: string

Code
function parenthesize(text: string) {
  return `(${text})`;
}

Internal helpers

Declared inside another function in this file.

getBooleanComparison(node: TSESTree.BinaryExpression): BooleanComparisonWithTypeInformation | undefined

Parameters:

  • node TSESTree.BinaryExpression

Returns: BooleanComparisonWithTypeInformation | undefined

Calls:

  • deconstructComparison
  • getConstraintInfo (from ../util)
  • services.getTypeAtLocation
  • isBooleanType
  • isNullableBoolean
Code
function getBooleanComparison(
      node: TSESTree.BinaryExpression,
    ): BooleanComparisonWithTypeInformation | undefined {
      const comparison = deconstructComparison(node);
      if (!comparison) {
        return undefined;
      }

      const { constraintType, isTypeParameter } = getConstraintInfo(
        checker,
        services.getTypeAtLocation(comparison.expression),
      );

      if (isTypeParameter && constraintType == null) {
        return undefined;
      }

      if (isBooleanType(constraintType)) {
        return {
          ...comparison,
          expressionIsNullableBoolean: false,
        };
      }

      if (isNullableBoolean(constraintType)) {
        return {
          ...comparison,
          expressionIsNullableBoolean: true,
        };
      }

      return undefined;
    }

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,
      );
    }

isNullableBoolean(expressionType: ts.Type): boolean

checks if the expressionType is a union that 1) contains at least one nullish type (null or undefined) 2) contains at least once boolean type (true or false or boolean) 3) does not contain any types besides nullish and boolean types

Raw JSDoc
/**
     * checks if the expressionType is a union that
     *   1) contains at least one nullish type (null or undefined)
     *   2) contains at least once boolean type (true or false or boolean)
     *   3) does not contain any types besides nullish and boolean types
     */

Calls:

  • expressionType.isUnion
  • types.filter
  • tsutils.isTypeFlagSet
  • nonNullishTypes.every
Code
function isNullableBoolean(expressionType: ts.Type): boolean {
      if (!expressionType.isUnion()) {
        return false;
      }

      const { types } = expressionType;

      const nonNullishTypes = types.filter(
        type =>
          !tsutils.isTypeFlagSet(
            type,
            ts.TypeFlags.Undefined | ts.TypeFlags.Null,
          ),
      );

      const hasNonNullishType = nonNullishTypes.length > 0;
      if (!hasNonNullishType) {
        return false;
      }

      const hasNullableType = nonNullishTypes.length < types.length;
      if (!hasNullableType) {
        return false;
      }

      const allNonNullishTypesAreBoolean = nonNullishTypes.every(isBooleanType);
      if (!allNonNullishTypesAreBoolean) {
        return false;
      }

      return true;
    }

deconstructComparison(node: TSESTree.BinaryExpression): BooleanComparison | undefined

Parameters:

  • node TSESTree.BinaryExpression

Returns: BooleanComparison | undefined

Calls:

  • getEqualsKind
Code
function deconstructComparison(
      node: TSESTree.BinaryExpression,
    ): BooleanComparison | undefined {
      const comparisonType = getEqualsKind(node.operator);
      if (!comparisonType) {
        return undefined;
      }

      for (const [against, expression] of [
        [node.right, node.left],
        [node.left, node.right],
      ]) {
        if (
          against.type !== AST_NODE_TYPES.Literal ||
          typeof against.value !== 'boolean'
        ) {
          continue;
        }

        const booleanLiteral = against.value ? 'true' : 'false';
        const negated = !comparisonType.isPositive;

        return {
          booleanLiteral,
          expression,
          negated,
        };
      }

      return undefined;
    }

nodeIsUnaryNegation(node: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node

Returns: boolean

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

Interfaces

BooleanComparison

Interface Code
interface BooleanComparison {
  expression: TSESTree.Expression | TSESTree.PrivateIdentifier;
  booleanLiteral: 'false' | 'true';
  negated: boolean;
}

Properties

Name Type Optional Description
expression TSESTree.Expression \| TSESTree.PrivateIdentifier not shown
booleanLiteral 'false' \| 'true' not shown
negated boolean not shown

BooleanComparisonWithTypeInformation

Interface Code
interface BooleanComparisonWithTypeInformation extends BooleanComparison {
  expressionIsNullableBoolean: boolean;
}

Properties

Name Type Optional Description
expressionIsNullableBoolean boolean not shown

EqualsKind

Interface Code
interface EqualsKind {
  isPositive: boolean;
  isStrict: boolean;
}

Properties

Name Type Optional Description
isPositive boolean not shown
isStrict boolean not shown

Type Aliases

MessageIds

type MessageIds = | 'comparingNullableToFalse'
  | 'comparingNullableToTrueDirect'
  | 'comparingNullableToTrueNegated'
  | 'direct'
  | 'negated'
  | 'noStrictNullCheck';

Options

type Options = [
  {
    allowComparingNullableBooleansToFalse?: boolean;
    allowComparingNullableBooleansToTrue?: boolean;
    allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
  },
];

Generated by Syntax Scribe