diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index b3360692178..cb8a5546142 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -147,6 +147,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | | [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Prefer initializing each enums member value | | | | | [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | | [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | | | [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: | diff --git a/packages/eslint-plugin/docs/rules/prefer-enum-initializers.md b/packages/eslint-plugin/docs/rules/prefer-enum-initializers.md new file mode 100644 index 00000000000..1e8f568d721 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/prefer-enum-initializers.md @@ -0,0 +1,70 @@ +# Prefer initializing each enums member value (`prefer-enum-initializers`) + +This rule recommends having each `enum`s member value explicitly initialized. + +`enum`s are a practical way to organize semantically related constant values. However, by implicitly defining values, `enum`s can lead to unexpected bugs if it's modified without paying attention to the order of its items. + +## Rule Details + +`enum`s infers sequential numbers automatically when initializers are omitted: + +```ts +enum Status { + Open, // infer 0 + Closed, // infer 1 +} +``` + +If a new member is added to the top of `Status`, both `Open` and `Closed` would have its values altered: + +```ts +enum Status { + Pending, // infer 0 + Open, // infer 1 + Closed, // infer 2 +} +``` + +Examples of **incorrect** code for this rule: + +```ts +enum Status { + Open = 1, + Close, +} + +enum Direction { + Up, + Down, +} + +enum Color { + Red, + Green = 'Green' + Blue = 'Blue', +} +``` + +Examples of **correct** code for this rule: + +```ts +enum Status { + Open = 'Open', + Close = 'Close', +} + +enum Direction { + Up = 1, + Down = 2, +} + +enum Color { + Red = 'Red', + Green = 'Green', + Blue = 'Blue', +} +``` + +## When Not To Use It + +If you don't care about `enum`s having implicit values you can safely disable this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index e78feb7d44a..8361bde29b2 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -97,6 +97,7 @@ export = { '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-var-requires': 'error', '@typescript-eslint/prefer-as-const': 'error', + '@typescript-eslint/prefer-enum-initializers': 'error', '@typescript-eslint/prefer-for-of': 'error', '@typescript-eslint/prefer-function-type': 'error', '@typescript-eslint/prefer-includes': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 73083bedfe4..85d0642fc79 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -70,6 +70,7 @@ import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; import preferAsConst from './prefer-as-const'; +import preferEnumInitializers from './prefer-enum-initializers'; import preferForOf from './prefer-for-of'; import preferFunctionType from './prefer-function-type'; import preferIncludes from './prefer-includes'; @@ -169,6 +170,7 @@ export default { 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, 'prefer-as-const': preferAsConst, + 'prefer-enum-initializers': preferEnumInitializers, 'prefer-for-of': preferForOf, 'prefer-function-type': preferFunctionType, 'prefer-includes': preferIncludes, diff --git a/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts b/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts new file mode 100644 index 00000000000..1a5c1248c89 --- /dev/null +++ b/packages/eslint-plugin/src/rules/prefer-enum-initializers.ts @@ -0,0 +1,73 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; +import { TSESLint } from '@typescript-eslint/experimental-utils'; + +type MessageIds = 'defineInitializer' | 'defineInitializerSuggestion'; + +export default util.createRule<[], MessageIds>({ + name: 'prefer-enum-initializers', + meta: { + type: 'suggestion', + docs: { + description: 'Prefer initializing each enums member value', + category: 'Best Practices', + recommended: false, + suggestion: true, + }, + messages: { + defineInitializer: + "The value of the member '{{ name }}' should be explicitly defined", + defineInitializerSuggestion: + 'Can be fixed to {{ name }} = {{ suggested }}', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.getSourceCode(); + + function TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void { + const { members } = node; + + members.forEach((member, index) => { + if (member.initializer == null) { + const name = sourceCode.getText(member); + context.report({ + node: member, + messageId: 'defineInitializer', + data: { + name, + }, + suggest: [ + { + messageId: 'defineInitializerSuggestion', + data: { name, suggested: index }, + fix: (fixer): TSESLint.RuleFix => { + return fixer.replaceText(member, `${name} = ${index}`); + }, + }, + { + messageId: 'defineInitializerSuggestion', + data: { name, suggested: index + 1 }, + fix: (fixer): TSESLint.RuleFix => { + return fixer.replaceText(member, `${name} = ${index + 1}`); + }, + }, + { + messageId: 'defineInitializerSuggestion', + data: { name, suggested: `'${name}'` }, + fix: (fixer): TSESLint.RuleFix => { + return fixer.replaceText(member, `${name} = '${name}'`); + }, + }, + ], + }); + } + }); + } + + return { + TSEnumDeclaration, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/prefer-enum-initializers.test.ts b/packages/eslint-plugin/tests/rules/prefer-enum-initializers.test.ts new file mode 100644 index 00000000000..0a49c52d945 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/prefer-enum-initializers.test.ts @@ -0,0 +1,241 @@ +import rule from '../../src/rules/prefer-enum-initializers'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('prefer-enum-initializers', rule, { + valid: [ + ` +enum Direction {} + `, + ` +enum Direction { + Up = 1, +} + `, + ` +enum Direction { + Up = 1, + Down = 2, +} + `, + ` +enum Direction { + Up = 'Up', + Down = 'Down', +} + `, + ], + // We need to keep indentation for avoiding @typescript-eslint/internal/plugin-test-formatting. + // Use trimRight() to make tests pass for now. https://github.com/typescript-eslint/typescript-eslint/pull/2326#discussion_r461760044 + invalid: [ + { + code: ` +enum Direction { + Up, +} + `.trimRight(), + errors: [ + { + messageId: 'defineInitializer', + data: { name: 'Up' }, + line: 3, + suggestions: [ + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 0, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 1, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 'Up', +} + `.trimRight(), + }, + ], + }, + ], + }, + { + code: ` +enum Direction { + Up, + Down, +} + `.trimRight(), + errors: [ + { + messageId: 'defineInitializer', + data: { name: 'Up' }, + line: 3, + suggestions: [ + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 0, + Down, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 1, + Down, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 'Up', + Down, +} + `.trimRight(), + }, + ], + }, + { + messageId: 'defineInitializer', + data: { name: 'Down' }, + line: 4, + suggestions: [ + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up, + Down = 1, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up, + Down = 2, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up, + Down = 'Down', +} + `.trimRight(), + }, + ], + }, + ], + }, + { + code: ` +enum Direction { + Up = 'Up', + Down, +} + `.trimRight(), + errors: [ + { + messageId: 'defineInitializer', + data: { name: 'Down' }, + line: 4, + suggestions: [ + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 'Up', + Down = 1, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 'Up', + Down = 2, +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 'Up', + Down = 'Down', +} + `.trimRight(), + }, + ], + }, + ], + }, + { + code: ` +enum Direction { + Up, + Down = 'Down', +} + `.trimRight(), + errors: [ + { + messageId: 'defineInitializer', + data: { name: 'Up' }, + line: 3, + suggestions: [ + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 0, + Down = 'Down', +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 1, + Down = 'Down', +} + `.trimRight(), + }, + { + messageId: 'defineInitializerSuggestion', + output: ` +enum Direction { + Up = 'Up', + Down = 'Down', +} + `.trimRight(), + }, + ], + }, + ], + }, + ], +});