Skip to content

⬅️ Back to Table of Contents

πŸ“„ gatherLogicalOperands

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 6
πŸ“¦ Imports 13
πŸ“Š Variables & Constants 1
πŸ“ Interfaces 3
πŸ“‘ Type Aliases 1
🎯 Enums 5

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/prefer-optional-chain-utils/gatherLogicalOperands.ts

πŸ“¦ Imports

Name Source
ParserServicesWithTypeInformation @typescript-eslint/utils
TSESTree @typescript-eslint/utils
SourceCode @typescript-eslint/utils/ts-eslint
AST_NODE_TYPES @typescript-eslint/utils
intersectionConstituents ts-api-utils
isBigIntLiteralType ts-api-utils
isBooleanLiteralType ts-api-utils
isNumberLiteralType ts-api-utils
isStringLiteralType ts-api-utils
unionConstituents ts-api-utils
PreferOptionalChainOptions ./PreferOptionalChainOptions
isReferenceToGlobalFunction ../../util
isTypeFlagSet ../../util

Variables & Constants

Name Type Kind Value Exported
NULLISH_FLAGS number const ts.TypeFlags.Null \| ts.TypeFlags.Undefined βœ—

Functions

gatherLogicalOperands(…): { newlySeenLogicals: Set<TSESTree.LogicalExpression>; opera…

Parameters:

  • node TSESTree.LogicalExpression
  • parserServices ParserServicesWithTypeInformation
  • sourceCode Readonly<SourceCode>
  • options PreferOptionalChainOptions

Returns: { newlySeenLogicals: Set<TSESTree.LogicalExpression>; operands: Operand[]; }

Calls:

  • flattenLogicalOperands
  • operands.at
  • complex_call_4922
  • getComparisonValueType
  • isReferenceToGlobalFunction (from ../../util)
  • result.push
  • operand.operator.startsWith
  • getBinaryComparisonChain
  • isValidFalseBooleanCheckType
  • stack.pop
  • newlySeenLogicals.add
  • stack.push
  • operands.push
  • isMemberBasedExpression

Internal Comments:

// check for "yoda" style logical: null != x (x2)
// non-yoda checks are by far the most common, so check for them first (x2)
// typeof window === 'undefined' (x2)
// typeof x.y === 'undefined' (x4)
// y === 'undefined' (x4)
// x == null, x == undefined (x4)
// x == something :( (x2)
// x === something :( (x2)
// x != something :( (x2)
// x !== something :( (x2)
// explicitly ignore the mixed logical expression cases (x4)
/*
  The AST is always constructed such the first element is always the deepest element.
  I.e. for this code: `foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz`
  The AST will look like this:
  {
    left: {
      left: {
        left: foo
        right: foo.bar
      }
      right: foo.bar.baz
    }
    right: foo.bar.baz.buzz
  }

  So given any logical expression, we can perform a depth-first traversal to get
  the operands in order.

  Note that this function purposely does not inspect mixed logical expressions
  like `foo || foo.bar && foo.bar.baz` - separate selector
  */
// eslint-disable-next-line eqeqeq, @typescript-eslint/internal/eqeq-nullish -- intentional exact comparison against null

Code
export function gatherLogicalOperands(
  node: TSESTree.LogicalExpression,
  parserServices: ParserServicesWithTypeInformation,
  sourceCode: Readonly<SourceCode>,
  options: PreferOptionalChainOptions,
): {
  newlySeenLogicals: Set<TSESTree.LogicalExpression>;
  operands: Operand[];
} {
  const result: Operand[] = [];
  const { newlySeenLogicals, operands } = flattenLogicalOperands(node);

  for (const operand of operands) {
    const areMoreOperands = operand !== operands.at(-1);
    switch (operand.type) {
      case AST_NODE_TYPES.BinaryExpression: {
        // check for "yoda" style logical: null != x

        const { comparedExpression, comparedValue, isYoda } = (() => {
          // non-yoda checks are by far the most common, so check for them first
          const comparedValueRight = getComparisonValueType(operand.right);
          if (comparedValueRight) {
            return {
              comparedExpression: operand.left,
              comparedValue: comparedValueRight,
              isYoda: false,
            };
          }
          return {
            comparedExpression: operand.right,
            comparedValue: getComparisonValueType(operand.left),
            isYoda: true,
          };
        })();

        if (comparedValue === ComparisonValueType.UndefinedStringLiteral) {
          if (
            comparedExpression.type === AST_NODE_TYPES.UnaryExpression &&
            comparedExpression.operator === 'typeof'
          ) {
            const argument = comparedExpression.argument;
            if (
              argument.type === AST_NODE_TYPES.Identifier &&
              // typeof window === 'undefined'
              isReferenceToGlobalFunction(argument.name, argument, sourceCode)
            ) {
              result.push({ type: OperandValidity.Invalid });
              continue;
            }

            // typeof x.y === 'undefined'
            result.push({
              comparedName: comparedExpression.argument,
              comparisonType: operand.operator.startsWith('!')
                ? NullishComparisonType.NotStrictEqualUndefined
                : NullishComparisonType.StrictEqualUndefined,
              isYoda,
              node: operand,
              type: OperandValidity.Valid,
            });
            continue;
          }

          // y === 'undefined'
          result.push({ type: OperandValidity.Invalid });
          continue;
        }

        if (operand.operator.startsWith('!') !== (node.operator === '||')) {
          switch (operand.operator) {
            case '!=':
            case '==':
              if (
                comparedValue === ComparisonValueType.Null ||
                comparedValue === ComparisonValueType.Undefined
              ) {
                // x == null, x == undefined
                result.push({
                  comparedName: comparedExpression,
                  comparisonType: operand.operator.startsWith('!')
                    ? NullishComparisonType.NotEqualNullOrUndefined
                    : NullishComparisonType.EqualNullOrUndefined,
                  isYoda,
                  node: operand,
                  type: OperandValidity.Valid,
                });
                continue;
              }
              break;

            case '!==':
            case '===': {
              const comparedName = comparedExpression;
              switch (comparedValue) {
                case ComparisonValueType.Null:
                  result.push({
                    comparedName,
                    comparisonType: operand.operator.startsWith('!')
                      ? NullishComparisonType.NotStrictEqualNull
                      : NullishComparisonType.StrictEqualNull,
                    isYoda,
                    node: operand,
                    type: OperandValidity.Valid,
                  });
                  continue;

                case ComparisonValueType.Undefined:
                  result.push({
                    comparedName,
                    comparisonType: operand.operator.startsWith('!')
                      ? NullishComparisonType.NotStrictEqualUndefined
                      : NullishComparisonType.StrictEqualUndefined,
                    isYoda,
                    node: operand,
                    type: OperandValidity.Valid,
                  });
                  continue;
              }
            }
          }
        }

        // x == something :(
        // x === something :(
        // x != something :(
        // x !== something :(
        const binaryComparisonChain = getBinaryComparisonChain(operand);
        if (binaryComparisonChain) {
          const { comparedName, comparedValue, yoda } = binaryComparisonChain;

          switch (operand.operator) {
            case '==':
            case '===': {
              const comparisonType =
                operand.operator === '=='
                  ? ComparisonType.Equal
                  : ComparisonType.StrictEqual;
              result.push({
                comparedName,
                comparisonType,
                comparisonValue: comparedValue,
                node: operand,
                type: OperandValidity.Last,
                yoda,
              });
              continue;
            }

            case '!=':
            case '!==': {
              const comparisonType =
                operand.operator === '!='
                  ? ComparisonType.NotEqual
                  : ComparisonType.NotStrictEqual;
              result.push({
                comparedName,
                comparisonType,
                comparisonValue: comparedValue,
                node: operand,
                type: OperandValidity.Last,
                yoda,
              });
              continue;
            }
          }
        }

        result.push({ type: OperandValidity.Invalid });
        continue;
      }

      case AST_NODE_TYPES.UnaryExpression:
        if (
          operand.operator === '!' &&
          (!areMoreOperands ||
            isValidFalseBooleanCheckType(
              operand.argument,
              node.operator === '||',
              parserServices,
              options,
            ))
        ) {
          result.push({
            comparedName: operand.argument,
            comparisonType: NullishComparisonType.NotBoolean,
            isYoda: false,
            node: operand,
            type: OperandValidity.Valid,
          });
          continue;
        }
        result.push({ type: OperandValidity.Invalid });
        continue;

      case AST_NODE_TYPES.LogicalExpression:
        // explicitly ignore the mixed logical expression cases
        result.push({ type: OperandValidity.Invalid });
        continue;

      default:
        if (
          !areMoreOperands ||
          isValidFalseBooleanCheckType(
            operand,
            node.operator === '&&',
            parserServices,
            options,
          )
        ) {
          result.push({
            comparedName: operand,
            comparisonType: NullishComparisonType.Boolean,
            isYoda: false,
            node: operand,
            type: OperandValidity.Valid,
          });
        } else {
          result.push({ type: OperandValidity.Invalid });
        }
        continue;
    }
  }

  return {
    newlySeenLogicals,
    operands: result,
  };

  /*
  The AST is always constructed such the first element is always the deepest element.
  I.e. for this code: `foo && foo.bar && foo.bar.baz && foo.bar.baz.buzz`
  The AST will look like this:
  {
    left: {
      left: {
        left: foo
        right: foo.bar
      }
      right: foo.bar.baz
    }
    right: foo.bar.baz.buzz
  }

  So given any logical expression, we can perform a depth-first traversal to get
  the operands in order.

  Note that this function purposely does not inspect mixed logical expressions
  like `foo || foo.bar && foo.bar.baz` - separate selector
  */
  function flattenLogicalOperands(node: TSESTree.LogicalExpression): {
    newlySeenLogicals: Set<TSESTree.LogicalExpression>;
    operands: TSESTree.Expression[];
  } {
    const operands: TSESTree.Expression[] = [];
    const newlySeenLogicals = new Set<TSESTree.LogicalExpression>([node]);

    const stack: TSESTree.Expression[] = [node.right, node.left];
    let current: TSESTree.Expression | undefined;
    while ((current = stack.pop())) {
      if (
        current.type === AST_NODE_TYPES.LogicalExpression &&
        current.operator === node.operator
      ) {
        newlySeenLogicals.add(current);
        stack.push(current.right);
        stack.push(current.left);
      } else {
        operands.push(current);
      }
    }

    return {
      newlySeenLogicals,
      operands,
    };
  }

  function getComparisonValueType(
    node: TSESTree.Node,
  ): ComparisonValueType | null {
    switch (node.type) {
      case AST_NODE_TYPES.Literal:
        // eslint-disable-next-line eqeqeq, @typescript-eslint/internal/eqeq-nullish -- intentional exact comparison against null
        if (node.value === null && node.raw === 'null') {
          return ComparisonValueType.Null;
        }
        if (node.value === 'undefined') {
          return ComparisonValueType.UndefinedStringLiteral;
        }
        return null;

      case AST_NODE_TYPES.Identifier:
        if (node.name === 'undefined') {
          return ComparisonValueType.Undefined;
        }
        return null;
    }

    return null;
  }

  function isMemberBasedExpression(
    node: TSESTree.Expression | TSESTree.PrivateIdentifier,
  ): node is TSESTree.CallExpression | TSESTree.MemberExpression {
    if (node.type === AST_NODE_TYPES.MemberExpression) {
      return true;
    }
    if (
      node.type === AST_NODE_TYPES.CallExpression &&
      node.callee.type === AST_NODE_TYPES.MemberExpression
    ) {
      return true;
    }
    return false;
  }

  function getBinaryComparisonChain(node: TSESTree.BinaryExpression) {
    const { left, right } = node;
    const isLeftMemberExpression = isMemberBasedExpression(left);
    const isRightMemberExpression = isMemberBasedExpression(right);
    if (isLeftMemberExpression && !isRightMemberExpression) {
      const [comparedName, comparedValue] = [left, right];
      return {
        comparedName,
        comparedValue,
        yoda: Yoda.No,
      };
    }
    if (!isLeftMemberExpression && isRightMemberExpression) {
      const [comparedName, comparedValue] = [right, left];
      return {
        comparedName,
        comparedValue,
        yoda: Yoda.Yes,
      };
    }
    if (isLeftMemberExpression && isRightMemberExpression) {
      return {
        comparedName: left,
        comparedValue: right,
        yoda: Yoda.Unknown,
      };
    }
    return null;
  }
}

isValidFalseBooleanCheckType(node: TSESTree.Node, disallowFalseyLiteral: boolean, parserServices: ParserServicesWithTypeInformation, options: PreferOptionalChainOptions): boolean

Parameters:

  • node TSESTree.Node
  • disallowFalseyLiteral boolean
  • parserServices ParserServicesWithTypeInformation
  • options PreferOptionalChainOptions

Returns: boolean

Calls:

  • parserServices.getTypeAtLocation
  • unionConstituents (from ts-api-utils)
  • types.flatMap
  • intersectionConstituents (from ts-api-utils)
  • primitiveAndObjectParts.some
  • isBooleanLiteralType (from ts-api-utils)
  • isStringLiteralType (from ts-api-utils)
  • isNumberLiteralType (from ts-api-utils)
  • isBigIntLiteralType (from ts-api-utils)
  • primitiveAndObjectParts.every
  • isTypeFlagSet (from ../../util)

Internal Comments:

/*
    ```
    declare const x: false | {a: string};
    x && x.a;
    !x || x.a;
    ```

    We don't want to consider these two cases because the boolean expression
    narrows out the non-nullish falsy cases - so converting the chain to `x?.a`
    would introduce a build error
    */

Code
function isValidFalseBooleanCheckType(
  node: TSESTree.Node,
  disallowFalseyLiteral: boolean,
  parserServices: ParserServicesWithTypeInformation,
  options: PreferOptionalChainOptions,
): boolean {
  const type = parserServices.getTypeAtLocation(node);
  const types = unionConstituents(type);
  const primitiveAndObjectParts = types.flatMap(type =>
    intersectionConstituents(type),
  );

  if (
    disallowFalseyLiteral &&
    /*
    ```
    declare const x: false | {a: string};
    x && x.a;
    !x || x.a;
    ```

    We don't want to consider these two cases because the boolean expression
    narrows out the non-nullish falsy cases - so converting the chain to `x?.a`
    would introduce a build error
    */ (primitiveAndObjectParts.some(
      t => isBooleanLiteralType(t) && t.intrinsicName === 'false',
    ) ||
      primitiveAndObjectParts.some(
        t => isStringLiteralType(t) && t.value === '',
      ) ||
      primitiveAndObjectParts.some(
        t => isNumberLiteralType(t) && t.value === 0,
      ) ||
      primitiveAndObjectParts.some(
        t => isBigIntLiteralType(t) && t.value.base10Value === '0',
      ))
  ) {
    return false;
  }

  let allowedFlags = NULLISH_FLAGS | ts.TypeFlags.Object;
  if (options.checkAny === true) {
    allowedFlags |= ts.TypeFlags.Any;
  }
  if (options.checkUnknown === true) {
    allowedFlags |= ts.TypeFlags.Unknown;
  }
  if (options.checkString === true) {
    allowedFlags |= ts.TypeFlags.StringLike;
  }
  if (options.checkNumber === true) {
    allowedFlags |= ts.TypeFlags.NumberLike;
  }
  if (options.checkBoolean === true) {
    allowedFlags |= ts.TypeFlags.BooleanLike;
  }
  if (options.checkBigInt === true) {
    allowedFlags |= ts.TypeFlags.BigIntLike;
  }
  return primitiveAndObjectParts.every(t => isTypeFlagSet(t, allowedFlags));
}

Internal helpers

Declared inside another function in this file.

flattenLogicalOperands(node: TSESTree.LogicalExpression): { newlySeenLogicals: Set<TSESTree.LogicalExpression>; opera…

Parameters:

  • node TSESTree.LogicalExpression

Returns: { newlySeenLogicals: Set<TSESTree.LogicalExpression>; operands: TSESTree.Expression[]; }

Calls:

  • stack.pop
  • newlySeenLogicals.add
  • stack.push
  • operands.push
Code
function flattenLogicalOperands(node: TSESTree.LogicalExpression): {
    newlySeenLogicals: Set<TSESTree.LogicalExpression>;
    operands: TSESTree.Expression[];
  } {
    const operands: TSESTree.Expression[] = [];
    const newlySeenLogicals = new Set<TSESTree.LogicalExpression>([node]);

    const stack: TSESTree.Expression[] = [node.right, node.left];
    let current: TSESTree.Expression | undefined;
    while ((current = stack.pop())) {
      if (
        current.type === AST_NODE_TYPES.LogicalExpression &&
        current.operator === node.operator
      ) {
        newlySeenLogicals.add(current);
        stack.push(current.right);
        stack.push(current.left);
      } else {
        operands.push(current);
      }
    }

    return {
      newlySeenLogicals,
      operands,
    };
  }

getComparisonValueType(node: TSESTree.Node): ComparisonValueType | null

Parameters:

  • node TSESTree.Node

Returns: ComparisonValueType | null

Internal Comments:

// eslint-disable-next-line eqeqeq, @typescript-eslint/internal/eqeq-nullish -- intentional exact comparison against null

Code
function getComparisonValueType(
    node: TSESTree.Node,
  ): ComparisonValueType | null {
    switch (node.type) {
      case AST_NODE_TYPES.Literal:
        // eslint-disable-next-line eqeqeq, @typescript-eslint/internal/eqeq-nullish -- intentional exact comparison against null
        if (node.value === null && node.raw === 'null') {
          return ComparisonValueType.Null;
        }
        if (node.value === 'undefined') {
          return ComparisonValueType.UndefinedStringLiteral;
        }
        return null;

      case AST_NODE_TYPES.Identifier:
        if (node.name === 'undefined') {
          return ComparisonValueType.Undefined;
        }
        return null;
    }

    return null;
  }

isMemberBasedExpression(node: TSESTree.Expression | TSESTree.PrivateI…): node is TSESTree.CallExpression | TSESTree.MemberExpression

Parameters:

  • node TSESTree.Expression | TSESTree.PrivateIdentifier

Returns: node is TSESTree.CallExpression | TSESTree.MemberExpression

Code
function isMemberBasedExpression(
    node: TSESTree.Expression | TSESTree.PrivateIdentifier,
  ): node is TSESTree.CallExpression | TSESTree.MemberExpression {
    if (node.type === AST_NODE_TYPES.MemberExpression) {
      return true;
    }
    if (
      node.type === AST_NODE_TYPES.CallExpression &&
      node.callee.type === AST_NODE_TYPES.MemberExpression
    ) {
      return true;
    }
    return false;
  }

getBinaryComparisonChain(node: TSESTree.BinaryExpression): { comparedName: any; comparedValue: TSESTree.BinaryExpressi…

Parameters:

  • node TSESTree.BinaryExpression

Returns: { comparedName: any; comparedValue: TSESTree.BinaryExpression; yoda: Yoda; }

Calls:

  • isMemberBasedExpression
Code
function getBinaryComparisonChain(node: TSESTree.BinaryExpression) {
    const { left, right } = node;
    const isLeftMemberExpression = isMemberBasedExpression(left);
    const isRightMemberExpression = isMemberBasedExpression(right);
    if (isLeftMemberExpression && !isRightMemberExpression) {
      const [comparedName, comparedValue] = [left, right];
      return {
        comparedName,
        comparedValue,
        yoda: Yoda.No,
      };
    }
    if (!isLeftMemberExpression && isRightMemberExpression) {
      const [comparedName, comparedValue] = [right, left];
      return {
        comparedName,
        comparedValue,
        yoda: Yoda.Yes,
      };
    }
    if (isLeftMemberExpression && isRightMemberExpression) {
      return {
        comparedName: left,
        comparedValue: right,
        yoda: Yoda.Unknown,
      };
    }
    return null;
  }

Interfaces

ValidOperand

Interface Code
export interface ValidOperand {
  comparedName: TSESTree.Node;
  comparisonType: NullishComparisonType;
  isYoda: boolean;
  node: TSESTree.Expression;
  type: OperandValidity.Valid;
}

Properties

Name Type Optional Description
comparedName TSESTree.Node βœ— not shown
comparisonType NullishComparisonType βœ— not shown
isYoda boolean βœ— not shown
node TSESTree.Expression βœ— not shown
type OperandValidity.Valid βœ— not shown

LastChainOperand

Interface Code
export interface LastChainOperand {
  comparedName: TSESTree.Node;
  comparisonType: ComparisonType;
  comparisonValue: TSESTree.Node;
  yoda: Yoda;
  node: TSESTree.BinaryExpression;
  type: OperandValidity.Last;
}

Properties

Name Type Optional Description
comparedName TSESTree.Node βœ— not shown
comparisonType ComparisonType βœ— not shown
comparisonValue TSESTree.Node βœ— not shown
yoda Yoda βœ— not shown
node TSESTree.BinaryExpression βœ— not shown
type OperandValidity.Last βœ— not shown

InvalidOperand

Interface Code
export interface InvalidOperand {
  type: OperandValidity.Invalid;
}

Properties

Name Type Optional Description
type OperandValidity.Invalid βœ— not shown

Type Aliases

Operand

type Operand = InvalidOperand | LastChainOperand | ValidOperand;

Enums

const enum Yoda

Enum Code
export const enum Yoda {
  Yes,
  No,
  Unknown,
}

Members

Name Value Description
Yes auto not shown
No auto not shown
Unknown auto not shown

const enum ComparisonValueType

Enum Code
const enum ComparisonValueType {
  Null = 'Null', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum
  Undefined = 'Undefined',
  UndefinedStringLiteral = 'UndefinedStringLiteral',
}

Members

Name Value Description
Null Null not shown
Undefined Undefined not shown
UndefinedStringLiteral UndefinedStringLiteral not shown

const enum OperandValidity

Enum Code
export const enum OperandValidity {
  Valid = 'Valid',
  Last = 'Last',
  Invalid = 'Invalid',
}

Members

Name Value Description
Valid Valid not shown
Last Last not shown
Invalid Invalid not shown

const enum NullishComparisonType

Enum Code
export const enum NullishComparisonType {
  /** `x != null`, `x != undefined` */
  NotEqualNullOrUndefined = 'NotEqualNullOrUndefined',
  /** `x == null`, `x == undefined` */
  EqualNullOrUndefined = 'EqualNullOrUndefined',

  /** `x !== null` */
  NotStrictEqualNull = 'NotStrictEqualNull',
  /** `x === null` */
  StrictEqualNull = 'StrictEqualNull',

  /** `x !== undefined`, `typeof x !== 'undefined'` */
  NotStrictEqualUndefined = 'NotStrictEqualUndefined',
  /** `x === undefined`, `typeof x === 'undefined'` */
  StrictEqualUndefined = 'StrictEqualUndefined',

  /** `!x` */
  NotBoolean = 'NotBoolean',
  /** `x` */
  Boolean = 'Boolean', // eslint-disable-line @typescript-eslint/internal/prefer-ast-types-enum
}

Members

Name Value Description
NotEqualNullOrUndefined NotEqualNullOrUndefined / x != null, x != undefined */
EqualNullOrUndefined EqualNullOrUndefined / x == null, x == undefined */
NotStrictEqualNull NotStrictEqualNull / x !== null */
StrictEqualNull StrictEqualNull / x === null */
NotStrictEqualUndefined NotStrictEqualUndefined / x !== undefined, typeof x !== 'undefined' */
StrictEqualUndefined StrictEqualUndefined / x === undefined, typeof x === 'undefined' */
NotBoolean NotBoolean / !x */
Boolean Boolean / x */

const enum ComparisonType

Enum Code
export const enum ComparisonType {
  NotEqual = 'NotEqual',
  Equal = 'Equal',
  NotStrictEqual = 'NotStrictEqual',
  StrictEqual = 'StrictEqual',
}

Members

Name Value Description
NotEqual NotEqual not shown
Equal Equal not shown
NotStrictEqual NotStrictEqual not shown
StrictEqual StrictEqual not shown

Generated by Syntax Scribe