Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork2.8k
feat(eslint-plugin): [no-unsafe-enum-comparison] add rule#6107
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
481fb44
5776a01
094c90e
311b1f8
ac5e6d9
7d55248
74af3d1
1eb70ee
cf006fd
4d7f3fe
2bbc2b7
75c2252
3976fdc
2406d03
4244376
8b4dcdc
12f8120
e638718
aef971a
75f60b9
d784aa0
811de98
9a71d45
f93a02a
2ced9f8
0695c1b
7272a95
6ed1f66
83c70e3
0ff3096
ddebded
c8b239b
90981a9
32dec63
d0dbcbb
c63c518
c5cf7f3
1b26d74
6e4f705
a0a2ee6
dbae5db
16965d9
8b8f8f8
5f437cc
b1dbeb6
25caa05
f42de61
a866ddd
61cf107
304d796
83c5f30
23ac918
098d732
99424e1
0de3b26
2d488cc
15dc3df
a3d0122
5cd8fa7
a236085
c357432
2395998
2851c3b
149c049
cf7c9b7
3b2fa57
6c43f71
af7d5da
a50f6e1
85563be
ee96ce5
943bf9f
7679c4a
871e9ca
f861606
cbe9e90
c37165c
3f37c67
c942261
b2947f9
a7772d2
4204a60
89bcd73
4f13db4
4706e4f
be144e3
e4ac325
62ccdb1
da7501a
83c33cb
a3e6236
f1071fa
680293a
51bc2b1
14cceea
bc65e3c
adf6f2e
68ad65c
57376a7
559c0ab
b1c9bec
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
--- | ||
description: 'Disallow comparing an enum value with a non-enum value.' | ||
--- | ||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/no-unsafe-enum-comparison** for documentation. | ||
The TypeScript compiler can be surprisingly lenient when working with enums. | ||
For example, it will allow you to compare enum values against numbers even though they might not have any type overlap: | ||
```ts | ||
enum Fruit { | ||
Apple, | ||
Banana, | ||
} | ||
declare let fruit: Fruit; | ||
fruit === 999; // No error | ||
``` | ||
This rule flags when an enum typed value is compared to a non-enum `number`. | ||
<!--tabs--> | ||
### ❌ Incorrect | ||
```ts | ||
enum Fruit { | ||
Apple, | ||
} | ||
declare let fruit: Fruit; | ||
fruit === 999; | ||
``` | ||
```ts | ||
enum Vegetable { | ||
Asparagus = 'asparagus', | ||
} | ||
declare let vegetable: Vegetable; | ||
vegetable === 'asparagus'; | ||
``` | ||
### ✅ Correct | ||
```ts | ||
enum Fruit { | ||
Apple, | ||
} | ||
declare let fruit: Fruit; | ||
fruit === Fruit.Banana; | ||
``` | ||
```ts | ||
enum Vegetable { | ||
Asparagus = 'asparagus', | ||
} | ||
declare let vegetable: Vegetable; | ||
vegetable === Vegetable.Asparagus; | ||
``` | ||
<!--/tabs--> | ||
## When Not to Use It | ||
If you don't mind number and/or literal string constants being compared against enums, you likely don't need this rule. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -29,6 +29,7 @@ export = { | ||
'@typescript-eslint/no-unnecessary-condition': 'warn', | ||
'@typescript-eslint/no-unnecessary-type-arguments': 'warn', | ||
'@typescript-eslint/no-unsafe-declaration-merging': 'warn', | ||
'@typescript-eslint/no-unsafe-enum-comparison': 'warn', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. changing the recommended configs is considered a breaking change There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Oh shoot There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Ah wait, perhttps://typescript-eslint.io/linting/configs, Still filing#6890 just in case. | ||
'no-useless-constructor': 'off', | ||
'@typescript-eslint/no-useless-constructor': 'warn', | ||
'@typescript-eslint/non-nullable-type-assertion-style': 'warn', | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
import * as util from '../../util'; | ||
/* | ||
* If passed an enum member, returns the type of the parent. Otherwise, | ||
* returns itself. | ||
* | ||
* For example: | ||
* - `Fruit` --> `Fruit` | ||
* - `Fruit.Apple` --> `Fruit` | ||
*/ | ||
function getBaseEnumType(typeChecker: ts.TypeChecker, type: ts.Type): ts.Type { | ||
const symbol = type.getSymbol()!; | ||
if (!tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.EnumMember)) { | ||
return type; | ||
} | ||
return typeChecker.getTypeAtLocation(symbol.valueDeclaration!.parent); | ||
} | ||
/** | ||
* A type can have 0 or more enum types. For example: | ||
* - 123 --> [] | ||
* - {} --> [] | ||
* - Fruit.Apple --> [Fruit] | ||
* - Fruit.Apple | Vegetable.Lettuce --> [Fruit, Vegetable] | ||
* - Fruit.Apple | Vegetable.Lettuce | 123 --> [Fruit, Vegetable] | ||
* - T extends Fruit --> [Fruit] | ||
*/ | ||
export function getEnumTypes( | ||
typeChecker: ts.TypeChecker, | ||
type: ts.Type, | ||
): ts.Type[] { | ||
return tsutils | ||
.unionTypeParts(type) | ||
.filter(subType => util.isTypeFlagSet(subType, ts.TypeFlags.EnumLiteral)) | ||
.map(type => getBaseEnumType(typeChecker, type)); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import type { TSESTree } from '@typescript-eslint/utils'; | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
import * as util from '../util'; | ||
import { getEnumTypes } from './enum-utils/shared'; | ||
/** | ||
* @returns Whether the right type is an unsafe comparison against any left type. | ||
*/ | ||
function typeViolates(leftTypeParts: ts.Type[], right: ts.Type): boolean { | ||
const leftValueKinds = new Set(leftTypeParts.map(getEnumValueType)); | ||
return ( | ||
(leftValueKinds.has(ts.TypeFlags.Number) && | ||
tsutils.isTypeFlagSet( | ||
right, | ||
ts.TypeFlags.Number | ts.TypeFlags.NumberLike, | ||
)) || | ||
(leftValueKinds.has(ts.TypeFlags.String) && | ||
tsutils.isTypeFlagSet( | ||
right, | ||
ts.TypeFlags.String | ts.TypeFlags.StringLike, | ||
)) | ||
); | ||
} | ||
/** | ||
* @returns What type a type's enum value is (number or string), if either. | ||
*/ | ||
function getEnumValueType(type: ts.Type): ts.TypeFlags | undefined { | ||
return util.isTypeFlagSet(type, ts.TypeFlags.EnumLike) | ||
? util.isTypeFlagSet(type, ts.TypeFlags.NumberLiteral) | ||
? ts.TypeFlags.Number | ||
: ts.TypeFlags.String | ||
: undefined; | ||
} | ||
export default util.createRule({ | ||
name: 'no-unsafe-enum-comparison', | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Disallow comparing an enum value with a non-enum value', | ||
recommended: 'strict', | ||
requiresTypeChecking: true, | ||
}, | ||
messages: { | ||
mismatched: | ||
'The two values in this comparison do not have a shared enum type.', | ||
}, | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
const parserServices = util.getParserServices(context); | ||
const typeChecker = parserServices.program.getTypeChecker(); | ||
function getTypeFromNode(node: TSESTree.Node): ts.Type { | ||
return typeChecker.getTypeAtLocation( | ||
parserServices.esTreeNodeToTSNodeMap.get(node), | ||
); | ||
} | ||
return { | ||
'BinaryExpression[operator=/=|<|>/]'( | ||
armano2 marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading.Please reload this page. | ||
node: TSESTree.BinaryExpression, | ||
): void { | ||
const left = getTypeFromNode(node.left); | ||
const right = getTypeFromNode(node.right); | ||
// Allow comparisons that don't have anything to do with enums: | ||
// | ||
// ```ts | ||
// 1 === 2; | ||
// ``` | ||
const leftEnumTypes = getEnumTypes(typeChecker, left); | ||
const rightEnumTypes = new Set(getEnumTypes(typeChecker, right)); | ||
if (leftEnumTypes.length === 0 && rightEnumTypes.size === 0) { | ||
return; | ||
} | ||
// Allow comparisons that share an enum type: | ||
// | ||
// ```ts | ||
// Fruit.Apple === Fruit.Banana; | ||
// ``` | ||
for (const leftEnumType of leftEnumTypes) { | ||
if (rightEnumTypes.has(leftEnumType)) { | ||
return; | ||
} | ||
} | ||
const leftTypeParts = tsutils.unionTypeParts(left); | ||
const rightTypeParts = tsutils.unionTypeParts(right); | ||
// If a type exists in both sides, we consider this comparison safe: | ||
// | ||
// ```ts | ||
// declare const fruit: Fruit.Apple | 0; | ||
// fruit === 0; | ||
// ``` | ||
for (const leftTypePart of leftTypeParts) { | ||
if (rightTypeParts.includes(leftTypePart)) { | ||
return; | ||
} | ||
} | ||
if ( | ||
typeViolates(leftTypeParts, right) || | ||
typeViolates(rightTypeParts, left) | ||
) { | ||
context.report({ | ||
messageId: 'mismatched', | ||
node, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
Uh oh!
There was an error while loading.Please reload this page.