Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add prefer-enum-initializers rule #2326

Merged
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -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: |
Expand Down
70 changes: 70 additions & 0 deletions 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.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
73 changes: 73 additions & 0 deletions 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,
};
},
});