Skip to content

Commit 5899164

Browse files
feat(eslint-plugin): [no-duplicate-enum-values] add rule (#4833)
* feat(eslint-plugin): create a new rule to disallow duplicate enum values * fix(eslint-plugin): remove unused imported variable from no-duplicate-enum-values.test.ts * fix(eslint-plugin): test falsy values and fix some metadata * fix(eslint-plugin): make Enums in the falsy test valid Co-authored-by: Josh Goldberg <[email protected]>
1 parent acb5310 commit 5899164

File tree

6 files changed

+267
-4
lines changed

6 files changed

+267
-4
lines changed

packages/eslint-plugin/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
119119
| [`@typescript-eslint/no-base-to-string`](./docs/rules/no-base-to-string.md) | Requires that `.toString()` is only called on objects which provide useful information when stringified | | | :thought_balloon: |
120120
| [`@typescript-eslint/no-confusing-non-null-assertion`](./docs/rules/no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | | :wrench: | |
121121
| [`@typescript-eslint/no-confusing-void-expression`](./docs/rules/no-confusing-void-expression.md) | Requires expressions of type void to appear in statement position | | :wrench: | :thought_balloon: |
122+
| [`@typescript-eslint/no-duplicate-enum-values`](./docs/rules/no-duplicate-enum-values.md) | Disallow duplicate enum member values | | | |
122123
| [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | |
123124
| [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :white_check_mark: | :wrench: | |
124125
| [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :white_check_mark: | :wrench: | |
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# `no-duplicate-enum-values`
2+
3+
Disallow duplicate enum member values.
4+
5+
Although TypeScript supports duplicate enum member values, people usually expect members to have unique values within the same enum. Duplicate values can lead to bugs that are hard to track down.
6+
7+
## Rule Details
8+
9+
This rule disallows defining an enum with multiple members initialized to the same value. Now it only enforces on enum members initialized with String or Number literals. Members without initializer or initialized with an expression are not checked by this rule.
10+
11+
<!--tabs-->
12+
13+
### ❌ Incorrect
14+
15+
```ts
16+
enum E {
17+
A = 0,
18+
B = 0,
19+
}
20+
```
21+
22+
```ts
23+
enum E {
24+
A = 'A'
25+
B = 'A'
26+
}
27+
```
28+
29+
### ✅ Correct
30+
31+
```ts
32+
enum E {
33+
A = 0,
34+
B = 1,
35+
}
36+
```
37+
38+
```ts
39+
enum E {
40+
A = 'A'
41+
B = 'B'
42+
}
43+
```
44+
45+
This rule is not configurable.
46+
47+
## Attributes
48+
49+
- [ ] ✅ Recommended
50+
- [ ] 🔧 Fixable
51+
- [ ] 💭 Requires type information

packages/eslint-plugin/src/configs/all.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export = {
2121
'@typescript-eslint/consistent-indexed-object-style': 'error',
2222
'@typescript-eslint/consistent-type-assertions': 'error',
2323
'@typescript-eslint/consistent-type-definitions': 'error',
24-
'@typescript-eslint/consistent-type-imports': 'error',
2524
'@typescript-eslint/consistent-type-exports': 'error',
25+
'@typescript-eslint/consistent-type-imports': 'error',
2626
'default-param-last': 'off',
2727
'@typescript-eslint/default-param-last': 'error',
2828
'dot-notation': 'off',
@@ -51,6 +51,7 @@ export = {
5151
'@typescript-eslint/no-confusing-void-expression': 'error',
5252
'no-dupe-class-members': 'off',
5353
'@typescript-eslint/no-dupe-class-members': 'error',
54+
'@typescript-eslint/no-duplicate-enum-values': 'error',
5455
'no-duplicate-imports': 'off',
5556
'@typescript-eslint/no-duplicate-imports': 'error',
5657
'@typescript-eslint/no-dynamic-delete': 'error',
@@ -115,9 +116,9 @@ export = {
115116
'@typescript-eslint/no-unused-vars': 'error',
116117
'no-use-before-define': 'off',
117118
'@typescript-eslint/no-use-before-define': 'error',
118-
'@typescript-eslint/no-useless-empty-export': 'error',
119119
'no-useless-constructor': 'off',
120120
'@typescript-eslint/no-useless-constructor': 'error',
121+
'@typescript-eslint/no-useless-empty-export': 'error',
121122
'@typescript-eslint/no-var-requires': 'error',
122123
'@typescript-eslint/non-nullable-type-assertion-style': 'error',
123124
'object-curly-spacing': 'off',
@@ -153,12 +154,12 @@ export = {
153154
semi: 'off',
154155
'@typescript-eslint/semi': 'error',
155156
'@typescript-eslint/sort-type-union-intersection-members': 'error',
157+
'space-before-blocks': 'off',
158+
'@typescript-eslint/space-before-blocks': 'error',
156159
'space-before-function-paren': 'off',
157160
'@typescript-eslint/space-before-function-paren': 'error',
158161
'space-infix-ops': 'off',
159162
'@typescript-eslint/space-infix-ops': 'error',
160-
'space-before-blocks': 'off',
161-
'@typescript-eslint/space-before-blocks': 'error',
162163
'@typescript-eslint/strict-boolean-expressions': 'error',
163164
'@typescript-eslint/switch-exhaustiveness-check': 'error',
164165
'@typescript-eslint/triple-slash-reference': 'error',

packages/eslint-plugin/src/rules/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import noBaseToString from './no-base-to-string';
3232
import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion';
3333
import noConfusingVoidExpression from './no-confusing-void-expression';
3434
import noDupeClassMembers from './no-dupe-class-members';
35+
import noDuplicateEnumValues from './no-duplicate-enum-values';
3536
import noDuplicateImports from './no-duplicate-imports';
3637
import noDynamicDelete from './no-dynamic-delete';
3738
import noEmptyFunction from './no-empty-function';
@@ -159,6 +160,7 @@ export default {
159160
'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual,
160161
'no-confusing-void-expression': noConfusingVoidExpression,
161162
'no-dupe-class-members': noDupeClassMembers,
163+
'no-duplicate-enum-values': noDuplicateEnumValues,
162164
'no-duplicate-imports': noDuplicateImports,
163165
'no-dynamic-delete': noDynamicDelete,
164166
'no-empty-function': noEmptyFunction,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils';
2+
import * as util from '../util';
3+
4+
export default util.createRule({
5+
name: 'no-duplicate-enum-values',
6+
meta: {
7+
type: 'problem',
8+
docs: {
9+
description: 'Disallow duplicate enum member values',
10+
recommended: false,
11+
},
12+
hasSuggestions: true,
13+
messages: {
14+
duplicateValue: 'Duplicate enum member value {{value}}.',
15+
},
16+
schema: [],
17+
},
18+
defaultOptions: [],
19+
create(context) {
20+
function isStringLiteral(
21+
node: TSESTree.Expression,
22+
): node is TSESTree.StringLiteral {
23+
return (
24+
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string'
25+
);
26+
}
27+
28+
function isNumberLiteral(
29+
node: TSESTree.Expression,
30+
): node is TSESTree.NumberLiteral {
31+
return (
32+
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number'
33+
);
34+
}
35+
36+
return {
37+
TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void {
38+
const enumMembers = node.members;
39+
const seenValues = new Set<number | string>();
40+
41+
enumMembers.forEach(member => {
42+
if (member.initializer === undefined) {
43+
return;
44+
}
45+
46+
let value: string | number | undefined;
47+
if (isStringLiteral(member.initializer)) {
48+
value = String(member.initializer.value);
49+
} else if (isNumberLiteral(member.initializer)) {
50+
value = Number(member.initializer.value);
51+
}
52+
53+
if (value === undefined) {
54+
return;
55+
}
56+
57+
if (seenValues.has(value)) {
58+
context.report({
59+
node: member,
60+
messageId: 'duplicateValue',
61+
data: {
62+
value,
63+
},
64+
});
65+
} else {
66+
seenValues.add(value);
67+
}
68+
});
69+
},
70+
};
71+
},
72+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import rule from '../../src/rules/no-duplicate-enum-values';
2+
import { RuleTester } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('no-duplicate-enum-values', rule, {
9+
valid: [
10+
`
11+
enum E {
12+
A,
13+
B,
14+
}
15+
`,
16+
`
17+
enum E {
18+
A = 1,
19+
B,
20+
}
21+
`,
22+
`
23+
enum E {
24+
A = 1,
25+
B = 2,
26+
}
27+
`,
28+
`
29+
enum E {
30+
A = 'A',
31+
B = 'B',
32+
}
33+
`,
34+
`
35+
enum E {
36+
A = 'A',
37+
B = 'B',
38+
C,
39+
}
40+
`,
41+
`
42+
enum E {
43+
A = 'A',
44+
B = 'B',
45+
C = 2,
46+
D = 1 + 1,
47+
}
48+
`,
49+
`
50+
enum E {
51+
A = 3,
52+
B = 2,
53+
C,
54+
}
55+
`,
56+
`
57+
enum E {
58+
A = 'A',
59+
B = 'B',
60+
C = 2,
61+
D = foo(),
62+
}
63+
`,
64+
`
65+
enum E {
66+
A = '',
67+
B = 0,
68+
}
69+
`,
70+
`
71+
enum E {
72+
A = 0,
73+
B = -0,
74+
C = NaN,
75+
}
76+
`,
77+
],
78+
invalid: [
79+
{
80+
code: `
81+
enum E {
82+
A = 1,
83+
B = 1,
84+
}
85+
`,
86+
errors: [
87+
{
88+
line: 4,
89+
column: 3,
90+
messageId: 'duplicateValue',
91+
data: { value: 1 },
92+
},
93+
],
94+
},
95+
{
96+
code: `
97+
enum E {
98+
A = 'A',
99+
B = 'A',
100+
}
101+
`,
102+
errors: [
103+
{
104+
line: 4,
105+
column: 3,
106+
messageId: 'duplicateValue',
107+
data: { value: 'A' },
108+
},
109+
],
110+
},
111+
{
112+
code: `
113+
enum E {
114+
A = 'A',
115+
B = 'A',
116+
C = 1,
117+
D = 1,
118+
}
119+
`,
120+
errors: [
121+
{
122+
line: 4,
123+
column: 3,
124+
messageId: 'duplicateValue',
125+
data: { value: 'A' },
126+
},
127+
{
128+
line: 6,
129+
column: 3,
130+
messageId: 'duplicateValue',
131+
data: { value: 1 },
132+
},
133+
],
134+
},
135+
],
136+
});

0 commit comments

Comments
 (0)