Skip to content

⬅️ Back to Table of Contents

📄 no-misused-spread

📊 Analysis Summary

Metric Count
🔧 Functions 15
📦 Imports 14
📑 Type Aliases 2

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/no-misused-spread.ts

📤 Default Export

export default createRule<Options, MessageIds>({ ... })
Property Value
name 'no-misused-spread'
meta.type 'problem'
meta.docs.description 'Disallow using the spread operator when it might cause unexpected behavior'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.hasSuggestions true
meta.messages.addAwait 'Add await operator.'
meta.messages.noArraySpreadInObject 'Using the spread operator on an array in an object will result in a list of indices.'
meta.messages.noClassDeclarationSpreadInObject 'Using the spread operator on class declarations will spread only their static properties, and will lose their class ...
meta.messages.noClassInstanceSpreadInObject 'Using the spread operator on class instances will lose their class prototype.'
meta.messages.noFunctionSpreadInObject 'Using the spread operator on a function without additional properties can cause unexpected behavior. Did you forget ...
meta.messages.noIterableSpreadInObject 'Using the spread operator on an Iterable in an object can cause unexpected behavior.'
meta.messages.noMapSpreadInObject 'Using the spread operator on a Map in an object will result in an empty object. Did you mean to use `Object.fromEntr...
meta.messages.noPromiseSpreadInObject 'Using the spread operator on Promise in an object can cause unexpected behavior. Did you forget to await the promise?'
meta.messages.noStringSpread [ 'Using the spread operator on a string can mishandle special characters, as can .split("").', '- ... produces U...
meta.messages.replaceMapSpreadInObject 'Replace map spread in object with Object.fromEntries()'
meta.schema [ { type: 'object', additionalProperties: false, properties: { allow: { ...readonlynessOptionsSchema.properties.allow...
defaultOptions [ { allow: [], }, ]

Entry point: create — documented under Functions.


📦 Imports

Name Source
TSESLint @typescript-eslint/utils
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
TypeOrValueSpecifier ../util
createRule ../util
getConstrainedTypeAtLocation ../util
getParserServices ../util
getWrappingFixer ../util
isBuiltinSymbolLike ../util
isPromiseLike ../util
isTypeFlagSet ../util
readonlynessOptionsSchema ../util
typeMatchesSomeSpecifier ../util
isHigherPrecedenceThanAwait ../util

Functions

create(context: any, [options]: any): { 'ArrayExpression > SpreadElement': (node: TSESTree.Spread…

Parameters:

  • context any
  • [options] any

Returns: { 'ArrayExpression > SpreadElement': (node: TSESTree.SpreadElement) => void; 'CallExpression > SpreadElement': (node: TSESTree.SpreadElement) => void; JSXSpreadAttribute: (node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement) => void; 'ObjectExpression > SpreadElement': (node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement) => void; }

Calls:

  • getParserServices (from ../util)
  • services.program.getTypeChecker
  • getConstrainedTypeAtLocation (from ../util)
  • typeMatchesSomeSpecifier (from ../util)
  • isString
  • context.report
  • tsutils.unionConstituents
  • types.some
  • isMap
  • getWrappingFixer (from ../util)
  • isHigherPrecedenceThanAwait (from ../util)
  • services.esTreeNodeToTSNodeMap.get
  • fixer.insertTextBefore
  • fixer.insertTextAfter
  • isPromise
  • getPromiseSpreadSuggestions
  • isFunctionWithoutProps
  • getMapSpreadSuggestions
  • isArray
  • isIterable
  • isClassInstance
  • isClassDeclaration

Internal Comments:

// Don't report when the type is string, since TS will flag it already

Code
create(context, [options]) {
    const services = getParserServices(context);
    const checker = services.program.getTypeChecker();

    function checkArrayOrCallSpread(node: TSESTree.SpreadElement): void {
      const type = getConstrainedTypeAtLocation(services, node.argument);

      if (
        !typeMatchesSomeSpecifier(type, options.allow, services.program) &&
        isString(type)
      ) {
        context.report({
          node,
          messageId: 'noStringSpread',
        });
      }
    }

    function getMapSpreadSuggestions(
      node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
      type: ts.Type,
    ): TSESLint.ReportSuggestionArray<MessageIds> | null {
      const types = tsutils.unionConstituents(type);
      if (types.some(t => !isMap(services.program, t))) {
        return null;
      }

      if (
        node.parent.type === AST_NODE_TYPES.ObjectExpression &&
        node.parent.properties.length === 1
      ) {
        return [
          {
            messageId: 'replaceMapSpreadInObject',
            fix: getWrappingFixer({
              node: node.parent,
              innerNode: node.argument,
              sourceCode: context.sourceCode,
              wrap: code => `Object.fromEntries(${code})`,
            }),
          },
        ];
      }

      return [
        {
          messageId: 'replaceMapSpreadInObject',
          fix: getWrappingFixer({
            node: node.argument,
            sourceCode: context.sourceCode,
            wrap: code => `Object.fromEntries(${code})`,
          }),
        },
      ];
    }

    function getPromiseSpreadSuggestions(
      node: TSESTree.Expression,
    ): TSESLint.ReportSuggestionArray<MessageIds> {
      const isHighPrecedence = isHigherPrecedenceThanAwait(
        services.esTreeNodeToTSNodeMap.get(node),
      );

      return [
        {
          messageId: 'addAwait',
          fix: fixer =>
            isHighPrecedence
              ? fixer.insertTextBefore(node, 'await ')
              : [
                  fixer.insertTextBefore(node, 'await ('),
                  fixer.insertTextAfter(node, ')'),
                ],
        },
      ];
    }

    function checkObjectSpread(
      node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
    ): void {
      const type = getConstrainedTypeAtLocation(services, node.argument);

      if (typeMatchesSomeSpecifier(type, options.allow, services.program)) {
        return;
      }

      if (isPromise(services.program, type)) {
        context.report({
          node,
          messageId: 'noPromiseSpreadInObject',
          suggest: getPromiseSpreadSuggestions(node.argument),
        });

        return;
      }

      if (isFunctionWithoutProps(type)) {
        context.report({
          node,
          messageId: 'noFunctionSpreadInObject',
        });

        return;
      }

      if (isMap(services.program, type)) {
        context.report({
          node,
          messageId: 'noMapSpreadInObject',
          suggest: getMapSpreadSuggestions(node, type),
        });

        return;
      }

      if (isArray(checker, type)) {
        context.report({
          node,
          messageId: 'noArraySpreadInObject',
        });

        return;
      }

      if (
        isIterable(type, checker) &&
        // Don't report when the type is string, since TS will flag it already
        !isString(type)
      ) {
        context.report({
          node,
          messageId: 'noIterableSpreadInObject',
        });

        return;
      }

      if (isClassInstance(checker, type)) {
        context.report({
          node,
          messageId: 'noClassInstanceSpreadInObject',
        });

        return;
      }

      if (isClassDeclaration(type)) {
        context.report({
          node,
          messageId: 'noClassDeclarationSpreadInObject',
        });
      }
    }

    return {
      'ArrayExpression > SpreadElement': checkArrayOrCallSpread,
      'CallExpression > SpreadElement': checkArrayOrCallSpread,
      JSXSpreadAttribute: checkObjectSpread,
      'ObjectExpression > SpreadElement': checkObjectSpread,
    };
  }

isIterable(type: ts.Type, checker: ts.TypeChecker): boolean

Parameters:

  • type ts.Type
  • checker ts.TypeChecker

Returns: boolean

Calls:

  • tsutils .typeConstituents(type) .some
  • tsutils.getWellKnownSymbolPropertyOfType
Code
function isIterable(type: ts.Type, checker: ts.TypeChecker): boolean {
  return tsutils
    .typeConstituents(type)
    .some(
      t => !!tsutils.getWellKnownSymbolPropertyOfType(t, 'iterator', checker),
    );
}

isArray(checker: ts.TypeChecker, type: ts.Type): boolean

Parameters:

  • checker ts.TypeChecker
  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • checker.isArrayType
  • checker.isTupleType
Code
function isArray(checker: ts.TypeChecker, type: ts.Type): boolean {
  return isTypeRecurser(
    type,
    t => checker.isArrayType(t) || checker.isTupleType(t),
  );
}

isString(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • isTypeFlagSet (from ../util)
Code
function isString(type: ts.Type): boolean {
  return isTypeRecurser(type, t => isTypeFlagSet(t, ts.TypeFlags.StringLike));
}

isFunctionWithoutProps(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • t.getCallSignatures
  • t.getProperties
Code
function isFunctionWithoutProps(type: ts.Type): boolean {
  return isTypeRecurser(
    type,
    t => t.getCallSignatures().length > 0 && t.getProperties().length === 0,
  );
}

isPromise(program: ts.Program, type: ts.Type): boolean

Parameters:

  • program ts.Program
  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • isPromiseLike (from ../util)
Code
function isPromise(program: ts.Program, type: ts.Type): boolean {
  return isTypeRecurser(type, t => isPromiseLike(program, t));
}

isClassInstance(checker: ts.TypeChecker, type: ts.Type): boolean

Parameters:

  • checker ts.TypeChecker
  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • t.getConstructSignatures
  • t.getSymbol
  • symbol ?.getDeclarations() ?.some
  • checker .getTypeOfSymbolAtLocation(symbol, declaration) .getConstructSignatures

Internal Comments:

// If the type itself has a construct signature, it's a class(-like)
// If the type's symbol has a construct signature, the type is an instance

Code
function isClassInstance(checker: ts.TypeChecker, type: ts.Type): boolean {
  return isTypeRecurser(type, t => {
    // If the type itself has a construct signature, it's a class(-like)
    if (t.getConstructSignatures().length) {
      return false;
    }

    const symbol = t.getSymbol();

    // If the type's symbol has a construct signature, the type is an instance
    return !!symbol
      ?.getDeclarations()
      ?.some(
        declaration =>
          checker
            .getTypeOfSymbolAtLocation(symbol, declaration)
            .getConstructSignatures().length,
      );
  });
}

isClassDeclaration(type: ts.Type): boolean

Parameters:

  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • tsutils.isObjectType
  • tsutils.isObjectFlagSet
  • t.getSymbol
Code
function isClassDeclaration(type: ts.Type): boolean {
  return isTypeRecurser(type, t => {
    if (
      tsutils.isObjectType(t) &&
      tsutils.isObjectFlagSet(t, ts.ObjectFlags.InstantiationExpressionType)
    ) {
      return true;
    }

    const kind = t.getSymbol()?.valueDeclaration?.kind;

    return (
      kind === ts.SyntaxKind.ClassDeclaration ||
      kind === ts.SyntaxKind.ClassExpression
    );
  });
}

isMap(program: ts.Program, type: ts.Type): boolean

Parameters:

  • program ts.Program
  • type ts.Type

Returns: boolean

Calls:

  • isTypeRecurser
  • isBuiltinSymbolLike (from ../util)
Code
function isMap(program: ts.Program, type: ts.Type): boolean {
  return isTypeRecurser(type, t =>
    isBuiltinSymbolLike(program, t, ['Map', 'ReadonlyMap', 'WeakMap']),
  );
}

isTypeRecurser(type: ts.Type, predicate: (t: ts.Type) => boolean): boolean

Parameters:

  • type ts.Type
  • predicate (t: ts.Type) => boolean

Returns: boolean

Calls:

  • type.isUnionOrIntersection
  • type.types.some
  • isTypeRecurser
  • predicate
Code
function isTypeRecurser(
  type: ts.Type,
  predicate: (t: ts.Type) => boolean,
): boolean {
  if (type.isUnionOrIntersection()) {
    return type.types.some(t => isTypeRecurser(t, predicate));
  }

  return predicate(type);
}

Internal helpers

Declared inside another function in this file.

checkArrayOrCallSpread(node: TSESTree.SpreadElement): void

Parameters:

  • node TSESTree.SpreadElement

Returns: void

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • typeMatchesSomeSpecifier (from ../util)
  • isString
  • context.report
Code
function checkArrayOrCallSpread(node: TSESTree.SpreadElement): void {
      const type = getConstrainedTypeAtLocation(services, node.argument);

      if (
        !typeMatchesSomeSpecifier(type, options.allow, services.program) &&
        isString(type)
      ) {
        context.report({
          node,
          messageId: 'noStringSpread',
        });
      }
    }

getMapSpreadSuggestions(node: TSESTree.JSXSpreadAttribute | TSESTree.…, type: ts.Type): TSESLint.ReportSuggestionArray<MessageIds> | null

Parameters:

  • node TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement
  • type ts.Type

Returns: TSESLint.ReportSuggestionArray<MessageIds> | null

Calls:

  • tsutils.unionConstituents
  • types.some
  • isMap
  • getWrappingFixer (from ../util)
Code
function getMapSpreadSuggestions(
      node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
      type: ts.Type,
    ): TSESLint.ReportSuggestionArray<MessageIds> | null {
      const types = tsutils.unionConstituents(type);
      if (types.some(t => !isMap(services.program, t))) {
        return null;
      }

      if (
        node.parent.type === AST_NODE_TYPES.ObjectExpression &&
        node.parent.properties.length === 1
      ) {
        return [
          {
            messageId: 'replaceMapSpreadInObject',
            fix: getWrappingFixer({
              node: node.parent,
              innerNode: node.argument,
              sourceCode: context.sourceCode,
              wrap: code => `Object.fromEntries(${code})`,
            }),
          },
        ];
      }

      return [
        {
          messageId: 'replaceMapSpreadInObject',
          fix: getWrappingFixer({
            node: node.argument,
            sourceCode: context.sourceCode,
            wrap: code => `Object.fromEntries(${code})`,
          }),
        },
      ];
    }

getPromiseSpreadSuggestions(node: TSESTree.Expression): TSESLint.ReportSuggestionArray<MessageIds>

Parameters:

  • node TSESTree.Expression

Returns: TSESLint.ReportSuggestionArray<MessageIds>

Calls:

  • isHigherPrecedenceThanAwait (from ../util)
  • services.esTreeNodeToTSNodeMap.get
  • fixer.insertTextBefore
  • fixer.insertTextAfter
Code
function getPromiseSpreadSuggestions(
      node: TSESTree.Expression,
    ): TSESLint.ReportSuggestionArray<MessageIds> {
      const isHighPrecedence = isHigherPrecedenceThanAwait(
        services.esTreeNodeToTSNodeMap.get(node),
      );

      return [
        {
          messageId: 'addAwait',
          fix: fixer =>
            isHighPrecedence
              ? fixer.insertTextBefore(node, 'await ')
              : [
                  fixer.insertTextBefore(node, 'await ('),
                  fixer.insertTextAfter(node, ')'),
                ],
        },
      ];
    }

fix(fixer: any): any

Parameters:

  • fixer any

Returns: any

Code
fixer =>
            isHighPrecedence
              ? fixer.insertTextBefore(node, 'await ')
              : [
                  fixer.insertTextBefore(node, 'await ('),
                  fixer.insertTextAfter(node, ')'),
                ]

checkObjectSpread(node: TSESTree.JSXSpreadAttribute | TSESTree.…): void

Parameters:

  • node TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement

Returns: void

Calls:

  • getConstrainedTypeAtLocation (from ../util)
  • typeMatchesSomeSpecifier (from ../util)
  • isPromise
  • context.report
  • getPromiseSpreadSuggestions
  • isFunctionWithoutProps
  • isMap
  • getMapSpreadSuggestions
  • isArray
  • isIterable
  • isString
  • isClassInstance
  • isClassDeclaration

Internal Comments:

// Don't report when the type is string, since TS will flag it already

Code
function checkObjectSpread(
      node: TSESTree.JSXSpreadAttribute | TSESTree.SpreadElement,
    ): void {
      const type = getConstrainedTypeAtLocation(services, node.argument);

      if (typeMatchesSomeSpecifier(type, options.allow, services.program)) {
        return;
      }

      if (isPromise(services.program, type)) {
        context.report({
          node,
          messageId: 'noPromiseSpreadInObject',
          suggest: getPromiseSpreadSuggestions(node.argument),
        });

        return;
      }

      if (isFunctionWithoutProps(type)) {
        context.report({
          node,
          messageId: 'noFunctionSpreadInObject',
        });

        return;
      }

      if (isMap(services.program, type)) {
        context.report({
          node,
          messageId: 'noMapSpreadInObject',
          suggest: getMapSpreadSuggestions(node, type),
        });

        return;
      }

      if (isArray(checker, type)) {
        context.report({
          node,
          messageId: 'noArraySpreadInObject',
        });

        return;
      }

      if (
        isIterable(type, checker) &&
        // Don't report when the type is string, since TS will flag it already
        !isString(type)
      ) {
        context.report({
          node,
          messageId: 'noIterableSpreadInObject',
        });

        return;
      }

      if (isClassInstance(checker, type)) {
        context.report({
          node,
          messageId: 'noClassInstanceSpreadInObject',
        });

        return;
      }

      if (isClassDeclaration(type)) {
        context.report({
          node,
          messageId: 'noClassDeclarationSpreadInObject',
        });
      }
    }

Type Aliases

Options

type Options = [
  {
    allow?: TypeOrValueSpecifier[];
  },
];

MessageIds

type MessageIds = | 'addAwait'
  | 'noArraySpreadInObject'
  | 'noClassDeclarationSpreadInObject'
  | 'noClassInstanceSpreadInObject'
  | 'noFunctionSpreadInObject'
  | 'noIterableSpreadInObject'
  | 'noMapSpreadInObject'
  | 'noPromiseSpreadInObject'
  | 'noStringSpread'
  | 'replaceMapSpreadInObject';

Generated by Syntax Scribe