diff --git a/packages/eslint-plugin/docs/rules/no-enum-literals.md b/packages/eslint-plugin/docs/rules/no-enum-literals.md new file mode 100644 index 00000000000..fd09e220291 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-enum-literals.md @@ -0,0 +1,67 @@ +# Disallows usage of literals instead of enums + +## Rule Details + +It's possible to use number of string literals instead of enum values. This rule disallows using string or number literals instead of enum values. + +Examples of **incorrect** code for this rule: + +```ts +enum Foo { + ONE, + TWO, +} + +function foo(f: Foo) { + if (f === 1) { + } + + let ff: Foo; + + ff = 1; +} +foo(1); + +enum Bar { + ONE = 'ONE', + TWO = 'TWO', +} + +function bar(b: Bar) { + if (b === 'ONE') { + } +} +``` + +Examples of **correct** code for this rule: + +```ts +enum Foo { + ONE, + TWO, +} + +function foo(f: Foo) { + if (f === Foo.ONE) { + } + + let ff: Foo; + + ff = Foo.TWO; +} +foo(1); + +enum Bar { + ONE = 'ONE', + TWO = 'TWO', +} + +function bar(b: Bar) { + if (b === Bar.ONE) { + } +} +``` + +## When Not To Use It + +If you want to allow usage of literals instead of enums diff --git a/packages/eslint-plugin/src/rules/no-enum-literals.ts b/packages/eslint-plugin/src/rules/no-enum-literals.ts new file mode 100644 index 00000000000..912cd27ced8 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-enum-literals.ts @@ -0,0 +1,99 @@ +import ts from 'typescript'; +import * as util from '../util'; +import { TSESTree } from '@typescript-eslint/typescript-estree'; + +export default util.createRule({ + name: 'no-enum-literals', + meta: { + type: 'suggestion', + docs: { + description: 'Disallows usage of literals instead of enums', + category: 'Best Practices', + recommended: 'error', + }, + messages: { + noLiterals: 'Do not use literal values instead of enums', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + + /** + * is node an identifier with type of an enum + * @param node identifier node + */ + function isNodeEnumIdentifier(node: TSESTree.Node): boolean { + const originalNode = parserServices.esTreeNodeToTSNodeMap.get< + ts.Identifier + >(node); + const type = checker.getTypeAtLocation(originalNode); + + if (!type.symbol) { + return false; + } + + const { name } = type.symbol; + + return !['Number', 'String'].includes(name); + } + + function isNumberOrStringLiteral( + node: TSESTree.Node, + ): node is TSESTree.Literal { + return ( + node.type === 'Literal' && + ['number', 'string'].includes(typeof node.value) + ); + } + + return { + AssignmentExpression(node) { + if ( + isNodeEnumIdentifier(node.left) && + isNumberOrStringLiteral(node.right) + ) { + context.report({ + node: node.right, + messageId: 'noLiterals', + }); + } + }, + BinaryExpression(node) { + if ( + isNodeEnumIdentifier(node.left) && + isNumberOrStringLiteral(node.right) + ) { + context.report({ + node: node.right, + messageId: 'noLiterals', + }); + } + + if ( + isNumberOrStringLiteral(node.left) && + isNodeEnumIdentifier(node.right) + ) { + context.report({ + node: node.left, + messageId: 'noLiterals', + }); + } + }, + VariableDeclarator(node) { + if ( + isNodeEnumIdentifier(node.id) && + node.init && + isNumberOrStringLiteral(node.init) + ) { + context.report({ + node: node.init, + messageId: 'noLiterals', + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-enum-literals.test.ts b/packages/eslint-plugin/tests/rules/no-enum-literals.test.ts new file mode 100644 index 00000000000..80bec392ce9 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-enum-literals.test.ts @@ -0,0 +1,226 @@ +import rule from '../../src/rules/no-enum-literals'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-enum-literals', rule, { + valid: [ + // Make sure non-enum BinaryExpressions are still valid + '0 === 0', + 'var a = 1', + 'var b = "str"', + ` +var a = 1; +(a === 0)`, + ` +var a = 1; +(0 === a)`, + ` +var a = 1; +(a === a)`, + ` +var a = "str"; +(a === "other")`, + + // Make sure non-enum VariableDeclaration are still valid + 'var a: number = 1', + 'var a: string = "str"', + + // Enum cases + ` +enum Foo { ONE, TWO }; +let f: Foo; +(f === Foo.ONE);`, + ` +enum Foo { ONE, TWO }; +let f: Foo; +(Foo.ONE === f);`, + ` +enum Foo { ONE, TWO }; +let f = Foo.ONE;`, + ` +enum Foo { ONE, TWO }; +let f: Foo; +f = Foo.ONE;`, + ` +enum Foo { ONE, TWO }; +function foo(f: Foo) {}; +foo(Foo.ONE);`, + ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +(f === Foo.ONE);`, + ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +(Foo.ONE === f);`, + ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +f = Foo.ONE;`, + ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +f = Foo.ONE;`, + ` +enum Foo { ONE = 1, TWO = 2 }; +function foo(f: Foo) {}; +foo(Foo.ONE);`, + ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +let f: Foo; +(f === Foo.ONE);`, + ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +let f: Foo; +(Foo.ONE === f);`, + ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +let f: Foo; +f = Foo.ONE;`, + ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +let f: Foo = Foo.ONE;`, + ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +function foo(f: Foo) {}; +foo(Foo.ONE);`, + ], + + invalid: [ + { + code: ` +enum Foo { ONE, TWO }; +let f: Foo; +(f === 0);`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 8, + }, + ], + }, + { + code: ` +enum Foo { ONE, TWO }; +let f: Foo; +(0 === f);`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 2, + }, + ], + }, + { + code: ` +enum Foo { ONE, TWO }; +let f: Foo; +f = 0;`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 5, + }, + ], + }, + + { + code: ` +enum Foo { ONE, TWO }; +let f: Foo = 0;`, + errors: [ + { + messageId: 'noLiterals', + line: 3, + column: 14, + }, + ], + }, + { + code: ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +(f === 0);`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 8, + }, + ], + }, + { + code: ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +(0 === f);`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 2, + }, + ], + }, + { + code: ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo; +f = 0;`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 5, + }, + ], + }, + { + code: ` +enum Foo { ONE = 1, TWO = 2 }; +let f: Foo = 0;`, + errors: [ + { + messageId: 'noLiterals', + line: 3, + column: 14, + }, + ], + }, + { + code: ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +let f: Foo; +(f === "ONE");`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 8, + }, + ], + }, + { + code: ` +enum Foo { ONE = "ONE", TWO = "TWO" }; +let f: Foo; +("ONE" === f);`, + errors: [ + { + messageId: 'noLiterals', + line: 4, + column: 2, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/ts-eslint.d.ts b/packages/eslint-plugin/typings/ts-eslint.d.ts index 998c6958342..421ac358a9c 100644 --- a/packages/eslint-plugin/typings/ts-eslint.d.ts +++ b/packages/eslint-plugin/typings/ts-eslint.d.ts @@ -404,8 +404,10 @@ declare module 'ts-eslint' { ArrayExpression?: RuleFunction; ArrayPattern?: RuleFunction; ArrowFunctionExpression?: RuleFunction; + AssignmentExpression?: RuleFunction; AssignmentPattern?: RuleFunction; AwaitExpression?: RuleFunction; + BinaryExpression?: RuleFunction; BlockStatement?: RuleFunction; BreakStatement?: RuleFunction; CallExpression?: RuleFunction;