Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <me@joshuakgoldberg.com>
- Loading branch information
1 parent
acb5310
commit 5899164
Showing
6 changed files
with
267 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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
51 changes: 51 additions & 0 deletions
51
packages/eslint-plugin/docs/rules/no-duplicate-enum-values.md
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# `no-duplicate-enum-values` | ||
|
||
Disallow duplicate enum member values. | ||
|
||
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. | ||
|
||
## Rule Details | ||
|
||
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. | ||
|
||
<!--tabs--> | ||
|
||
### ❌ Incorrect | ||
|
||
```ts | ||
enum E { | ||
A = 0, | ||
B = 0, | ||
} | ||
``` | ||
|
||
```ts | ||
enum E { | ||
A = 'A' | ||
B = 'A' | ||
} | ||
``` | ||
|
||
### ✅ Correct | ||
|
||
```ts | ||
enum E { | ||
A = 0, | ||
B = 1, | ||
} | ||
``` | ||
|
||
```ts | ||
enum E { | ||
A = 'A' | ||
B = 'B' | ||
} | ||
``` | ||
|
||
This rule is not configurable. | ||
|
||
## Attributes | ||
|
||
- [ ] ✅ Recommended | ||
- [ ] 🔧 Fixable | ||
- [ ] 💭 Requires type information |
This file contains 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
This file contains 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
72 changes: 72 additions & 0 deletions
72
packages/eslint-plugin/src/rules/no-duplicate-enum-values.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { AST_NODE_TYPES, TSESTree } from '@typescript-eslint/utils'; | ||
import * as util from '../util'; | ||
|
||
export default util.createRule({ | ||
name: 'no-duplicate-enum-values', | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Disallow duplicate enum member values', | ||
recommended: false, | ||
}, | ||
hasSuggestions: true, | ||
messages: { | ||
duplicateValue: 'Duplicate enum member value {{value}}.', | ||
}, | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
function isStringLiteral( | ||
node: TSESTree.Expression, | ||
): node is TSESTree.StringLiteral { | ||
return ( | ||
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string' | ||
); | ||
} | ||
|
||
function isNumberLiteral( | ||
node: TSESTree.Expression, | ||
): node is TSESTree.NumberLiteral { | ||
return ( | ||
node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number' | ||
); | ||
} | ||
|
||
return { | ||
TSEnumDeclaration(node: TSESTree.TSEnumDeclaration): void { | ||
const enumMembers = node.members; | ||
const seenValues = new Set<number | string>(); | ||
|
||
enumMembers.forEach(member => { | ||
if (member.initializer === undefined) { | ||
return; | ||
} | ||
|
||
let value: string | number | undefined; | ||
if (isStringLiteral(member.initializer)) { | ||
value = String(member.initializer.value); | ||
} else if (isNumberLiteral(member.initializer)) { | ||
value = Number(member.initializer.value); | ||
} | ||
|
||
if (value === undefined) { | ||
return; | ||
} | ||
|
||
if (seenValues.has(value)) { | ||
context.report({ | ||
node: member, | ||
messageId: 'duplicateValue', | ||
data: { | ||
value, | ||
}, | ||
}); | ||
} else { | ||
seenValues.add(value); | ||
} | ||
}); | ||
}, | ||
}; | ||
}, | ||
}); |
136 changes: 136 additions & 0 deletions
136
packages/eslint-plugin/tests/rules/no-duplicate-enum-values.test.ts
This file contains 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import rule from '../../src/rules/no-duplicate-enum-values'; | ||
import { RuleTester } from '../RuleTester'; | ||
|
||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
}); | ||
|
||
ruleTester.run('no-duplicate-enum-values', rule, { | ||
valid: [ | ||
` | ||
enum E { | ||
A, | ||
B, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 1, | ||
B, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 1, | ||
B = 2, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 'A', | ||
B = 'B', | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 'A', | ||
B = 'B', | ||
C, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 'A', | ||
B = 'B', | ||
C = 2, | ||
D = 1 + 1, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 3, | ||
B = 2, | ||
C, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 'A', | ||
B = 'B', | ||
C = 2, | ||
D = foo(), | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = '', | ||
B = 0, | ||
} | ||
`, | ||
` | ||
enum E { | ||
A = 0, | ||
B = -0, | ||
C = NaN, | ||
} | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
enum E { | ||
A = 1, | ||
B = 1, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
line: 4, | ||
column: 3, | ||
messageId: 'duplicateValue', | ||
data: { value: 1 }, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum E { | ||
A = 'A', | ||
B = 'A', | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
line: 4, | ||
column: 3, | ||
messageId: 'duplicateValue', | ||
data: { value: 'A' }, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum E { | ||
A = 'A', | ||
B = 'A', | ||
C = 1, | ||
D = 1, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
line: 4, | ||
column: 3, | ||
messageId: 'duplicateValue', | ||
data: { value: 'A' }, | ||
}, | ||
{ | ||
line: 6, | ||
column: 3, | ||
messageId: 'duplicateValue', | ||
data: { value: 1 }, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |