Skip to content

⬅️ Back to Table of Contents

πŸ“„ restrict-plus-operands

πŸ“Š Analysis Summary

Metric Count
πŸ”§ Functions 5
πŸ“¦ Imports 7
πŸ“‘ Type Aliases 2

πŸ“š Table of Contents

πŸ› οΈ File Location:

πŸ“‚ packages/eslint-plugin/src/rules/restrict-plus-operands.ts

πŸ“€ Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'restrict-plus-operands'
meta.type 'problem'
meta.docs.description 'Require both operands of addition to be the same type and be bigint, number, or string'
meta.docs.recommended.recommended true
meta.docs.recommended.strict [ { allowAny: false, allowBoolean: false, allowNullish: false, allowNumberAndString: false, allowRegExp: false, }, ]
meta.docs.requiresTypeChecking true
meta.messages.bigintAndNumber "Numeric '+' operations must either be both bigints or both numbers. Got {{left}} + {{right}}."
meta.messages.invalid "Invalid operand for a '+' operation. Operands must each be a number or {{stringLike}}. Got {{type}}."
meta.messages.mismatched "Operands of '+' operations must be a number or {{stringLike}}. Got {{left}} + {{right}}."
meta.schema [ { type: 'object', additionalProperties: false, properties: { allowAny: { type: 'boolean', description: 'Whether to ...
defaultOptions [ { allowAny: true, allowBoolean: true, allowNullish: true, allowNumberAndString: true, allowRegExp: true, skipCompou...

Entry point: create β€” documented under Functions.


πŸ“¦ Imports

Name Source
TSESTree @typescript-eslint/utils
createRule ../util
getConstrainedTypeAtLocation ../util
getParserServices ../util
getTypeName ../util
isTypeAnyType ../util
isTypeFlagSet ../util

Functions

create(context: any, [ { allowAny, allowBoolean, all…: any): { "AssignmentExpression[operator='+=']"(node: any): void; "…

Parameters:

  • context any
  • [ { allowAny, allowBoolean, allowNullish, allowNumberAndString, allowRegExp, skipCompoundAssignments, }, ] any

Returns: { "AssignmentExpression[operator='+=']"(node: any): void; "BinaryExpression[operator='+']": (node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression) => void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • [ allowAny && 'any', allowBoolean && 'boolean', allowNullish && 'null', allowRegExp && 'RegExp', allowNullish && 'undefined', ].filter
  • stringLikes.join
  • typeChecker.getBaseTypeOfLiteralType
  • getConstrainedTypeAtLocation (from ../util)
  • getTypeConstrained
  • tsutils.isTypeFlagSet
  • isTypeFlagSetInUnion
  • isTypeFlagSet (from ../util)
  • context.report
  • typeChecker.typeToString
  • tsutils.unionConstituents
  • getTypeName (from ../util)
  • isTypeAnyType (from ../util)
  • isDeeplyObjectType
  • checkPlusOperands

Internal Comments:

// RegExps also contain ts.TypeFlags.Any & ts.TypeFlags.Object

Code
create(
    context,
    [
      {
        allowAny,
        allowBoolean,
        allowNullish,
        allowNumberAndString,
        allowRegExp,
        skipCompoundAssignments,
      },
    ],
  ) {
    const services = getParserServices(context);
    const typeChecker = services.program.getTypeChecker();

    const stringLikes = [
      allowAny && '`any`',
      allowBoolean && '`boolean`',
      allowNullish && '`null`',
      allowRegExp && '`RegExp`',
      allowNullish && '`undefined`',
    ].filter((value): value is string => typeof value === 'string');
    const stringLike = stringLikes.length
      ? stringLikes.length === 1
        ? `string, allowing a string + ${stringLikes[0]}`
        : `string, allowing a string + any of: ${stringLikes.join(', ')}`
      : 'string';

    function getTypeConstrained(node: TSESTree.Node): ts.Type {
      return typeChecker.getBaseTypeOfLiteralType(
        getConstrainedTypeAtLocation(services, node),
      );
    }

    function checkPlusOperands(
      node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
    ): void {
      const leftType = getTypeConstrained(node.left);
      const rightType = getTypeConstrained(node.right);

      if (
        leftType === rightType &&
        tsutils.isTypeFlagSet(
          leftType,
          ts.TypeFlags.BigIntLike |
            ts.TypeFlags.NumberLike |
            ts.TypeFlags.StringLike,
        )
      ) {
        return;
      }

      let hadIndividualComplaint = false;

      for (const [baseNode, baseType, otherType] of [
        [node.left, leftType, rightType],
        [node.right, rightType, leftType],
      ] as const) {
        if (
          isTypeFlagSetInUnion(
            baseType,
            ts.TypeFlags.ESSymbolLike |
              ts.TypeFlags.Never |
              ts.TypeFlags.Unknown,
          ) ||
          (!allowAny && isTypeFlagSetInUnion(baseType, ts.TypeFlags.Any)) ||
          (!allowBoolean &&
            isTypeFlagSetInUnion(baseType, ts.TypeFlags.BooleanLike)) ||
          (!allowNullish &&
            isTypeFlagSet(baseType, ts.TypeFlags.Null | ts.TypeFlags.Undefined))
        ) {
          context.report({
            node: baseNode,
            messageId: 'invalid',
            data: {
              type: typeChecker.typeToString(baseType),
              stringLike,
            },
          });
          hadIndividualComplaint = true;
          continue;
        }

        // RegExps also contain ts.TypeFlags.Any & ts.TypeFlags.Object
        for (const subBaseType of tsutils.unionConstituents(baseType)) {
          const typeName = getTypeName(typeChecker, subBaseType);
          if (
            typeName === 'RegExp'
              ? !allowRegExp ||
                tsutils.isTypeFlagSet(otherType, ts.TypeFlags.NumberLike)
              : (!allowAny && isTypeAnyType(subBaseType)) ||
                isDeeplyObjectType(subBaseType)
          ) {
            context.report({
              node: baseNode,
              messageId: 'invalid',
              data: {
                type: typeChecker.typeToString(subBaseType),
                stringLike,
              },
            });
            hadIndividualComplaint = true;
            continue;
          }
        }
      }

      if (hadIndividualComplaint) {
        return;
      }

      for (const [baseType, otherType] of [
        [leftType, rightType],
        [rightType, leftType],
      ] as const) {
        if (
          !allowNumberAndString &&
          isTypeFlagSetInUnion(baseType, ts.TypeFlags.StringLike) &&
          isTypeFlagSetInUnion(
            otherType,
            ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
          )
        ) {
          return context.report({
            node,
            messageId: 'mismatched',
            data: {
              left: typeChecker.typeToString(leftType),
              right: typeChecker.typeToString(rightType),
              stringLike,
            },
          });
        }

        if (
          isTypeFlagSetInUnion(baseType, ts.TypeFlags.NumberLike) &&
          isTypeFlagSetInUnion(otherType, ts.TypeFlags.BigIntLike)
        ) {
          return context.report({
            node,
            messageId: 'bigintAndNumber',
            data: {
              left: typeChecker.typeToString(leftType),
              right: typeChecker.typeToString(rightType),
            },
          });
        }
      }
    }

    return {
      "BinaryExpression[operator='+']": checkPlusOperands,
      ...(!skipCompoundAssignments && {
        "AssignmentExpression[operator='+=']"(node): void {
          checkPlusOperands(node);
        },
      }),
    };
  }

isDeeplyObjectType(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • type.isIntersection
  • tsutils.intersectionConstituents(type).every
  • tsutils.unionConstituents(type).every
Code
function isDeeplyObjectType(type: ts.Type): boolean {
  return type.isIntersection()
    ? tsutils.intersectionConstituents(type).every(tsutils.isObjectType)
    : tsutils.unionConstituents(type).every(tsutils.isObjectType);
}

isTypeFlagSetInUnion(type: ts.Type, flag: ts.TypeFlags): boolean

Parameters:

  • type ts.Type
  • flag ts.TypeFlags

Returns: boolean

Calls:

  • tsutils .unionConstituents(type) .some
  • tsutils.isTypeFlagSet
Code
function isTypeFlagSetInUnion(type: ts.Type, flag: ts.TypeFlags): boolean {
  return tsutils
    .unionConstituents(type)
    .some(subType => tsutils.isTypeFlagSet(subType, flag));
}

Internal helpers

Declared inside another function in this file.

getTypeConstrained(node: TSESTree.Node): ts.Type

Parameters:

  • node TSESTree.Node

Returns: ts.Type

Calls:

  • typeChecker.getBaseTypeOfLiteralType
  • getConstrainedTypeAtLocation (from ../util)
Code
function getTypeConstrained(node: TSESTree.Node): ts.Type {
      return typeChecker.getBaseTypeOfLiteralType(
        getConstrainedTypeAtLocation(services, node),
      );
    }

checkPlusOperands(node: TSESTree.AssignmentExpression | TSESTre…): void

Parameters:

  • node TSESTree.AssignmentExpression | TSESTree.BinaryExpression

Returns: void

Calls:

  • getTypeConstrained
  • tsutils.isTypeFlagSet
  • isTypeFlagSetInUnion
  • isTypeFlagSet (from ../util)
  • context.report
  • typeChecker.typeToString
  • tsutils.unionConstituents
  • getTypeName (from ../util)
  • isTypeAnyType (from ../util)
  • isDeeplyObjectType

Internal Comments:

// RegExps also contain ts.TypeFlags.Any & ts.TypeFlags.Object

Code
function checkPlusOperands(
      node: TSESTree.AssignmentExpression | TSESTree.BinaryExpression,
    ): void {
      const leftType = getTypeConstrained(node.left);
      const rightType = getTypeConstrained(node.right);

      if (
        leftType === rightType &&
        tsutils.isTypeFlagSet(
          leftType,
          ts.TypeFlags.BigIntLike |
            ts.TypeFlags.NumberLike |
            ts.TypeFlags.StringLike,
        )
      ) {
        return;
      }

      let hadIndividualComplaint = false;

      for (const [baseNode, baseType, otherType] of [
        [node.left, leftType, rightType],
        [node.right, rightType, leftType],
      ] as const) {
        if (
          isTypeFlagSetInUnion(
            baseType,
            ts.TypeFlags.ESSymbolLike |
              ts.TypeFlags.Never |
              ts.TypeFlags.Unknown,
          ) ||
          (!allowAny && isTypeFlagSetInUnion(baseType, ts.TypeFlags.Any)) ||
          (!allowBoolean &&
            isTypeFlagSetInUnion(baseType, ts.TypeFlags.BooleanLike)) ||
          (!allowNullish &&
            isTypeFlagSet(baseType, ts.TypeFlags.Null | ts.TypeFlags.Undefined))
        ) {
          context.report({
            node: baseNode,
            messageId: 'invalid',
            data: {
              type: typeChecker.typeToString(baseType),
              stringLike,
            },
          });
          hadIndividualComplaint = true;
          continue;
        }

        // RegExps also contain ts.TypeFlags.Any & ts.TypeFlags.Object
        for (const subBaseType of tsutils.unionConstituents(baseType)) {
          const typeName = getTypeName(typeChecker, subBaseType);
          if (
            typeName === 'RegExp'
              ? !allowRegExp ||
                tsutils.isTypeFlagSet(otherType, ts.TypeFlags.NumberLike)
              : (!allowAny && isTypeAnyType(subBaseType)) ||
                isDeeplyObjectType(subBaseType)
          ) {
            context.report({
              node: baseNode,
              messageId: 'invalid',
              data: {
                type: typeChecker.typeToString(subBaseType),
                stringLike,
              },
            });
            hadIndividualComplaint = true;
            continue;
          }
        }
      }

      if (hadIndividualComplaint) {
        return;
      }

      for (const [baseType, otherType] of [
        [leftType, rightType],
        [rightType, leftType],
      ] as const) {
        if (
          !allowNumberAndString &&
          isTypeFlagSetInUnion(baseType, ts.TypeFlags.StringLike) &&
          isTypeFlagSetInUnion(
            otherType,
            ts.TypeFlags.NumberLike | ts.TypeFlags.BigIntLike,
          )
        ) {
          return context.report({
            node,
            messageId: 'mismatched',
            data: {
              left: typeChecker.typeToString(leftType),
              right: typeChecker.typeToString(rightType),
              stringLike,
            },
          });
        }

        if (
          isTypeFlagSetInUnion(baseType, ts.TypeFlags.NumberLike) &&
          isTypeFlagSetInUnion(otherType, ts.TypeFlags.BigIntLike)
        ) {
          return context.report({
            node,
            messageId: 'bigintAndNumber',
            data: {
              left: typeChecker.typeToString(leftType),
              right: typeChecker.typeToString(rightType),
            },
          });
        }
      }
    }

Type Aliases

Options

type Options = [
  {
    allowAny?: boolean;
    allowBoolean?: boolean;
    allowNullish?: boolean;
    allowNumberAndString?: boolean;
    allowRegExp?: boolean;
    skipCompoundAssignments?: boolean;
  },
];

MessageIds

type MessageIds = 'bigintAndNumber' | 'invalid' | 'mismatched';

Generated by Syntax Scribe