Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fe2b2ec

Browse files
authored
feat(eslint-plugin): add rule prefer-literal-enum-member (typescript-eslint#1898)
1 parent c1df669 commit fe2b2ec

File tree

6 files changed

+301
-3
lines changed

6 files changed

+301
-3
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
150150
| [`@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 | | | |
151151
| [`@typescript-eslint/prefer-function-type`](./docs/rules/prefer-function-type.md) | Use function types instead of interfaces with call signatures | | :wrench: | |
152152
| [`@typescript-eslint/prefer-includes`](./docs/rules/prefer-includes.md) | Enforce `includes` method over `indexOf` method | | :wrench: | :thought_balloon: |
153+
| [`@typescript-eslint/prefer-literal-enum-member`](./docs/rules/prefer-literal-enum-member.md) | Require that all enum members be literal values to prevent unintended enum member name shadow issues | | | |
153154
| [`@typescript-eslint/prefer-namespace-keyword`](./docs/rules/prefer-namespace-keyword.md) | Require the use of the `namespace` keyword instead of the `module` keyword to declare custom TypeScript modules | :heavy_check_mark: | :wrench: | |
154155
| [`@typescript-eslint/prefer-nullish-coalescing`](./docs/rules/prefer-nullish-coalescing.md) | Enforce the usage of the nullish coalescing operator instead of logical chaining | | | :thought_balloon: |
155156
| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | | |
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Require that all enum members be literal values to prevent unintended enum member name shadow issues (`prefer-literal-enum-member`)
2+
3+
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:
4+
5+
```ts
6+
const imOutside = 2;
7+
const b = 2;
8+
enum Foo {
9+
outer = imOutside,
10+
a = 1,
11+
b = a,
12+
c = b,
13+
// does c == Foo.b == Foo.c == 1?
14+
// or does c == b == 2?
15+
}
16+
```
17+
18+
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.
19+
20+
## Rule Details
21+
22+
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.
23+
24+
Examples of **incorrect** code for this rule:
25+
26+
```ts
27+
const str = 'Test';
28+
enum Invalid {
29+
A = str, // Variable assignment
30+
B = {}, // Object assignment
31+
C = `A template literal string`, // Template literal
32+
D = new Set(1, 2, 3), // Constructor in assignment
33+
E = 2 + 2, // Expression assignment
34+
}
35+
```
36+
37+
Examples of **correct** code for this rule:
38+
39+
```ts
40+
enum Valid {
41+
A,
42+
B = 'TestStr', // A regular string
43+
C = 4, // A number
44+
D = null,
45+
E = /some_regex/,
46+
}
47+
```
48+
49+
## When Not To Use It
50+
51+
If you want use anything other than simple literals as an enum value.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export = {
100100
'@typescript-eslint/prefer-for-of': 'error',
101101
'@typescript-eslint/prefer-function-type': 'error',
102102
'@typescript-eslint/prefer-includes': 'error',
103+
'@typescript-eslint/prefer-literal-enum-member': 'error',
103104
'@typescript-eslint/prefer-namespace-keyword': 'error',
104105
'@typescript-eslint/prefer-nullish-coalescing': 'error',
105106
'@typescript-eslint/prefer-optional-chain': 'error',

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import explicitMemberAccessibility from './explicit-member-accessibility';
1717
import explicitModuleBoundaryTypes from './explicit-module-boundary-types';
1818
import funcCallSpacing from './func-call-spacing';
1919
import indent from './indent';
20+
import initDeclarations from './init-declarations';
2021
import keywordSpacing from './keyword-spacing';
22+
import linesBetweenClassMembers from './lines-between-class-members';
2123
import memberDelimiterStyle from './member-delimiter-style';
2224
import memberOrdering from './member-ordering';
2325
import methodSignatureStyle from './method-signature-style';
@@ -35,10 +37,12 @@ import noExtraParens from './no-extra-parens';
3537
import noExtraSemi from './no-extra-semi';
3638
import noFloatingPromises from './no-floating-promises';
3739
import noForInArray from './no-for-in-array';
40+
import preferLiteralEnumMember from './prefer-literal-enum-member';
3841
import noImpliedEval from './no-implied-eval';
3942
import noInferrableTypes from './no-inferrable-types';
4043
import noInvalidThis from './no-invalid-this';
4144
import noInvalidVoidType from './no-invalid-void-type';
45+
import noLossOfPrecision from './no-loss-of-precision';
4246
import noMagicNumbers from './no-magic-numbers';
4347
import noMisusedNew from './no-misused-new';
4448
import noMisusedPromises from './no-misused-promises';
@@ -85,7 +89,6 @@ import requireAwait from './require-await';
8589
import restrictPlusOperands from './restrict-plus-operands';
8690
import restrictTemplateExpressions from './restrict-template-expressions';
8791
import returnAwait from './return-await';
88-
import initDeclarations from './init-declarations';
8992
import semi from './semi';
9093
import spaceBeforeFunctionParen from './space-before-function-paren';
9194
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -95,8 +98,6 @@ import typeAnnotationSpacing from './type-annotation-spacing';
9598
import typedef from './typedef';
9699
import unboundMethod from './unbound-method';
97100
import unifiedSignatures from './unified-signatures';
98-
import linesBetweenClassMembers from './lines-between-class-members';
99-
import noLossOfPrecision from './no-loss-of-precision';
100101

101102
export default {
102103
'adjacent-overload-signatures': adjacentOverloadSignatures,
@@ -171,6 +172,7 @@ export default {
171172
'prefer-for-of': preferForOf,
172173
'prefer-function-type': preferFunctionType,
173174
'prefer-includes': preferIncludes,
175+
'prefer-literal-enum-member': preferLiteralEnumMember,
174176
'prefer-namespace-keyword': preferNamespaceKeyword,
175177
'prefer-nullish-coalescing': preferNullishCoalescing,
176178
'prefer-optional-chain': preferOptionalChain,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
2+
import { createRule } from '../util';
3+
4+
export default createRule<[], 'notLiteral'>({
5+
name: 'prefer-literal-enum-member',
6+
meta: {
7+
type: 'suggestion',
8+
docs: {
9+
description:
10+
'Require that all enum members be literal values to prevent unintended enum member name shadow issues',
11+
category: 'Best Practices',
12+
recommended: false,
13+
requiresTypeChecking: false,
14+
},
15+
messages: {
16+
notLiteral: `Explicit enum value must only be a literal value (string, number, boolean, etc).`,
17+
},
18+
schema: [],
19+
},
20+
defaultOptions: [],
21+
create(context) {
22+
return {
23+
TSEnumMember(node): void {
24+
// If there is no initializer, then this node is just the name of the member, so ignore.
25+
if (
26+
node.initializer != null &&
27+
node.initializer.type !== AST_NODE_TYPES.Literal
28+
) {
29+
context.report({
30+
node: node.id,
31+
messageId: 'notLiteral',
32+
});
33+
}
34+
},
35+
};
36+
},
37+
});
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import rule from '../../src/rules/prefer-literal-enum-member';
2+
import { RuleTester, noFormat } from '../RuleTester';
3+
4+
const ruleTester = new RuleTester({
5+
parser: '@typescript-eslint/parser',
6+
});
7+
8+
ruleTester.run('prefer-literal-enum-member', rule, {
9+
valid: [
10+
`
11+
enum ValidRegex {
12+
A = /test/,
13+
}
14+
`,
15+
`
16+
enum ValidString {
17+
A = 'test',
18+
}
19+
`,
20+
`
21+
enum ValidNumber {
22+
A = 42,
23+
}
24+
`,
25+
`
26+
enum ValidNull {
27+
A = null,
28+
}
29+
`,
30+
`
31+
enum ValidPlain {
32+
A,
33+
}
34+
`,
35+
`
36+
enum ValidQuotedKey {
37+
'a',
38+
}
39+
`,
40+
`
41+
enum ValidQuotedKeyWithAssignment {
42+
'a' = 1,
43+
}
44+
`,
45+
noFormat`
46+
enum ValidKeyWithComputedSyntaxButNoComputedKey {
47+
['a'],
48+
}
49+
`,
50+
],
51+
invalid: [
52+
{
53+
code: `
54+
enum InvalidObject {
55+
A = {},
56+
}
57+
`,
58+
errors: [
59+
{
60+
messageId: 'notLiteral',
61+
line: 3,
62+
column: 3,
63+
},
64+
],
65+
},
66+
{
67+
code: `
68+
enum InvalidArray {
69+
A = [],
70+
}
71+
`,
72+
errors: [
73+
{
74+
messageId: 'notLiteral',
75+
line: 3,
76+
column: 3,
77+
},
78+
],
79+
},
80+
{
81+
code: `
82+
enum InvalidTemplateLiteral {
83+
A = \`a\`,
84+
}
85+
`,
86+
errors: [
87+
{
88+
messageId: 'notLiteral',
89+
line: 3,
90+
column: 3,
91+
},
92+
],
93+
},
94+
{
95+
code: `
96+
enum InvalidConstructor {
97+
A = new Set(),
98+
}
99+
`,
100+
errors: [
101+
{
102+
messageId: 'notLiteral',
103+
line: 3,
104+
column: 3,
105+
},
106+
],
107+
},
108+
{
109+
code: `
110+
enum InvalidExpression {
111+
A = 2 + 2,
112+
}
113+
`,
114+
errors: [
115+
{
116+
messageId: 'notLiteral',
117+
line: 3,
118+
column: 3,
119+
},
120+
],
121+
},
122+
{
123+
code: `
124+
const variable = 'Test';
125+
enum InvalidVariable {
126+
A = 'TestStr',
127+
B = 2,
128+
C,
129+
V = variable,
130+
}
131+
`,
132+
errors: [
133+
{
134+
messageId: 'notLiteral',
135+
line: 7,
136+
column: 3,
137+
},
138+
],
139+
},
140+
{
141+
code: `
142+
enum InvalidEnumMember {
143+
A = 'TestStr',
144+
B = A,
145+
}
146+
`,
147+
errors: [
148+
{
149+
messageId: 'notLiteral',
150+
line: 4,
151+
column: 3,
152+
},
153+
],
154+
},
155+
{
156+
code: `
157+
const Valid = { A: 2 };
158+
enum InvalidObjectMember {
159+
A = 'TestStr',
160+
B = Valid.A,
161+
}
162+
`,
163+
errors: [
164+
{
165+
messageId: 'notLiteral',
166+
line: 5,
167+
column: 3,
168+
},
169+
],
170+
},
171+
{
172+
code: `
173+
enum Valid {
174+
A,
175+
}
176+
enum InvalidEnumMember {
177+
A = 'TestStr',
178+
B = Valid.A,
179+
}
180+
`,
181+
errors: [
182+
{
183+
messageId: 'notLiteral',
184+
line: 7,
185+
column: 3,
186+
},
187+
],
188+
},
189+
{
190+
code: `
191+
const obj = { a: 1 };
192+
enum InvalidSpread {
193+
A = 'TestStr',
194+
B = { ...a },
195+
}
196+
`,
197+
errors: [
198+
{
199+
messageId: 'notLiteral',
200+
line: 5,
201+
column: 3,
202+
},
203+
],
204+
},
205+
],
206+
});

0 commit comments

Comments
 (0)