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): add rule
prefer-literal-enum-member
(#1898)
- Loading branch information
Showing
6 changed files
with
301 additions
and
3 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/prefer-literal-enum-member.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 @@ | ||
# Require that all enum members be literal values to prevent unintended enum member name shadow issues (`prefer-literal-enum-member`) | ||
|
||
TypeScript allows the value of an enum member to be many different kinds of valid JavaScript expressions. However, because enums create their own scope whereby each enum member becomes a variable in that scope, unexpected values could be used at runtime. Example: | ||
|
||
```ts | ||
const imOutside = 2; | ||
const b = 2; | ||
enum Foo { | ||
outer = imOutside, | ||
a = 1, | ||
b = a, | ||
c = b, | ||
// does c == Foo.b == Foo.c == 1? | ||
// or does c == b == 2? | ||
} | ||
``` | ||
|
||
The answer is that `Foo.c` will be `1` at runtime. The [playground](https://www.typescriptlang.org/play/#src=const%20imOutside%20%3D%202%3B%0D%0Aconst%20b%20%3D%202%3B%0D%0Aenum%20Foo%20%7B%0D%0A%20%20%20%20outer%20%3D%20imOutside%2C%0D%0A%20%20%20%20a%20%3D%201%2C%0D%0A%20%20%20%20b%20%3D%20a%2C%0D%0A%20%20%20%20c%20%3D%20b%2C%0D%0A%20%20%20%20%2F%2F%20does%20c%20%3D%3D%20Foo.b%20%3D%3D%20Foo.c%20%3D%3D%201%3F%0D%0A%20%20%20%20%2F%2F%20or%20does%20c%20%3D%3D%20b%20%3D%3D%202%3F%0D%0A%7D) illustrates this quite nicely. | ||
|
||
## Rule Details | ||
|
||
This rule is meant to prevent unexpected results in code by requiring the use of literal values as enum members to prevent unexpected runtime behavior. Template literals, arrays, objects, constructors, and all other expression types can end up using a variable from its scope or the parent scope, which can result in the same unexpected behavior at runtime. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```ts | ||
const str = 'Test'; | ||
enum Invalid { | ||
A = str, // Variable assignment | ||
B = {}, // Object assignment | ||
C = `A template literal string`, // Template literal | ||
D = new Set(1, 2, 3), // Constructor in assignment | ||
E = 2 + 2, // Expression assignment | ||
} | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```ts | ||
enum Valid { | ||
A, | ||
B = 'TestStr', // A regular string | ||
C = 4, // A number | ||
D = null, | ||
E = /some_regex/, | ||
} | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
If you want use anything other than simple literals as an enum value. |
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
37 changes: 37 additions & 0 deletions
37
packages/eslint-plugin/src/rules/prefer-literal-enum-member.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,37 @@ | ||
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; | ||
import { createRule } from '../util'; | ||
|
||
export default createRule<[], 'notLiteral'>({ | ||
name: 'prefer-literal-enum-member', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: | ||
'Require that all enum members be literal values to prevent unintended enum member name shadow issues', | ||
category: 'Best Practices', | ||
recommended: false, | ||
requiresTypeChecking: false, | ||
}, | ||
messages: { | ||
notLiteral: `Explicit enum value must only be a literal value (string, number, boolean, etc).`, | ||
}, | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
return { | ||
TSEnumMember(node): void { | ||
// If there is no initializer, then this node is just the name of the member, so ignore. | ||
if ( | ||
node.initializer != null && | ||
node.initializer.type !== AST_NODE_TYPES.Literal | ||
) { | ||
context.report({ | ||
node: node.id, | ||
messageId: 'notLiteral', | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
206 changes: 206 additions & 0 deletions
206
packages/eslint-plugin/tests/rules/prefer-literal-enum-member.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,206 @@ | ||
import rule from '../../src/rules/prefer-literal-enum-member'; | ||
import { RuleTester, noFormat } from '../RuleTester'; | ||
|
||
const ruleTester = new RuleTester({ | ||
parser: '@typescript-eslint/parser', | ||
}); | ||
|
||
ruleTester.run('prefer-literal-enum-member', rule, { | ||
valid: [ | ||
` | ||
enum ValidRegex { | ||
A = /test/, | ||
} | ||
`, | ||
` | ||
enum ValidString { | ||
A = 'test', | ||
} | ||
`, | ||
` | ||
enum ValidNumber { | ||
A = 42, | ||
} | ||
`, | ||
` | ||
enum ValidNull { | ||
A = null, | ||
} | ||
`, | ||
` | ||
enum ValidPlain { | ||
A, | ||
} | ||
`, | ||
` | ||
enum ValidQuotedKey { | ||
'a', | ||
} | ||
`, | ||
` | ||
enum ValidQuotedKeyWithAssignment { | ||
'a' = 1, | ||
} | ||
`, | ||
noFormat` | ||
enum ValidKeyWithComputedSyntaxButNoComputedKey { | ||
['a'], | ||
} | ||
`, | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
enum InvalidObject { | ||
A = {}, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 3, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum InvalidArray { | ||
A = [], | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 3, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum InvalidTemplateLiteral { | ||
A = \`a\`, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 3, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum InvalidConstructor { | ||
A = new Set(), | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 3, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum InvalidExpression { | ||
A = 2 + 2, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 3, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
const variable = 'Test'; | ||
enum InvalidVariable { | ||
A = 'TestStr', | ||
B = 2, | ||
C, | ||
V = variable, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 7, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum InvalidEnumMember { | ||
A = 'TestStr', | ||
B = A, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 4, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
const Valid = { A: 2 }; | ||
enum InvalidObjectMember { | ||
A = 'TestStr', | ||
B = Valid.A, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 5, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
enum Valid { | ||
A, | ||
} | ||
enum InvalidEnumMember { | ||
A = 'TestStr', | ||
B = Valid.A, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 7, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
{ | ||
code: ` | ||
const obj = { a: 1 }; | ||
enum InvalidSpread { | ||
A = 'TestStr', | ||
B = { ...a }, | ||
} | ||
`, | ||
errors: [ | ||
{ | ||
messageId: 'notLiteral', | ||
line: 5, | ||
column: 3, | ||
}, | ||
], | ||
}, | ||
], | ||
}); |