Skip to content

Commit

Permalink
feat(eslint-plugin): add rule prefer-literal-enum-member (#1898)
Browse files Browse the repository at this point in the history
  • Loading branch information
oigewan committed Jul 6, 2020
1 parent c1df669 commit fe2b2ec
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -150,6 +150,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@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: |
| [`@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 | | | |
| [`@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: | |
| [`@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: |
| [`@typescript-eslint/prefer-optional-chain`](./docs/rules/prefer-optional-chain.md) | Prefer using concise optional chain expressions instead of chained logical ands | | | |
Expand Down
51 changes: 51 additions & 0 deletions packages/eslint-plugin/docs/rules/prefer-literal-enum-member.md
@@ -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.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -100,6 +100,7 @@ export = {
'@typescript-eslint/prefer-for-of': 'error',
'@typescript-eslint/prefer-function-type': 'error',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-literal-enum-member': 'error',
'@typescript-eslint/prefer-namespace-keyword': 'error',
'@typescript-eslint/prefer-nullish-coalescing': 'error',
'@typescript-eslint/prefer-optional-chain': 'error',
Expand Down
8 changes: 5 additions & 3 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -17,7 +17,9 @@ import explicitMemberAccessibility from './explicit-member-accessibility';
import explicitModuleBoundaryTypes from './explicit-module-boundary-types';
import funcCallSpacing from './func-call-spacing';
import indent from './indent';
import initDeclarations from './init-declarations';
import keywordSpacing from './keyword-spacing';
import linesBetweenClassMembers from './lines-between-class-members';
import memberDelimiterStyle from './member-delimiter-style';
import memberOrdering from './member-ordering';
import methodSignatureStyle from './method-signature-style';
Expand All @@ -35,10 +37,12 @@ import noExtraParens from './no-extra-parens';
import noExtraSemi from './no-extra-semi';
import noFloatingPromises from './no-floating-promises';
import noForInArray from './no-for-in-array';
import preferLiteralEnumMember from './prefer-literal-enum-member';
import noImpliedEval from './no-implied-eval';
import noInferrableTypes from './no-inferrable-types';
import noInvalidThis from './no-invalid-this';
import noInvalidVoidType from './no-invalid-void-type';
import noLossOfPrecision from './no-loss-of-precision';
import noMagicNumbers from './no-magic-numbers';
import noMisusedNew from './no-misused-new';
import noMisusedPromises from './no-misused-promises';
Expand Down Expand Up @@ -85,7 +89,6 @@ import requireAwait from './require-await';
import restrictPlusOperands from './restrict-plus-operands';
import restrictTemplateExpressions from './restrict-template-expressions';
import returnAwait from './return-await';
import initDeclarations from './init-declarations';
import semi from './semi';
import spaceBeforeFunctionParen from './space-before-function-paren';
import strictBooleanExpressions from './strict-boolean-expressions';
Expand All @@ -95,8 +98,6 @@ import typeAnnotationSpacing from './type-annotation-spacing';
import typedef from './typedef';
import unboundMethod from './unbound-method';
import unifiedSignatures from './unified-signatures';
import linesBetweenClassMembers from './lines-between-class-members';
import noLossOfPrecision from './no-loss-of-precision';

export default {
'adjacent-overload-signatures': adjacentOverloadSignatures,
Expand Down Expand Up @@ -171,6 +172,7 @@ export default {
'prefer-for-of': preferForOf,
'prefer-function-type': preferFunctionType,
'prefer-includes': preferIncludes,
'prefer-literal-enum-member': preferLiteralEnumMember,
'prefer-namespace-keyword': preferNamespaceKeyword,
'prefer-nullish-coalescing': preferNullishCoalescing,
'prefer-optional-chain': preferOptionalChain,
Expand Down
37 changes: 37 additions & 0 deletions packages/eslint-plugin/src/rules/prefer-literal-enum-member.ts
@@ -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 packages/eslint-plugin/tests/rules/prefer-literal-enum-member.test.ts
@@ -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,
},
],
},
],
});

0 comments on commit fe2b2ec

Please sign in to comment.