Skip to content

⬅️ Back to Table of Contents

📄 no-mixed-enums

📊 Analysis Summary

Metric Count
🔧 Functions 9
📦 Imports 8
📐 Interfaces 1
🎯 Enums 1

📚 Table of Contents

🛠️ File Location:

📂 packages/eslint-plugin/src/rules/no-mixed-enums.ts

📤 Default Export

export default createRule({ ... })
Property Value
name 'no-mixed-enums'
meta.type 'problem'
meta.docs.description 'Disallow enums from having both number and string members'
meta.docs.recommended 'strict'
meta.docs.requiresTypeChecking true
meta.messages.mixed Mixing number and string enums can be confusing.
meta.schema []
defaultOptions []

Entry point: create — documented under Functions.


📦 Imports

Name Source
Scope @typescript-eslint/scope-manager
TSESTree @typescript-eslint/utils
DefinitionType @typescript-eslint/scope-manager
AST_NODE_TYPES @typescript-eslint/utils
createRule ../util
getParserServices ../util
nullThrows ../util
NullThrowsReasons ../util

Functions

create(context: any): { TSEnumDeclaration(node: any): void; }

Parameters:

  • context any

Returns: { TSEnumDeclaration(node: any): void; }

Calls:

  • getParserServices (from ../util)
  • parserServices.program.getTypeChecker
  • JSON.stringify
  • context.sourceCode.getText
  • getModuleName
  • getEnclosingScopes(block).flatMap
  • enclosing.childScopes.filter
  • nullThrows (from ../util)
  • context.sourceCode.getScope
  • getMergedScopes
  • getEnclosingScopes(node).flatMap
  • enclosing.set.get
  • scope.set.get(name)?.defs.forEach
  • found.imports.push
  • tsutils.isTypeFlagSet
  • typeChecker.getTypeAtLocation
  • parserServices.esTreeNodeToTSNodeMap.get
  • type.getSymbol
  • ts.isEnumDeclaration
  • getAllowedTypeForNode
  • collectNodeDefinitions
  • getTypeFromImported
  • getMemberType
  • getDesiredTypeForDefinition
  • context.report

Internal Comments:

// Case: Merged ambiently via module augmentation
// import { MyEnum } from 'other-module';
// declare module 'other-module' {
//   enum MyEnum { A }
// } (x3)
// Case: Multiple enum declarations in the same file
// enum MyEnum { A }
// enum MyEnum { B }
//
// ...including ones merged across namespace declarations
// namespace MyNamespace { (x2)
//   export enum MyEnum { A }
//   export enum MyEnum { B }
// Finally, we default to the type of the first enum member

Code
create(context) {
    const parserServices = getParserServices(context);
    const typeChecker = parserServices.program.getTypeChecker();

    interface CollectedDefinitions {
      imports: TSESTree.Node[];
      previousSibling: TSESTree.TSEnumDeclaration | undefined;
    }

    function getModuleName(id: TSESTree.TSModuleDeclaration['id']): string {
      if (id.type === AST_NODE_TYPES.Literal) {
        return JSON.stringify(id.value);
      }

      let qualifiers = '';
      let current: TSESTree.EntityName = id;
      while (current.type === AST_NODE_TYPES.TSQualifiedName) {
        qualifiers = `.${current.right.name}${qualifiers}`;
        current = current.left;
      }

      return context.sourceCode.getText(current) + qualifiers;
    }

    function getMergedScopes(scope: Scope): Scope[] {
      const { block } = scope;
      if (block.type !== AST_NODE_TYPES.TSModuleDeclaration) {
        return [scope];
      }

      const name = getModuleName(block.id);
      return getEnclosingScopes(block).flatMap(enclosing =>
        enclosing.childScopes.filter(
          childScope =>
            childScope.block.type === AST_NODE_TYPES.TSModuleDeclaration &&
            getModuleName(childScope.block.id) === name,
        ),
      );
    }

    function getEnclosingScopes(
      node: TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration,
    ): Scope[] {
      const enclosing = nullThrows(
        context.sourceCode.getScope(node).upper,
        NullThrowsReasons.MissingParent,
      );

      return node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration
        ? getMergedScopes(enclosing)
        : [enclosing];
    }

    function collectNodeDefinitions(
      node: TSESTree.TSEnumDeclaration,
    ): CollectedDefinitions {
      const { name } = node.id;
      const found: CollectedDefinitions = {
        imports: [],
        previousSibling: undefined,
      };

      for (const definition of getEnclosingScopes(node).flatMap(
        enclosing => enclosing.set.get(name)?.defs ?? [],
      )) {
        if (
          definition.node.type === AST_NODE_TYPES.TSEnumDeclaration &&
          definition.node.range[0] < node.range[0] &&
          definition.node.body.members.length > 0
        ) {
          found.previousSibling = definition.node;
          break;
        }
      }

      let scope: Scope | null = context.sourceCode.getScope(node);
      while (scope) {
        scope.set.get(name)?.defs.forEach(definition => {
          if (definition.type === DefinitionType.ImportBinding) {
            found.imports.push(definition.node);
          }
        });

        scope = scope.upper;
      }

      return found;
    }

    function getAllowedTypeForNode(node: ts.Node): AllowedType {
      return tsutils.isTypeFlagSet(
        typeChecker.getTypeAtLocation(node),
        ts.TypeFlags.StringLike,
      )
        ? AllowedType.String
        : AllowedType.Number;
    }

    function getTypeFromImported(
      imported: TSESTree.Node,
    ): AllowedType | undefined {
      const type = typeChecker.getTypeAtLocation(
        parserServices.esTreeNodeToTSNodeMap.get(imported),
      );

      const valueDeclaration = type.getSymbol()?.valueDeclaration;
      if (
        !valueDeclaration ||
        !ts.isEnumDeclaration(valueDeclaration) ||
        valueDeclaration.members.length === 0
      ) {
        return undefined;
      }

      return getAllowedTypeForNode(valueDeclaration.members[0]);
    }

    function getMemberType(member: TSESTree.TSEnumMember): AllowedType {
      if (!member.initializer) {
        return AllowedType.Number;
      }

      switch (member.initializer.type) {
        case AST_NODE_TYPES.Literal:
          switch (typeof member.initializer.value) {
            case 'number':
              return AllowedType.Number;
            case 'string':
              return AllowedType.String;
            default:
              return AllowedType.Unknown;
          }

        case AST_NODE_TYPES.TemplateLiteral:
          return AllowedType.String;

        default:
          return getAllowedTypeForNode(
            parserServices.esTreeNodeToTSNodeMap.get(member.initializer),
          );
      }
    }

    function getDesiredTypeForDefinition(
      node: TSESTree.TSEnumDeclaration,
    ): AllowedType | ts.TypeFlags.Unknown | undefined {
      const { imports, previousSibling } = collectNodeDefinitions(node);

      // Case: Merged ambiently via module augmentation
      // import { MyEnum } from 'other-module';
      // declare module 'other-module' {
      //   enum MyEnum { A }
      // }
      for (const imported of imports) {
        const typeFromImported = getTypeFromImported(imported);
        if (typeFromImported != null) {
          return typeFromImported;
        }
      }

      // Case: Multiple enum declarations in the same file
      // enum MyEnum { A }
      // enum MyEnum { B }
      //
      // ...including ones merged across namespace declarations
      // namespace MyNamespace {
      //   export enum MyEnum { A }
      // }
      // namespace MyNamespace {
      //   export enum MyEnum { B }
      // }
      if (previousSibling) {
        return getMemberType(previousSibling.body.members[0]);
      }

      // Finally, we default to the type of the first enum member
      return getMemberType(node.body.members[0]);
    }

    return {
      TSEnumDeclaration(node): void {
        if (!node.body.members.length) {
          return;
        }

        let desiredType = getDesiredTypeForDefinition(node);
        if (desiredType === ts.TypeFlags.Unknown) {
          return;
        }

        for (const member of node.body.members) {
          const currentType = getMemberType(member);
          if (currentType === AllowedType.Unknown) {
            return;
          }

          if (currentType === AllowedType.Number) {
            desiredType ??= currentType;
          }

          if (currentType !== desiredType) {
            context.report({
              node: member.initializer ?? member,
              messageId: 'mixed',
            });
            return;
          }
        }
      },
    };
  }

Internal helpers

Declared inside another function in this file.

getModuleName(id: TSESTree.TSModuleDeclaration['id']): string

Parameters:

  • id TSESTree.TSModuleDeclaration['id']

Returns: string

Calls:

  • JSON.stringify
  • context.sourceCode.getText
Code
function getModuleName(id: TSESTree.TSModuleDeclaration['id']): string {
      if (id.type === AST_NODE_TYPES.Literal) {
        return JSON.stringify(id.value);
      }

      let qualifiers = '';
      let current: TSESTree.EntityName = id;
      while (current.type === AST_NODE_TYPES.TSQualifiedName) {
        qualifiers = `.${current.right.name}${qualifiers}`;
        current = current.left;
      }

      return context.sourceCode.getText(current) + qualifiers;
    }

getMergedScopes(scope: Scope): Scope[]

Parameters:

  • scope Scope

Returns: Scope[]

Calls:

  • getModuleName
  • getEnclosingScopes(block).flatMap
  • enclosing.childScopes.filter
Code
function getMergedScopes(scope: Scope): Scope[] {
      const { block } = scope;
      if (block.type !== AST_NODE_TYPES.TSModuleDeclaration) {
        return [scope];
      }

      const name = getModuleName(block.id);
      return getEnclosingScopes(block).flatMap(enclosing =>
        enclosing.childScopes.filter(
          childScope =>
            childScope.block.type === AST_NODE_TYPES.TSModuleDeclaration &&
            getModuleName(childScope.block.id) === name,
        ),
      );
    }

getEnclosingScopes(node: TSESTree.TSEnumDeclaration | TSESTree.T…): Scope[]

Parameters:

  • node TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration

Returns: Scope[]

Calls:

  • nullThrows (from ../util)
  • context.sourceCode.getScope
  • getMergedScopes
Code
function getEnclosingScopes(
      node: TSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration,
    ): Scope[] {
      const enclosing = nullThrows(
        context.sourceCode.getScope(node).upper,
        NullThrowsReasons.MissingParent,
      );

      return node.parent.type === AST_NODE_TYPES.ExportNamedDeclaration
        ? getMergedScopes(enclosing)
        : [enclosing];
    }

collectNodeDefinitions(node: TSESTree.TSEnumDeclaration): CollectedDefinitions

Parameters:

  • node TSESTree.TSEnumDeclaration

Returns: CollectedDefinitions

Calls:

  • getEnclosingScopes(node).flatMap
  • enclosing.set.get
  • context.sourceCode.getScope
  • scope.set.get(name)?.defs.forEach
  • found.imports.push
Code
function collectNodeDefinitions(
      node: TSESTree.TSEnumDeclaration,
    ): CollectedDefinitions {
      const { name } = node.id;
      const found: CollectedDefinitions = {
        imports: [],
        previousSibling: undefined,
      };

      for (const definition of getEnclosingScopes(node).flatMap(
        enclosing => enclosing.set.get(name)?.defs ?? [],
      )) {
        if (
          definition.node.type === AST_NODE_TYPES.TSEnumDeclaration &&
          definition.node.range[0] < node.range[0] &&
          definition.node.body.members.length > 0
        ) {
          found.previousSibling = definition.node;
          break;
        }
      }

      let scope: Scope | null = context.sourceCode.getScope(node);
      while (scope) {
        scope.set.get(name)?.defs.forEach(definition => {
          if (definition.type === DefinitionType.ImportBinding) {
            found.imports.push(definition.node);
          }
        });

        scope = scope.upper;
      }

      return found;
    }

getAllowedTypeForNode(node: ts.Node): AllowedType

Parameters:

  • node ts.Node

Returns: AllowedType

Calls:

  • tsutils.isTypeFlagSet
  • typeChecker.getTypeAtLocation
Code
function getAllowedTypeForNode(node: ts.Node): AllowedType {
      return tsutils.isTypeFlagSet(
        typeChecker.getTypeAtLocation(node),
        ts.TypeFlags.StringLike,
      )
        ? AllowedType.String
        : AllowedType.Number;
    }

getTypeFromImported(imported: TSESTree.Node): AllowedType | undefined

Parameters:

  • imported TSESTree.Node

Returns: AllowedType | undefined

Calls:

  • typeChecker.getTypeAtLocation
  • parserServices.esTreeNodeToTSNodeMap.get
  • type.getSymbol
  • ts.isEnumDeclaration
  • getAllowedTypeForNode
Code
function getTypeFromImported(
      imported: TSESTree.Node,
    ): AllowedType | undefined {
      const type = typeChecker.getTypeAtLocation(
        parserServices.esTreeNodeToTSNodeMap.get(imported),
      );

      const valueDeclaration = type.getSymbol()?.valueDeclaration;
      if (
        !valueDeclaration ||
        !ts.isEnumDeclaration(valueDeclaration) ||
        valueDeclaration.members.length === 0
      ) {
        return undefined;
      }

      return getAllowedTypeForNode(valueDeclaration.members[0]);
    }

getMemberType(member: TSESTree.TSEnumMember): AllowedType

Parameters:

  • member TSESTree.TSEnumMember

Returns: AllowedType

Calls:

  • getAllowedTypeForNode
  • parserServices.esTreeNodeToTSNodeMap.get
Code
function getMemberType(member: TSESTree.TSEnumMember): AllowedType {
      if (!member.initializer) {
        return AllowedType.Number;
      }

      switch (member.initializer.type) {
        case AST_NODE_TYPES.Literal:
          switch (typeof member.initializer.value) {
            case 'number':
              return AllowedType.Number;
            case 'string':
              return AllowedType.String;
            default:
              return AllowedType.Unknown;
          }

        case AST_NODE_TYPES.TemplateLiteral:
          return AllowedType.String;

        default:
          return getAllowedTypeForNode(
            parserServices.esTreeNodeToTSNodeMap.get(member.initializer),
          );
      }
    }

getDesiredTypeForDefinition(node: TSESTree.TSEnumDeclaration): AllowedType | ts.TypeFlags.Unknown | undefined

Parameters:

  • node TSESTree.TSEnumDeclaration

Returns: AllowedType | ts.TypeFlags.Unknown | undefined

Calls:

  • collectNodeDefinitions
  • getTypeFromImported
  • getMemberType

Internal Comments:

// Case: Merged ambiently via module augmentation
// import { MyEnum } from 'other-module';
// declare module 'other-module' {
//   enum MyEnum { A }
// } (x3)
// Case: Multiple enum declarations in the same file
// enum MyEnum { A }
// enum MyEnum { B }
//
// ...including ones merged across namespace declarations
// namespace MyNamespace { (x2)
//   export enum MyEnum { A }
//   export enum MyEnum { B }
// Finally, we default to the type of the first enum member

Code
function getDesiredTypeForDefinition(
      node: TSESTree.TSEnumDeclaration,
    ): AllowedType | ts.TypeFlags.Unknown | undefined {
      const { imports, previousSibling } = collectNodeDefinitions(node);

      // Case: Merged ambiently via module augmentation
      // import { MyEnum } from 'other-module';
      // declare module 'other-module' {
      //   enum MyEnum { A }
      // }
      for (const imported of imports) {
        const typeFromImported = getTypeFromImported(imported);
        if (typeFromImported != null) {
          return typeFromImported;
        }
      }

      // Case: Multiple enum declarations in the same file
      // enum MyEnum { A }
      // enum MyEnum { B }
      //
      // ...including ones merged across namespace declarations
      // namespace MyNamespace {
      //   export enum MyEnum { A }
      // }
      // namespace MyNamespace {
      //   export enum MyEnum { B }
      // }
      if (previousSibling) {
        return getMemberType(previousSibling.body.members[0]);
      }

      // Finally, we default to the type of the first enum member
      return getMemberType(node.body.members[0]);
    }

Interfaces

CollectedDefinitions

Interface Code
interface CollectedDefinitions {
      imports: TSESTree.Node[];
      previousSibling: TSESTree.TSEnumDeclaration | undefined;
    }

Properties

Name Type Optional Description
imports TSESTree.Node[] not shown
previousSibling TSESTree.TSEnumDeclaration \| undefined not shown

Enums

enum AllowedType

Enum Code
enum AllowedType {
  Number,
  String,
  Unknown,
}

Members

Name Value Description
Number auto not shown
String auto not shown
Unknown auto not shown

Generated by Syntax Scribe