📄 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¶
| 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:
contextany
Returns: { TSEnumDeclaration(node: any): void; }
Calls:
getParserServices (from ../util)parserServices.program.getTypeCheckerJSON.stringifycontext.sourceCode.getTextgetModuleNamegetEnclosingScopes(block).flatMapenclosing.childScopes.filternullThrows (from ../util)context.sourceCode.getScopegetMergedScopesgetEnclosingScopes(node).flatMapenclosing.set.getscope.set.get(name)?.defs.forEachfound.imports.pushtsutils.isTypeFlagSettypeChecker.getTypeAtLocationparserServices.esTreeNodeToTSNodeMap.gettype.getSymbolts.isEnumDeclarationgetAllowedTypeForNodecollectNodeDefinitionsgetTypeFromImportedgetMemberTypegetDesiredTypeForDefinitioncontext.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:
idTSESTree.TSModuleDeclaration['id']
Returns: string
Calls:
JSON.stringifycontext.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:
scopeScope
Returns: Scope[]
Calls:
getModuleNamegetEnclosingScopes(block).flatMapenclosing.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:
nodeTSESTree.TSEnumDeclaration | TSESTree.TSModuleDeclaration
Returns: Scope[]
Calls:
nullThrows (from ../util)context.sourceCode.getScopegetMergedScopes
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:
nodeTSESTree.TSEnumDeclaration
Returns: CollectedDefinitions
Calls:
getEnclosingScopes(node).flatMapenclosing.set.getcontext.sourceCode.getScopescope.set.get(name)?.defs.forEachfound.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:
nodets.Node
Returns: AllowedType
Calls:
tsutils.isTypeFlagSettypeChecker.getTypeAtLocation
Code
getTypeFromImported(imported: TSESTree.Node): AllowedType | undefined¶
Parameters:
importedTSESTree.Node
Returns: AllowedType | undefined
Calls:
typeChecker.getTypeAtLocationparserServices.esTreeNodeToTSNodeMap.gettype.getSymbolts.isEnumDeclarationgetAllowedTypeForNode
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:
memberTSESTree.TSEnumMember
Returns: AllowedType
Calls:
getAllowedTypeForNodeparserServices.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:
nodeTSESTree.TSEnumDeclaration
Returns: AllowedType | ts.TypeFlags.Unknown | undefined
Calls:
collectNodeDefinitionsgetTypeFromImportedgetMemberType
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
Properties¶
| Name | Type | Optional | Description |
|---|---|---|---|
imports |
TSESTree.Node[] |
✗ | not shown |
previousSibling |
TSESTree.TSEnumDeclaration \| undefined |
✗ | not shown |
Enums¶
enum AllowedType¶
Members¶
| Name | Value | Description |
|---|---|---|
Number |
auto | not shown |
String |
auto | not shown |
Unknown |
auto | not shown |
Generated by Syntax Scribe