Skip to content

⬅️ Back to Table of Contents

πŸ“„ prefer-nullish-coalescing

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 16
πŸ“¦ Imports 23
πŸ“‘ Type Aliases 3

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/prefer-nullish-coalescing.ts

πŸ“€ Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'prefer-nullish-coalescing'
meta.type 'suggestion'
meta.docs.description 'Enforce using the nullish coalescing operator instead of logical assignments or chaining'
meta.docs.recommended 'stylistic'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.noStrictNullCheck 'This rule requires the strictNullChecks compiler option to be turned on to function correctly.'
meta.messages.preferNullishOverAssignment 'Prefer using nullish coalescing operator (??{{ equals }}) instead of an assignment expression, as it is simpler to...
meta.messages.preferNullishOverOr 'Prefer using nullish coalescing operator (??{{ equals }}) instead of a logical {{ description }} (`||{{ equals }...
meta.messages.preferNullishOverTernary 'Prefer using nullish coalescing operator (??{{ equals }}) instead of a ternary expression, as it is simpler to read.'
meta.messages.suggestNullish 'Fix to nullish coalescing operator (??{{ equals }}).'
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing...
defaultOptions [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing: false, ignoreBooleanCoercion: false, ignoreConditionalTes...

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
AST_TOKEN_TYPES @typescript-eslint/utils
ASTUtils @typescript-eslint/utils
createRule ../util
getParserServices ../util
getTextWithParentheses ../util
getTypeFlags ../util
isConditionalTest ../util
isLogicalOrOperator ../util
isNodeEqual ../util
isNodeOfTypes ../util
isNullLiteral ../util
isNullableType ../util
isUndefinedIdentifier ../util
nullThrows ../util
NullThrowsReasons ../util
skipChainExpression ../util
isParenthesized ../util
getOperatorPrecedenceForNode ../util
OperatorPrecedence ../util
getWrappedCode ../util/getWrappedCode

Functions

create(context: any, [ { allowRuleToRunWithoutStrict…: any): { 'AssignmentExpression[operator = "||="]'(node: TSESTree.A…

Parameters:

  • context any
  • [ { allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing, ignoreBooleanCoercion, ignoreConditionalTests, ignoreIfStatements, ignoreMixedLogicalExpressions, ignorePrimitives, ignoreTernaryTests, }, ] any

Returns: { 'AssignmentExpression[operator = "||="]'(node: TSESTree.AssignmentExpression): void; ConditionalExpression(node: TSESTree.ConditionalExpression): void; IfStatement(node: TSESTree.IfStatement): void; 'LogicalExpression[operator = "||"]'(node: TSESTree.LogicalExpression): void; }

Calls:

  • getParserServices (from ../util)
  • parserServices.program.getCompilerOptions
  • tsutils.isStrictCompilerOptionEnabled
  • context.report
  • isNullableType (from ../util)
  • [ /* eslint-disable @typescript-eslint/no-non-null-assertion */ (ignorePrimitives === true || ignorePrimitives!.bigint) && ts.TypeFlags.BigIntLike, (ignorePrimitives === true || ignorePrimitives!.boolean) && ts.TypeFlags.BooleanLike, (ignorePrimitives === true || ignorePrimitives!.number) && ts.TypeFlags.NumberLike, (ignorePrimitives === true || ignorePrimitives!.string) && ts.TypeFlags.StringLike, /* eslint-enable @typescript-eslint/no-non-null-assertion */ ] .filter((flag): flag is number => typeof flag === 'number') .reduce
  • tsutils.isTypeFlagSet
  • tsutils .typeConstituents(type) .some
  • tsutils .intersectionConstituents(t) .some
  • isConditionalTest (from ../util)
  • isBooleanConstructorContext
  • parserServices.getTypeAtLocation
  • isTypeEligibleForPreferNullish
  • isTruthinessCheckEligibleForPreferNullish
  • isMixedLogicalExpression
  • nullThrows (from ../util)
  • context.sourceCode.getTokenAfter
  • NullThrowsReasons.MissingToken
  • isLogicalOrOperator (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • fixer.replaceText
  • node.operator.replace
  • areNodesSimilarMemberAccess
  • isNullLiteral (from ../util)
  • isUndefinedIdentifier (from ../util)
  • complex_call_14270
  • ['==', '!='].includes
  • getTypeFlags (from ../util)
  • checkAndFixWithPreferNullishOverOr
  • getOperatorAndNodesInsideTestExpression
  • getBranchNodes
  • getNullishCoalescingParams
  • getTextWithParentheses (from ../util)
  • isParenthesized (from ../util)
  • getWrappedCode (from ../util/getWrappedCode)
  • getOperatorPrecedenceForNode (from ../util)
  • isMemberAccessLike
  • ['!', '==', '==='].includes
  • formatComments
  • context.sourceCode.getCommentsBefore
  • context.sourceCode.getCommentsAfter
  • fixes.push
  • commentsAfter.slice

Internal Comments:

/**
     * Checks whether a type tested for truthiness is eligible for conversion to
     * a nullishness check, taking into account the rule's configuration.
     */
/* eslint-disable @typescript-eslint/no-non-null-assertion */ (x2)
// any types are eligible for conversion.
// if the type is `any` or `unknown` we can't make any assumptions
// about the value, so it could be any primitive, even though the flags
// won't be set.
//
// technically, this is true of `void` as well, however, it's a TS error
// to test `void` for truthiness, so we don't need to bother checking for
// it in valid code.
/**
     * Determines whether a control flow construct that uses the truthiness of
     * a test expression is eligible for conversion to the nullish coalescing
     * operator, taking into account (both dependent on the rule's configuration):
     * 1. Whether the construct is in a permitted syntactic context
     * 2. Whether the type of the test expression is deemed eligible for
     *    conversion
     *
     * @param node The overall node to be converted (e.g. `a || b` or `a ? a : b`)
     * @param testNode The node being tested (i.e. `a`)
     */
// '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)
// we check that the test only contains null, undefined and the identifier
// Only consider the first expression in a multi-part nullish check, (x3)
// as subsequent expressions might not require all the optional chaining operators. (x3)
// For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo'; (x3)
// This works because `node.test` is always evaluated first in the loop (x3)
// and has the same or more necessary optional chaining operators (x3)
// than `node.alternate` or `node.consequent`. (x3)
// it is fixable if we check for both null and undefined, or not if neither
// it is fixable if we loosely check for either null or undefined
// it is fixable if we check for undefined and the type is not nullable
// it is fixable if we check for null and the type can't be undefined
// TODO: also account for = in the ternary clause (x2)
// Handle comments (x2)

Code
create(
    context,
    [
      {
        allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing,
        ignoreBooleanCoercion,
        ignoreConditionalTests,
        ignoreIfStatements,
        ignoreMixedLogicalExpressions,
        ignorePrimitives,
        ignoreTernaryTests,
      },
    ],
  ) {
    const parserServices = getParserServices(context);
    const compilerOptions = parserServices.program.getCompilerOptions();

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

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

    /**
     * Checks whether a type tested for truthiness is eligible for conversion to
     * a nullishness check, taking into account the rule's configuration.
     */
    function isTypeEligibleForPreferNullish(type: ts.Type): boolean {
      if (!isNullableType(type)) {
        return false;
      }

      const ignorableFlags = [
        /* eslint-disable @typescript-eslint/no-non-null-assertion */
        (ignorePrimitives === true || ignorePrimitives!.bigint) &&
          ts.TypeFlags.BigIntLike,
        (ignorePrimitives === true || ignorePrimitives!.boolean) &&
          ts.TypeFlags.BooleanLike,
        (ignorePrimitives === true || ignorePrimitives!.number) &&
          ts.TypeFlags.NumberLike,
        (ignorePrimitives === true || ignorePrimitives!.string) &&
          ts.TypeFlags.StringLike,
        /* eslint-enable @typescript-eslint/no-non-null-assertion */
      ]
        .filter((flag): flag is number => typeof flag === 'number')
        .reduce((previous, flag) => previous | flag, 0);

      if (ignorableFlags === 0) {
        // any types are eligible for conversion.
        return true;
      }

      // if the type is `any` or `unknown` we can't make any assumptions
      // about the value, so it could be any primitive, even though the flags
      // won't be set.
      //
      // technically, this is true of `void` as well, however, it's a TS error
      // to test `void` for truthiness, so we don't need to bother checking for
      // it in valid code.
      if (
        tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)
      ) {
        return false;
      }

      if (
        tsutils
          .typeConstituents(type)
          .some(t =>
            tsutils
              .intersectionConstituents(t)
              .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)),
          )
      ) {
        return false;
      }

      return true;
    }

    /**
     * Determines whether a control flow construct that uses the truthiness of
     * a test expression is eligible for conversion to the nullish coalescing
     * operator, taking into account (both dependent on the rule's configuration):
     * 1. Whether the construct is in a permitted syntactic context
     * 2. Whether the type of the test expression is deemed eligible for
     *    conversion
     *
     * @param node The overall node to be converted (e.g. `a || b` or `a ? a : b`)
     * @param testNode The node being tested (i.e. `a`)
     */
    function isTruthinessCheckEligibleForPreferNullish({
      node,
      testNode,
    }: {
      node:
        | TSESTree.AssignmentExpression
        | TSESTree.ConditionalExpression
        | TSESTree.IfStatement
        | TSESTree.LogicalExpression;
      testNode: TSESTree.Node;
    }): boolean {
      if (ignoreConditionalTests === true && isConditionalTest(node)) {
        return false;
      }

      if (
        ignoreBooleanCoercion === true &&
        isBooleanConstructorContext(node, context) &&
        !(
          node.type === AST_NODE_TYPES.ConditionalExpression &&
          node.parent.type === AST_NODE_TYPES.CallExpression
        )
      ) {
        return false;
      }

      const testType = parserServices.getTypeAtLocation(testNode);
      if (!isTypeEligibleForPreferNullish(testType)) {
        return false;
      }

      return true;
    }

    function checkAndFixWithPreferNullishOverOr(
      node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression,
      description: string,
      equals: string,
    ): void {
      if (
        !isTruthinessCheckEligibleForPreferNullish({
          node,
          testNode: node.left,
        })
      ) {
        return;
      }

      if (
        ignoreMixedLogicalExpressions === true &&
        isMixedLogicalExpression(node)
      ) {
        return;
      }

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

      function* fix(
        fixer: TSESLint.RuleFixer,
      ): IterableIterator<TSESLint.RuleFix> {
        if (isLogicalOrOperator(node.parent)) {
          // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)
          if (
            node.left.type === AST_NODE_TYPES.LogicalExpression &&
            !isLogicalOrOperator(node.left.left)
          ) {
            yield fixer.insertTextBefore(node.left.right, '(');
          } else {
            yield fixer.insertTextBefore(node.left, '(');
          }
          yield fixer.insertTextAfter(node.right, ')');
        }
        yield fixer.replaceText(
          barBarOperator,
          node.operator.replace('||', '??'),
        );
      }

      context.report({
        node: barBarOperator,
        messageId: 'preferNullishOverOr',
        data: { description, equals },
        suggest: [
          {
            messageId: 'suggestNullish',
            data: { equals },
            fix,
          },
        ],
      });
    }

    function getNullishCoalescingParams(
      node: TSESTree.ConditionalExpression | TSESTree.IfStatement,
      nonNullishNode: TSESTree.Expression,
      nodesInsideTestExpression: TSESTree.Node[],
      operator: NullishCheckOperator,
    ):
      | { isFixable: false }
      | { isFixable: true; nullishCoalescingLeftNode: TSESTree.Node } {
      let nullishCoalescingLeftNode: TSESTree.Node | undefined;
      let hasTruthinessCheck = false;
      let hasNullCheckWithoutTruthinessCheck = false;
      let hasUndefinedCheckWithoutTruthinessCheck = false;

      if (!nodesInsideTestExpression.length) {
        hasTruthinessCheck = true;
        nullishCoalescingLeftNode =
          node.test.type === AST_NODE_TYPES.UnaryExpression
            ? node.test.argument
            : node.test;

        if (
          !areNodesSimilarMemberAccess(
            nullishCoalescingLeftNode,
            nonNullishNode,
          )
        ) {
          return { isFixable: false };
        }
      } else {
        // we check that the test only contains null, undefined and the identifier
        for (const testNode of nodesInsideTestExpression) {
          if (isNullLiteral(testNode)) {
            hasNullCheckWithoutTruthinessCheck = true;
          } else if (isUndefinedIdentifier(testNode)) {
            hasUndefinedCheckWithoutTruthinessCheck = true;
          } else if (areNodesSimilarMemberAccess(testNode, nonNullishNode)) {
            // Only consider the first expression in a multi-part nullish check,
            // as subsequent expressions might not require all the optional chaining operators.
            // For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo';
            // This works because `node.test` is always evaluated first in the loop
            // and has the same or more necessary optional chaining operators
            // than `node.alternate` or `node.consequent`.
            nullishCoalescingLeftNode ??= testNode;
          } else {
            return { isFixable: false };
          }
        }
      }

      if (!nullishCoalescingLeftNode) {
        return { isFixable: false };
      }

      const isFixable = ((): boolean => {
        if (hasTruthinessCheck) {
          return isTruthinessCheckEligibleForPreferNullish({
            node,
            testNode: nullishCoalescingLeftNode,
          });
        }

        // it is fixable if we check for both null and undefined, or not if neither
        if (
          hasUndefinedCheckWithoutTruthinessCheck ===
          hasNullCheckWithoutTruthinessCheck
        ) {
          return hasUndefinedCheckWithoutTruthinessCheck;
        }

        // it is fixable if we loosely check for either null or undefined
        if (['==', '!='].includes(operator)) {
          return true;
        }

        const type = parserServices.getTypeAtLocation(
          nullishCoalescingLeftNode,
        );
        const flags = getTypeFlags(type);

        if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
          return false;
        }

        const hasNullType = (flags & ts.TypeFlags.Null) !== 0;

        // it is fixable if we check for undefined and the type is not nullable
        if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) {
          return true;
        }

        const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0;

        // it is fixable if we check for null and the type can't be undefined
        return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType;
      })();

      return isFixable
        ? { isFixable: true, nullishCoalescingLeftNode }
        : { isFixable: false };
    }

    return {
      'AssignmentExpression[operator = "||="]'(
        node: TSESTree.AssignmentExpression,
      ): void {
        checkAndFixWithPreferNullishOverOr(node, 'assignment', '=');
      },
      ConditionalExpression(node: TSESTree.ConditionalExpression): void {
        if (ignoreTernaryTests) {
          return;
        }

        const { nodesInsideTestExpression, operator } =
          getOperatorAndNodesInsideTestExpression(node);

        if (operator == null) {
          return;
        }

        const { nonNullishBranch, nullishBranch } = getBranchNodes(
          node,
          operator,
        );

        const nullishCoalescingParams = getNullishCoalescingParams(
          node,
          nonNullishBranch,
          nodesInsideTestExpression,
          operator,
        );

        if (nullishCoalescingParams.isFixable) {
          context.report({
            node,
            messageId: 'preferNullishOverTernary',
            // TODO: also account for = in the ternary clause
            data: { equals: '' },
            suggest: [
              {
                messageId: 'suggestNullish',
                data: { equals: '' },
                fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix {
                  const nullishBranchText = getTextWithParentheses(
                    context.sourceCode,
                    nullishBranch,
                  );
                  const rightOperandReplacement = isParenthesized(
                    nullishBranch,
                    context.sourceCode,
                  )
                    ? nullishBranchText
                    : getWrappedCode(
                        nullishBranchText,
                        getOperatorPrecedenceForNode(nullishBranch),
                        OperatorPrecedence.Coalesce,
                      );

                  return fixer.replaceText(
                    node,
                    `${getTextWithParentheses(
                      context.sourceCode,
                      nullishCoalescingParams.nullishCoalescingLeftNode,
                    )} ?? ${rightOperandReplacement}`,
                  );
                },
              },
            ],
          });
        }
      },
      IfStatement(node: TSESTree.IfStatement): void {
        if (ignoreIfStatements || node.alternate != null) {
          return;
        }

        let assignmentExpression: TSESTree.Expression | undefined;
        if (
          node.consequent.type === AST_NODE_TYPES.BlockStatement &&
          node.consequent.body.length === 1 &&
          node.consequent.body[0].type === AST_NODE_TYPES.ExpressionStatement
        ) {
          assignmentExpression = node.consequent.body[0].expression;
        } else if (
          node.consequent.type === AST_NODE_TYPES.ExpressionStatement
        ) {
          assignmentExpression = node.consequent.expression;
        }

        if (
          assignmentExpression?.type !== AST_NODE_TYPES.AssignmentExpression ||
          !isMemberAccessLike(assignmentExpression.left)
        ) {
          return;
        }

        const nullishCoalescingLeftNode = assignmentExpression.left;
        const nullishCoalescingRightNode = assignmentExpression.right;

        const { nodesInsideTestExpression, operator } =
          getOperatorAndNodesInsideTestExpression(node);

        if (operator == null || !['!', '==', '==='].includes(operator)) {
          return;
        }

        const nullishCoalescingParams = getNullishCoalescingParams(
          node,
          nullishCoalescingLeftNode,
          nodesInsideTestExpression,
          operator,
        );

        if (nullishCoalescingParams.isFixable) {
          // Handle comments
          const isConsequentNodeBlockStatement =
            node.consequent.type === AST_NODE_TYPES.BlockStatement;

          const commentsBefore = formatComments(
            context.sourceCode.getCommentsBefore(assignmentExpression),
            isConsequentNodeBlockStatement ? '\n' : ' ',
          );
          const commentsAfter = isConsequentNodeBlockStatement
            ? formatComments(
                context.sourceCode.getCommentsAfter(
                  assignmentExpression.parent,
                ),
                '\n',
              )
            : '';

          context.report({
            node,
            messageId: 'preferNullishOverAssignment',
            data: { equals: '=' },
            suggest: [
              {
                messageId: 'suggestNullish',
                data: { equals: '=' },
                fix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix[] {
                  const fixes: TSESLint.RuleFix[] = [];

                  if (commentsBefore) {
                    fixes.push(fixer.insertTextBefore(node, commentsBefore));
                  }

                  fixes.push(
                    fixer.replaceText(
                      node,
                      `${getTextWithParentheses(
                        context.sourceCode,
                        nullishCoalescingLeftNode,
                      )} ??= ${getTextWithParentheses(
                        context.sourceCode,
                        nullishCoalescingRightNode,
                      )};`,
                    ),
                  );

                  if (commentsAfter) {
                    fixes.push(
                      fixer.insertTextAfter(
                        node,
                        ` ${commentsAfter.slice(0, -1)}`,
                      ),
                    );
                  }

                  return fixes;
                },
              },
            ],
          });
        }
      },
      'LogicalExpression[operator = "||"]'(
        node: TSESTree.LogicalExpression,
      ): void {
        checkAndFixWithPreferNullishOverOr(node, 'or', '');
      },
    };
  }

isNullLiteralOrUndefinedIdentifier(node: TSESTree.Node): boolean

Parameters:

  • node TSESTree.Node

Returns: boolean

Code
(node: TSESTree.Node): boolean =>
  isNullLiteral(node) || isUndefinedIdentifier(node)

isNodeNullishComparison(node: TSESTree.BinaryExpression): boolean

Parameters:

  • node TSESTree.BinaryExpression

Returns: boolean

Code
(node: TSESTree.BinaryExpression): boolean =>
  isNullLiteralOrUndefinedIdentifier(node.left) &&
  isNullLiteralOrUndefinedIdentifier(node.right)

isBooleanConstructorContext(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<MessageId…): boolean

Parameters:

  • node TSESTree.Node
  • context Readonly<TSESLint.RuleContext<MessageIds, Options>>

Returns: boolean

Calls:

  • isBooleanConstructorContext
  • parent.expressions.at
  • isBuiltInBooleanCall
Code
function isBooleanConstructorContext(
  node: TSESTree.Node,
  context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
): boolean {
  const parent = node.parent;
  if (parent == null) {
    return false;
  }

  if (parent.type === AST_NODE_TYPES.LogicalExpression) {
    return isBooleanConstructorContext(parent, context);
  }

  if (
    parent.type === AST_NODE_TYPES.ConditionalExpression &&
    (parent.consequent === node || parent.alternate === node)
  ) {
    return isBooleanConstructorContext(parent, context);
  }

  if (
    parent.type === AST_NODE_TYPES.SequenceExpression &&
    parent.expressions.at(-1) === node
  ) {
    return isBooleanConstructorContext(parent, context);
  }

  return isBuiltInBooleanCall(parent, context);
}

isBuiltInBooleanCall(node: TSESTree.Node, context: Readonly<TSESLint.RuleContext<MessageId…): boolean

Parameters:

  • node TSESTree.Node
  • context Readonly<TSESLint.RuleContext<MessageIds, Options>>

Returns: boolean

Calls:

  • context.sourceCode.getScope
  • ASTUtils.findVariable

Internal Comments:

// eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum (x4)

Code
function isBuiltInBooleanCall(
  node: TSESTree.Node,
  context: Readonly<TSESLint.RuleContext<MessageIds, Options>>,
): boolean {
  if (
    node.type === AST_NODE_TYPES.CallExpression &&
    node.callee.type === AST_NODE_TYPES.Identifier &&
    // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum
    node.callee.name === 'Boolean' &&
    node.arguments[0]
  ) {
    const scope = context.sourceCode.getScope(node);
    const variable = ASTUtils.findVariable(scope, node.callee);
    return variable == null || variable.defs.length === 0;
  }
  return false;
}

isMixedLogicalExpression(node: TSESTree.AssignmentExpression | TSESTre…): boolean

Parameters:

  • node TSESTree.AssignmentExpression | TSESTree.LogicalExpression

Returns: boolean

Calls:

  • seen.has
  • seen.add
  • ['||', '||='].includes
  • queue.push

Internal Comments:

// check the pieces of the node to catch cases like `a || b || c && d` (x4)

Code
function isMixedLogicalExpression(
  node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression,
): boolean {
  const seen = new Set<TSESTree.Node | undefined>();
  const queue = [node.parent, node.left, node.right];
  for (const current of queue) {
    if (seen.has(current)) {
      continue;
    }
    seen.add(current);

    if (current.type === AST_NODE_TYPES.LogicalExpression) {
      if (current.operator === '&&') {
        return true;
      }

      if (['||', '||='].includes(current.operator)) {
        // check the pieces of the node to catch cases like `a || b || c && d`
        queue.push(current.parent, current.left, current.right);
      }
    }
  }

  return false;
}

areNodesSimilarMemberAccess(a: TSESTree.Node, b: TSESTree.Node): boolean

Checks if two TSESTree nodes have the same member access sequence, regardless of optional chaining differences.

Note: This does not imply that the nodes are runtime-equivalent.

Example: a.b.c, a?.b.c, a.b?.c, (a?.b).c, (a.b)?.c are considered similar.

Parameters:

  • a any: First TSESTree node.
  • b any: Second TSESTree node.

Returns: undefined true if the nodes access members in the same order; otherwise, false.

Raw JSDoc
/**
 * Checks if two TSESTree nodes have the same member access sequence,
 * regardless of optional chaining differences.
 *
 * Note: This does not imply that the nodes are runtime-equivalent.
 *
 * Example: `a.b.c`, `a?.b.c`, `a.b?.c`, `(a?.b).c`, `(a.b)?.c` are considered similar.
 *
 * @param a First TSESTree node.
 * @param b Second TSESTree node.
 * @returns `true` if the nodes access members in the same order; otherwise, `false`.
 */

Calls:

  • areNodesSimilarMemberAccess
  • isNodeEqual (from ../util)
  • skipChainExpression (from ../util)
Code
function areNodesSimilarMemberAccess(
  a: TSESTree.Node,
  b: TSESTree.Node,
): boolean {
  if (
    a.type === AST_NODE_TYPES.MemberExpression &&
    b.type === AST_NODE_TYPES.MemberExpression
  ) {
    if (!areNodesSimilarMemberAccess(a.object, b.object)) {
      return false;
    }

    if (a.computed === b.computed) {
      return isNodeEqual(a.property, b.property);
    }
    if (
      a.property.type === AST_NODE_TYPES.Literal &&
      b.property.type === AST_NODE_TYPES.Identifier
    ) {
      return a.property.value === b.property.name;
    }
    if (
      a.property.type === AST_NODE_TYPES.Identifier &&
      b.property.type === AST_NODE_TYPES.Literal
    ) {
      return a.property.name === b.property.value;
    }
    return false;
  }
  if (
    a.type === AST_NODE_TYPES.ChainExpression ||
    b.type === AST_NODE_TYPES.ChainExpression
  ) {
    return areNodesSimilarMemberAccess(
      skipChainExpression(a),
      skipChainExpression(b),
    );
  }
  return isNodeEqual(a, b);
}

getBranchNodes(node: TSESTree.ConditionalExpression, operator: NullishCheckOperator): { nonNullishBranch: TSESTree.Expression; nullishBranch: TSE…

Returns the branch nodes of a conditional expression: - the "nonNullish branch" is the branch when test node is not nullish - the "nullish branch" is the branch when test node is nullish

Raw JSDoc
/**
 * Returns the branch nodes of a conditional expression:
 * - the "nonNullish branch" is the branch when test node is not nullish
 * - the "nullish branch" is the branch when test node is nullish
 */

Calls:

  • ['', '!=', '!=='].includes
Code
function getBranchNodes(
  node: TSESTree.ConditionalExpression,
  operator: NullishCheckOperator,
): {
  nonNullishBranch: TSESTree.Expression;
  nullishBranch: TSESTree.Expression;
} {
  if (['', '!=', '!=='].includes(operator)) {
    return { nonNullishBranch: node.consequent, nullishBranch: node.alternate };
  }
  return { nonNullishBranch: node.alternate, nullishBranch: node.consequent };
}

getOperatorAndNodesInsideTestExpression(node: TSESTree.ConditionalExpression | TSESTr…): { nodesInsideTestExpression: TSESTree.Node[]; operator: Nul…

Parameters:

  • node TSESTree.ConditionalExpression | TSESTree.IfStatement

Returns: { nodesInsideTestExpression: TSESTree.Node[]; operator: NullishCheckOperator | null; }

Calls:

  • isMemberAccessLike
  • getNonBinaryNodeOperator
  • isNodeNullishComparison
  • ['||', '||='].includes
Code
function getOperatorAndNodesInsideTestExpression(
  node: TSESTree.ConditionalExpression | TSESTree.IfStatement,
): {
  nodesInsideTestExpression: TSESTree.Node[];
  operator: NullishCheckOperator | null;
} {
  let operator: NullishCheckOperator | null = null;
  let nodesInsideTestExpression: TSESTree.Node[] = [];

  if (
    isMemberAccessLike(node.test) ||
    node.test.type === AST_NODE_TYPES.UnaryExpression
  ) {
    operator = getNonBinaryNodeOperator(node.test);
  } else if (node.test.type === AST_NODE_TYPES.BinaryExpression) {
    nodesInsideTestExpression = [node.test.left, node.test.right];
    if (
      node.test.operator === '==' ||
      node.test.operator === '!=' ||
      node.test.operator === '===' ||
      node.test.operator === '!=='
    ) {
      operator = node.test.operator;
    }
  } else if (
    node.test.type === AST_NODE_TYPES.LogicalExpression &&
    node.test.left.type === AST_NODE_TYPES.BinaryExpression &&
    node.test.right.type === AST_NODE_TYPES.BinaryExpression
  ) {
    if (
      isNodeNullishComparison(node.test.left) ||
      isNodeNullishComparison(node.test.right)
    ) {
      return { nodesInsideTestExpression, operator };
    }
    nodesInsideTestExpression = [
      node.test.left.left,
      node.test.left.right,
      node.test.right.left,
      node.test.right.right,
    ];
    if (['||', '||='].includes(node.test.operator)) {
      if (
        node.test.left.operator === '===' &&
        node.test.right.operator === '==='
      ) {
        operator = '===';
      } else if (
        ((node.test.left.operator === '===' ||
          node.test.right.operator === '===') &&
          (node.test.left.operator === '==' ||
            node.test.right.operator === '==')) ||
        (node.test.left.operator === '==' && node.test.right.operator === '==')
      ) {
        operator = '==';
      }
    } else if (node.test.operator === '&&') {
      if (
        node.test.left.operator === '!==' &&
        node.test.right.operator === '!=='
      ) {
        operator = '!==';
      } else if (
        ((node.test.left.operator === '!==' ||
          node.test.right.operator === '!==') &&
          (node.test.left.operator === '!=' ||
            node.test.right.operator === '!=')) ||
        (node.test.left.operator === '!=' && node.test.right.operator === '!=')
      ) {
        operator = '!=';
      }
    }
  }

  return { nodesInsideTestExpression, operator };
}

getNonBinaryNodeOperator(node: | TSESTree.ChainExpression | TSESTree.I…): NullishCheckOperator | null

Parameters:

  • node | TSESTree.ChainExpression | TSESTree.Identifier | TSESTree.MemberExpression | TSESTree.UnaryExpression

Returns: NullishCheckOperator | null

Calls:

  • isMemberAccessLike
Code
function getNonBinaryNodeOperator(
  node:
    | TSESTree.ChainExpression
    | TSESTree.Identifier
    | TSESTree.MemberExpression
    | TSESTree.UnaryExpression,
): NullishCheckOperator | null {
  if (node.type !== AST_NODE_TYPES.UnaryExpression) {
    return '';
  }
  if (isMemberAccessLike(node.argument) && node.operator === '!') {
    return '!';
  }
  return null;
}

formatComments(comments: TSESTree.Comment[], separator: string): string

Parameters:

  • comments TSESTree.Comment[]
  • separator string

Returns: string

Calls:

  • comments .map(({ type, value }) => type === AST_TOKEN_TYPES.Line ?//${value}${separator}:/${value}/${separator}, ) .join
Code
function formatComments(
  comments: TSESTree.Comment[],
  separator: string,
): string {
  return comments
    .map(({ type, value }) =>
      type === AST_TOKEN_TYPES.Line
        ? `//${value}${separator}`
        : `/*${value}*/${separator}`,
    )
    .join('');
}

Internal helpers

Declared inside another function in this file.

isTypeEligibleForPreferNullish(type: ts.Type): boolean

Checks whether a type tested for truthiness is eligible for conversion to a nullishness check, taking into account the rule's configuration.

Raw JSDoc
/**
     * Checks whether a type tested for truthiness is eligible for conversion to
     * a nullishness check, taking into account the rule's configuration.
     */

Calls:

  • isNullableType (from ../util)
  • [ /* eslint-disable @typescript-eslint/no-non-null-assertion */ (ignorePrimitives === true || ignorePrimitives!.bigint) && ts.TypeFlags.BigIntLike, (ignorePrimitives === true || ignorePrimitives!.boolean) && ts.TypeFlags.BooleanLike, (ignorePrimitives === true || ignorePrimitives!.number) && ts.TypeFlags.NumberLike, (ignorePrimitives === true || ignorePrimitives!.string) && ts.TypeFlags.StringLike, /* eslint-enable @typescript-eslint/no-non-null-assertion */ ] .filter((flag): flag is number => typeof flag === 'number') .reduce
  • tsutils.isTypeFlagSet
  • tsutils .typeConstituents(type) .some
  • tsutils .intersectionConstituents(t) .some

Internal Comments:

/* eslint-disable @typescript-eslint/no-non-null-assertion */ (x2)
// any types are eligible for conversion.
// if the type is `any` or `unknown` we can't make any assumptions
// about the value, so it could be any primitive, even though the flags
// won't be set.
//
// technically, this is true of `void` as well, however, it's a TS error
// to test `void` for truthiness, so we don't need to bother checking for
// it in valid code.

Code
function isTypeEligibleForPreferNullish(type: ts.Type): boolean {
      if (!isNullableType(type)) {
        return false;
      }

      const ignorableFlags = [
        /* eslint-disable @typescript-eslint/no-non-null-assertion */
        (ignorePrimitives === true || ignorePrimitives!.bigint) &&
          ts.TypeFlags.BigIntLike,
        (ignorePrimitives === true || ignorePrimitives!.boolean) &&
          ts.TypeFlags.BooleanLike,
        (ignorePrimitives === true || ignorePrimitives!.number) &&
          ts.TypeFlags.NumberLike,
        (ignorePrimitives === true || ignorePrimitives!.string) &&
          ts.TypeFlags.StringLike,
        /* eslint-enable @typescript-eslint/no-non-null-assertion */
      ]
        .filter((flag): flag is number => typeof flag === 'number')
        .reduce((previous, flag) => previous | flag, 0);

      if (ignorableFlags === 0) {
        // any types are eligible for conversion.
        return true;
      }

      // if the type is `any` or `unknown` we can't make any assumptions
      // about the value, so it could be any primitive, even though the flags
      // won't be set.
      //
      // technically, this is true of `void` as well, however, it's a TS error
      // to test `void` for truthiness, so we don't need to bother checking for
      // it in valid code.
      if (
        tsutils.isTypeFlagSet(type, ts.TypeFlags.Any | ts.TypeFlags.Unknown)
      ) {
        return false;
      }

      if (
        tsutils
          .typeConstituents(type)
          .some(t =>
            tsutils
              .intersectionConstituents(t)
              .some(t => tsutils.isTypeFlagSet(t, ignorableFlags)),
          )
      ) {
        return false;
      }

      return true;
    }

isTruthinessCheckEligibleForPreferNullish({ node, testNode, }: { node: | TSESTree.AssignmentExpression…): boolean

Determines whether a control flow construct that uses the truthiness of a test expression is eligible for conversion to the nullish coalescing operator, taking into account (both dependent on the rule's configuration): 1. Whether the construct is in a permitted syntactic context 2. Whether the type of the test expression is deemed eligible for conversion

Parameters:

  • node any: The overall node to be converted (e.g. a || b or a ? a : b)
  • testNode any: The node being tested (i.e. a)
Raw JSDoc
/**
     * Determines whether a control flow construct that uses the truthiness of
     * a test expression is eligible for conversion to the nullish coalescing
     * operator, taking into account (both dependent on the rule's configuration):
     * 1. Whether the construct is in a permitted syntactic context
     * 2. Whether the type of the test expression is deemed eligible for
     *    conversion
     *
     * @param node The overall node to be converted (e.g. `a || b` or `a ? a : b`)
     * @param testNode The node being tested (i.e. `a`)
     */

Calls:

  • isConditionalTest (from ../util)
  • isBooleanConstructorContext
  • parserServices.getTypeAtLocation
  • isTypeEligibleForPreferNullish
Code
function isTruthinessCheckEligibleForPreferNullish({
      node,
      testNode,
    }: {
      node:
        | TSESTree.AssignmentExpression
        | TSESTree.ConditionalExpression
        | TSESTree.IfStatement
        | TSESTree.LogicalExpression;
      testNode: TSESTree.Node;
    }): boolean {
      if (ignoreConditionalTests === true && isConditionalTest(node)) {
        return false;
      }

      if (
        ignoreBooleanCoercion === true &&
        isBooleanConstructorContext(node, context) &&
        !(
          node.type === AST_NODE_TYPES.ConditionalExpression &&
          node.parent.type === AST_NODE_TYPES.CallExpression
        )
      ) {
        return false;
      }

      const testType = parserServices.getTypeAtLocation(testNode);
      if (!isTypeEligibleForPreferNullish(testType)) {
        return false;
      }

      return true;
    }

checkAndFixWithPreferNullishOverOr(node: TSESTree.AssignmentExpression | TSESTre…, description: string, equals: string): void

Parameters:

  • node TSESTree.AssignmentExpression | TSESTree.LogicalExpression
  • description string
  • equals string

Returns: void

Calls:

  • isTruthinessCheckEligibleForPreferNullish
  • isMixedLogicalExpression
  • nullThrows (from ../util)
  • context.sourceCode.getTokenAfter
  • NullThrowsReasons.MissingToken
  • isLogicalOrOperator (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • fixer.replaceText
  • node.operator.replace
  • context.report

Internal Comments:

// '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)

Code
function checkAndFixWithPreferNullishOverOr(
      node: TSESTree.AssignmentExpression | TSESTree.LogicalExpression,
      description: string,
      equals: string,
    ): void {
      if (
        !isTruthinessCheckEligibleForPreferNullish({
          node,
          testNode: node.left,
        })
      ) {
        return;
      }

      if (
        ignoreMixedLogicalExpressions === true &&
        isMixedLogicalExpression(node)
      ) {
        return;
      }

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

      function* fix(
        fixer: TSESLint.RuleFixer,
      ): IterableIterator<TSESLint.RuleFix> {
        if (isLogicalOrOperator(node.parent)) {
          // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)
          if (
            node.left.type === AST_NODE_TYPES.LogicalExpression &&
            !isLogicalOrOperator(node.left.left)
          ) {
            yield fixer.insertTextBefore(node.left.right, '(');
          } else {
            yield fixer.insertTextBefore(node.left, '(');
          }
          yield fixer.insertTextAfter(node.right, ')');
        }
        yield fixer.replaceText(
          barBarOperator,
          node.operator.replace('||', '??'),
        );
      }

      context.report({
        node: barBarOperator,
        messageId: 'preferNullishOverOr',
        data: { description, equals },
        suggest: [
          {
            messageId: 'suggestNullish',
            data: { equals },
            fix,
          },
        ],
      });
    }

fix(fixer: TSESLint.RuleFixer): IterableIterator<TSESLint.RuleFix>

Parameters:

  • fixer TSESLint.RuleFixer

Returns: IterableIterator<TSESLint.RuleFix>

Calls:

  • isLogicalOrOperator (from ../util)
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • fixer.replaceText
  • node.operator.replace

Internal Comments:

// '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)

Code
function* fix(
        fixer: TSESLint.RuleFixer,
      ): IterableIterator<TSESLint.RuleFix> {
        if (isLogicalOrOperator(node.parent)) {
          // '&&' and '??' operations cannot be mixed without parentheses (e.g. a && b ?? c)
          if (
            node.left.type === AST_NODE_TYPES.LogicalExpression &&
            !isLogicalOrOperator(node.left.left)
          ) {
            yield fixer.insertTextBefore(node.left.right, '(');
          } else {
            yield fixer.insertTextBefore(node.left, '(');
          }
          yield fixer.insertTextAfter(node.right, ')');
        }
        yield fixer.replaceText(
          barBarOperator,
          node.operator.replace('||', '??'),
        );
      }

getNullishCoalescingParams(…): | { isFixable: false } | { isFixable: true; nullishCoalesci…

Parameters:

  • node TSESTree.ConditionalExpression | TSESTree.IfStatement
  • nonNullishNode TSESTree.Expression
  • nodesInsideTestExpression TSESTree.Node[]
  • operator NullishCheckOperator

Returns: | { isFixable: false } | { isFixable: true; nullishCoalescingLeftNode: TSESTree.Node }

Calls:

  • areNodesSimilarMemberAccess
  • isNullLiteral (from ../util)
  • isUndefinedIdentifier (from ../util)
  • complex_call_14270
  • isTruthinessCheckEligibleForPreferNullish
  • ['==', '!='].includes
  • parserServices.getTypeAtLocation
  • getTypeFlags (from ../util)

Internal Comments:

// we check that the test only contains null, undefined and the identifier
// Only consider the first expression in a multi-part nullish check, (x3)
// as subsequent expressions might not require all the optional chaining operators. (x3)
// For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo'; (x3)
// This works because `node.test` is always evaluated first in the loop (x3)
// and has the same or more necessary optional chaining operators (x3)
// than `node.alternate` or `node.consequent`. (x3)
// it is fixable if we check for both null and undefined, or not if neither
// it is fixable if we loosely check for either null or undefined
// it is fixable if we check for undefined and the type is not nullable
// it is fixable if we check for null and the type can't be undefined

Code
function getNullishCoalescingParams(
      node: TSESTree.ConditionalExpression | TSESTree.IfStatement,
      nonNullishNode: TSESTree.Expression,
      nodesInsideTestExpression: TSESTree.Node[],
      operator: NullishCheckOperator,
    ):
      | { isFixable: false }
      | { isFixable: true; nullishCoalescingLeftNode: TSESTree.Node } {
      let nullishCoalescingLeftNode: TSESTree.Node | undefined;
      let hasTruthinessCheck = false;
      let hasNullCheckWithoutTruthinessCheck = false;
      let hasUndefinedCheckWithoutTruthinessCheck = false;

      if (!nodesInsideTestExpression.length) {
        hasTruthinessCheck = true;
        nullishCoalescingLeftNode =
          node.test.type === AST_NODE_TYPES.UnaryExpression
            ? node.test.argument
            : node.test;

        if (
          !areNodesSimilarMemberAccess(
            nullishCoalescingLeftNode,
            nonNullishNode,
          )
        ) {
          return { isFixable: false };
        }
      } else {
        // we check that the test only contains null, undefined and the identifier
        for (const testNode of nodesInsideTestExpression) {
          if (isNullLiteral(testNode)) {
            hasNullCheckWithoutTruthinessCheck = true;
          } else if (isUndefinedIdentifier(testNode)) {
            hasUndefinedCheckWithoutTruthinessCheck = true;
          } else if (areNodesSimilarMemberAccess(testNode, nonNullishNode)) {
            // Only consider the first expression in a multi-part nullish check,
            // as subsequent expressions might not require all the optional chaining operators.
            // For example: a?.b?.c !== undefined && a.b.c !== null ? a.b.c : 'foo';
            // This works because `node.test` is always evaluated first in the loop
            // and has the same or more necessary optional chaining operators
            // than `node.alternate` or `node.consequent`.
            nullishCoalescingLeftNode ??= testNode;
          } else {
            return { isFixable: false };
          }
        }
      }

      if (!nullishCoalescingLeftNode) {
        return { isFixable: false };
      }

      const isFixable = ((): boolean => {
        if (hasTruthinessCheck) {
          return isTruthinessCheckEligibleForPreferNullish({
            node,
            testNode: nullishCoalescingLeftNode,
          });
        }

        // it is fixable if we check for both null and undefined, or not if neither
        if (
          hasUndefinedCheckWithoutTruthinessCheck ===
          hasNullCheckWithoutTruthinessCheck
        ) {
          return hasUndefinedCheckWithoutTruthinessCheck;
        }

        // it is fixable if we loosely check for either null or undefined
        if (['==', '!='].includes(operator)) {
          return true;
        }

        const type = parserServices.getTypeAtLocation(
          nullishCoalescingLeftNode,
        );
        const flags = getTypeFlags(type);

        if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) {
          return false;
        }

        const hasNullType = (flags & ts.TypeFlags.Null) !== 0;

        // it is fixable if we check for undefined and the type is not nullable
        if (hasUndefinedCheckWithoutTruthinessCheck && !hasNullType) {
          return true;
        }

        const hasUndefinedType = (flags & ts.TypeFlags.Undefined) !== 0;

        // it is fixable if we check for null and the type can't be undefined
        return hasNullCheckWithoutTruthinessCheck && !hasUndefinedType;
      })();

      return isFixable
        ? { isFixable: true, nullishCoalescingLeftNode }
        : { isFixable: false };
    }

Type Aliases

NullishCheckOperator

type NullishCheckOperator = '!' | '!=' | '!==' | '' | '==' | '===';

Options

type Options = [
  {
    allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
    ignoreBooleanCoercion?: boolean;
    ignoreConditionalTests?: boolean;
    ignoreIfStatements?: boolean;
    ignoreMixedLogicalExpressions?: boolean;
    ignorePrimitives?:
      | true
      | {
          bigint?: boolean;
          boolean?: boolean;
          number?: boolean;
          string?: boolean;
        };
    ignoreTernaryTests?: boolean;
  },
];

MessageIds

type MessageIds = | 'noStrictNullCheck'
  | 'preferNullishOverAssignment'
  | 'preferNullishOverOr'
  | 'preferNullishOverTernary'
  | 'suggestNullish';

Generated by Syntax Scribe