Last active
December 17, 2022 10:43
-
-
Save temoncher/65ac0106d3377df56ef515fdaee479f8 to your computer and use it in GitHub Desktop.
ESLint rule that autofixes enums into constants
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @ts-check | |
| /** @type {import('@typescript-eslint/utils').TSESLint.RuleModule} */ | |
| module.exports = { | |
| meta: { | |
| docs: { | |
| description: | |
| "See [TypeScript Enums are TERRIBLE. Here's Why. - Michigan TypeScript](https://www.youtube.com/watch?v=0fTdCSH_QEU)", | |
| }, | |
| type: 'suggestion', | |
| fixable: 'code', | |
| schema: [ | |
| { | |
| type: 'object', | |
| properties: { | |
| UNSAFE_oneSidedNumericEnums: { | |
| type: 'boolean', | |
| }, | |
| }, | |
| additionalProperties: false, | |
| }, | |
| ], | |
| }, | |
| create: (context) => { | |
| const { UNSAFE_oneSidedNumericEnums } = context.options[0] ?? { | |
| UNSAFE_oneSidedNumericEnums: false, | |
| }; | |
| return { | |
| // eslint-disable-next-line complexity, max-statements | |
| TSEnumDeclaration(node) { | |
| const enumName = node.id.name; | |
| const keyValuePairs = []; | |
| let currentEnumNumber = 0; | |
| for (const member of node.members) { | |
| if (member.id.type !== 'Identifier') { | |
| throw new Error('Member id is not an `Identifier` node'); | |
| } | |
| if (!member.initializer) { | |
| keyValuePairs.push([member.id.name, currentEnumNumber]); | |
| if (!UNSAFE_oneSidedNumericEnums) { | |
| keyValuePairs.push([currentEnumNumber, member.id.name]); | |
| } | |
| currentEnumNumber++; | |
| continue; | |
| } | |
| if (member.initializer.type === 'Literal') { | |
| if (typeof member.initializer.value === 'number') { | |
| keyValuePairs.push([member.id.name, member.initializer.value]); | |
| if (!UNSAFE_oneSidedNumericEnums) { | |
| keyValuePairs.push([member.initializer.value, member.id.name]); | |
| } | |
| currentEnumNumber = member.initializer.value + 1; | |
| continue; | |
| } | |
| if (typeof member.initializer.value === 'string') { | |
| keyValuePairs.push([member.id.name, member.initializer.value]); | |
| continue; | |
| } | |
| } | |
| // Can't autofix | |
| context.report({ | |
| node, | |
| message: | |
| 'Please use object-style enums', | |
| }); | |
| return; | |
| } | |
| const enumIsExported = node.parent?.type === 'ExportNamedDeclaration'; | |
| context.report({ | |
| node, | |
| message: | |
| 'Please use object-style enums', | |
| *fix(fixer) { | |
| const keyValuePairsText = keyValuePairs | |
| .map(([key, value]) => | |
| typeof value === 'string' ? `${key}:'${value}'` : `${key}:${value}` | |
| ) | |
| .join(','); | |
| const exportPrefixOrEmpty = enumIsExported ? 'export ' : ''; | |
| const enumValueText = `${exportPrefixOrEmpty}const ${enumName} = {${keyValuePairsText}} as const;`; | |
| const enumTypeText = `${exportPrefixOrEmpty}type ${enumName} = typeof ${enumName}[keyof typeof ${enumName}];`; | |
| yield fixer.replaceText( | |
| enumIsExported ? node.parent : node, | |
| `${enumValueText}\n${enumTypeText}` | |
| ); | |
| }, | |
| }); | |
| }, | |
| }; | |
| }, | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment