Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): add rule prefer-literal-enum-member #1898

Merged
merged 12 commits into from Jul 6, 2020
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -117,6 +117,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-extraneous-class`](./docs/rules/no-extraneous-class.md) | Forbids the use of classes as namespaces | | | |
| [`@typescript-eslint/no-floating-promises`](./docs/rules/no-floating-promises.md) | Requires Promise-like values to be handled appropriately | | | :thought_balloon: |
| [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-identifier-enum-member`](./docs/rules/no-identifier-enum-member.md) | Disallow identifier (aka variable) enum members | | | |
| [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon: |
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallows usage of `void` type outside of generic or return types | | | |
Expand Down
56 changes: 56 additions & 0 deletions packages/eslint-plugin/docs/rules/no-identifier-enum-member.md
@@ -0,0 +1,56 @@
# Disallow identifier (aka variable) enum members (`no-identifier-enum-member`)

TypeScript allows the value of an enum member to be any valid JavaScript expression. 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 prohibiting the use of identifiers (aka variables) as enum members to prevent unexpected runtime behavior. All other values should be allowed since they do not have this particular issue.

Examples of **incorrect** code for this rule:

```ts
const str = 'Test';
enum Invalid {
A = str, // Variable assignment
}
oigewan marked this conversation as resolved.
Show resolved Hide resolved
```

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/,
F = 2 + 2, // Expressions
G = {}, // Objects
H = [], // Arrays
I = new Set([1, 2, 3]), // Any constructable class
}
oigewan marked this conversation as resolved.
Show resolved Hide resolved
```

## Options

There are no options.
oigewan marked this conversation as resolved.
Show resolved Hide resolved

## When Not To Use It

If you want the value of an enum member to be stored as a variable, do not use this rule.
oigewan marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.json
Expand Up @@ -52,6 +52,7 @@
"@typescript-eslint/no-extraneous-class": "error",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-identifier-enum-member": "error",
"@typescript-eslint/no-implied-eval": "error",
"@typescript-eslint/no-inferrable-types": "error",
"no-invalid-this": "off",
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -39,6 +39,7 @@ 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 noIdentifierEnumMember from './no-identifier-enum-member';
import noImpliedEval from './no-implied-eval';
import noInferrableTypes from './no-inferrable-types';
import noInvalidThis from './no-invalid-this';
Expand Down Expand Up @@ -180,6 +181,7 @@ export default {
'prefer-for-of': preferForOf,
'prefer-function-type': preferFunctionType,
'prefer-includes': preferIncludes,
'no-identifier-enum-member': noIdentifierEnumMember,
'prefer-namespace-keyword': preferNamespaceKeyword,
'prefer-nullish-coalescing': preferNullishCoalescing,
'prefer-optional-chain': preferOptionalChain,
Expand Down
46 changes: 46 additions & 0 deletions packages/eslint-plugin/src/rules/no-identifier-enum-member.ts
@@ -0,0 +1,46 @@
import { createRule } from '../util';
import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils';
import { TSEnumMember } from '../../../typescript-estree/dist/ts-estree/ts-estree';

export enum MessageId {
NoVariable = 'noVariable',
}

type Options = [];

export default createRule<Options, MessageId>({
name: 'no-identifier-enum-member',
meta: {
type: 'suggestion',
docs: {
description: 'Disallow identifier (aka variable) enum members',
category: 'Best Practices',
recommended: false,
requiresTypeChecking: false,
},
messages: {
[MessageId.NoVariable]: `Enum member can not be an ${AST_NODE_TYPES.Identifier}.`,
},
schema: [],
},
defaultOptions: [],
create(context) {
return {
TSEnumMember(node: TSEnumMember): 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.Identifier
) {
context.report({
node: node.id,
messageId: MessageId.NoVariable,
oigewan marked this conversation as resolved.
Show resolved Hide resolved
data: {
type: node.initializer.type,
},
});
}
},
};
},
});
@@ -0,0 +1,44 @@
import rule, { MessageId } from '../../src/rules/no-identifier-enum-member';
import { RuleTester } from '../RuleTester';

const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

ruleTester.run('prefer-literal-enum-member', rule, {
valid: [
`
enum Valid {
A = /test/,
B = 'test',
C = 42,
D = null,
E,
F = {},
G = [],
H = new Set(),
I = 2 + 2,
}
`,
oigewan marked this conversation as resolved.
Show resolved Hide resolved
],
oigewan marked this conversation as resolved.
Show resolved Hide resolved
invalid: [
{
code: `
const variable = 'Test';
enum Foo {
A = 'TestStr',
B = 2,
C,
V = variable,
}
`,
errors: [
{
messageId: MessageId.NoVariable,
line: 7,
column: 3,
},
],
},
],
});