Skip to content

⬅️ Back to Table of Contents

πŸ“„ prefer-optional-chain

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 3
πŸ“¦ Imports 14

πŸ“š Table of Contents

πŸ› οΈ File Location:

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

πŸ“€ Default Export

export default createRule<[PreferOptionalChainOptions], PreferOptionalChainMessageIds>({ ... })
Property Value
name 'prefer-optional-chain'
meta.type 'suggestion'
meta.docs.description 'Enforce using concise optional chain expressions instead of chained logical ands, negated logical ors, or empty obje...
meta.docs.recommended 'stylistic'
meta.docs.requiresTypeChecking true
meta.fixable 'code'
meta.hasSuggestions true
meta.messages.optionalChainSuggest 'Change to an optional chain.'
meta.messages.preferOptionalChain "Prefer using an optional chain expression instead, as it's more concise and easier to read."
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKno...
defaultOptions [ { allowPotentiallyUnsafeFixesThatModifyTheReturnTypeIKnowWhatImDoing: false, checkAny: true, checkBigInt: true, che...

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
RuleFix @typescript-eslint/utils/ts-eslint
AST_NODE_TYPES @typescript-eslint/utils
ValidOperand ./prefer-optional-chain-utils/gatherLogicalOperands
PreferOptionalChainMessageIds ./prefer-optional-chain-utils/PreferOptionalChainOptions
PreferOptionalChainOptions ./prefer-optional-chain-utils/PreferOptionalChainOptions
createRule ../util
getOperatorPrecedence ../util
getParserServices ../util
OperatorPrecedence ../util
analyzeChain ./prefer-optional-chain-utils/analyzeChain
checkNullishAndReport ./prefer-optional-chain-utils/checkNullishAndReport
gatherLogicalOperands ./prefer-optional-chain-utils/gatherLogicalOperands
OperandValidity ./prefer-optional-chain-utils/gatherLogicalOperands

Functions

create(context: any, [options]: any): { 'LogicalExpression[operator!="??"]'(node: TSESTree.Logica…

Parameters:

  • context any
  • [options] any

Returns: { 'LogicalExpression[operator!="??"]'(node: TSESTree.LogicalExpression): void; 'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'(node: TSESTree.LogicalExpression): void; }

Calls:

  • getParserServices (from ../util)
  • seenLogicals.has
  • gatherLogicalOperands (from ./prefer-optional-chain-utils/gatherLogicalOperands)
  • seenLogicals.add
  • analyzeChain (from ./prefer-optional-chain-utils/analyzeChain)
  • currentChain.push
  • parserServices.esTreeNodeToTSNodeMap.get
  • getOperatorPrecedence (from ../util)
  • checkNullishAndReport (from ./prefer-optional-chain-utils/checkNullishAndReport)
  • context.sourceCode.getText
  • isLeftSideLowerPrecedence
  • fixer.replaceTextRange

Internal Comments:

// make sure to check whatever's left
// specific handling for `(foo ?? {}).bar` / `(foo || {}).bar` (x2)
// Any node that is made of an operator with higher or equal precedence, (x2)

Code
create(context, [options]) {
    const parserServices = getParserServices(context);

    const seenLogicals = new Set<TSESTree.LogicalExpression>();

    return {
      'LogicalExpression[operator!="??"]'(
        node: TSESTree.LogicalExpression,
      ): void {
        if (seenLogicals.has(node)) {
          return;
        }

        const { newlySeenLogicals, operands } = gatherLogicalOperands(
          node,
          parserServices,
          context.sourceCode,
          options,
        );
        for (const logical of newlySeenLogicals) {
          seenLogicals.add(logical);
        }

        let currentChain: ValidOperand[] = [];
        for (const operand of operands) {
          if (operand.type === OperandValidity.Invalid) {
            analyzeChain(
              context,
              parserServices,
              options,
              node,
              node.operator,
              currentChain,
            );
            currentChain = [];
          } else if (operand.type === OperandValidity.Last) {
            analyzeChain(
              context,
              parserServices,
              options,
              node,
              node.operator,
              currentChain,
              operand,
            );
            currentChain = [];
          } else {
            currentChain.push(operand);
          }
        }

        // make sure to check whatever's left
        if (currentChain.length > 0) {
          analyzeChain(
            context,
            parserServices,
            options,
            node,
            node.operator,
            currentChain,
          );
        }
      },

      // specific handling for `(foo ?? {}).bar` / `(foo || {}).bar`
      'LogicalExpression[operator="||"], LogicalExpression[operator="??"]'(
        node: TSESTree.LogicalExpression,
      ): void {
        const leftNode = node.left;
        const rightNode = node.right;
        const parentNode = node.parent;
        const isRightNodeAnEmptyObjectLiteral =
          rightNode.type === AST_NODE_TYPES.ObjectExpression &&
          rightNode.properties.length === 0;
        if (
          !isRightNodeAnEmptyObjectLiteral ||
          parentNode.type !== AST_NODE_TYPES.MemberExpression ||
          parentNode.optional
        ) {
          return;
        }

        seenLogicals.add(node);

        function isLeftSideLowerPrecedence(): boolean {
          const logicalTsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
          const leftTsNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode);
          const leftPrecedence = getOperatorPrecedence(
            leftTsNode.kind,
            logicalTsNode.operatorToken.kind,
          );

          return leftPrecedence < OperatorPrecedence.LeftHandSide;
        }

        checkNullishAndReport(context, parserServices, options, [leftNode], {
          node: parentNode,
          messageId: 'preferOptionalChain',
          suggest: [
            {
              messageId: 'optionalChainSuggest',
              fix: (fixer): RuleFix => {
                const leftNodeText = context.sourceCode.getText(leftNode);
                // Any node that is made of an operator with higher or equal precedence,
                const maybeWrappedLeftNode = isLeftSideLowerPrecedence()
                  ? `(${leftNodeText})`
                  : leftNodeText;
                const propertyToBeOptionalText = context.sourceCode.getText(
                  parentNode.property,
                );
                const maybeWrappedProperty = parentNode.computed
                  ? `[${propertyToBeOptionalText}]`
                  : propertyToBeOptionalText;
                return fixer.replaceTextRange(
                  parentNode.range,
                  `${maybeWrappedLeftNode}?.${maybeWrappedProperty}`,
                );
              },
            },
          ],
        });
      },
    };
  }

Internal helpers

Declared inside another function in this file.

isLeftSideLowerPrecedence(): boolean

Returns: boolean

Calls:

  • parserServices.esTreeNodeToTSNodeMap.get
  • getOperatorPrecedence (from ../util)
Code
function isLeftSideLowerPrecedence(): boolean {
          const logicalTsNode = parserServices.esTreeNodeToTSNodeMap.get(node);
          const leftTsNode = parserServices.esTreeNodeToTSNodeMap.get(leftNode);
          const leftPrecedence = getOperatorPrecedence(
            leftTsNode.kind,
            logicalTsNode.operatorToken.kind,
          );

          return leftPrecedence < OperatorPrecedence.LeftHandSide;
        }

fix(fixer: any): RuleFix

Parameters:

  • fixer any

Returns: RuleFix

Calls:

  • context.sourceCode.getText
  • isLeftSideLowerPrecedence
  • fixer.replaceTextRange

Internal Comments:

// Any node that is made of an operator with higher or equal precedence, (x2)

Code
(fixer): RuleFix => {
                const leftNodeText = context.sourceCode.getText(leftNode);
                // Any node that is made of an operator with higher or equal precedence,
                const maybeWrappedLeftNode = isLeftSideLowerPrecedence()
                  ? `(${leftNodeText})`
                  : leftNodeText;
                const propertyToBeOptionalText = context.sourceCode.getText(
                  parentNode.property,
                );
                const maybeWrappedProperty = parentNode.computed
                  ? `[${propertyToBeOptionalText}]`
                  : propertyToBeOptionalText;
                return fixer.replaceTextRange(
                  parentNode.range,
                  `${maybeWrappedLeftNode}?.${maybeWrappedProperty}`,
                );
              }

Generated by Syntax Scribe