-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): new rule: non-nullable-type-assertion-style
- Loading branch information
1 parent
33522b4
commit 33853dd
Showing
7 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
packages/eslint-plugin/docs/rules/non-nullable-type-assertion-style.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# Prefers a non-null assertion over explicit type cast when possible (`non-nullable-type-assertion-style`) | ||
|
||
This rule detects when an `as` cast is doing the same job as a `!` would, and suggests fixing the code to be an `!`. | ||
|
||
## Rule Details | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```ts | ||
const maybe = Math.random() > 0.5 ? '' : undefined; | ||
|
||
const definitely = maybe as string; | ||
const alsoDefinitely = <string>maybe; | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```ts | ||
const maybe = Math.random() > 0.5 ? '' : undefined; | ||
|
||
const definitely = maybe!; | ||
const alsoDefinitely = maybe!; | ||
``` | ||
|
||
## When Not To Use It | ||
|
||
If you don't mind having unnecessarily verbose type casts, you can avoid this rule. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
packages/eslint-plugin/src/rules/non-nullable-type-assertion-style.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { TSESTree } from '@typescript-eslint/experimental-utils'; | ||
import * as tsutils from 'tsutils'; | ||
import * as ts from 'typescript'; | ||
|
||
import * as util from '../util'; | ||
|
||
export default util.createRule({ | ||
name: 'non-nullable-type-assertion-style', | ||
meta: { | ||
docs: { | ||
category: 'Best Practices', | ||
description: | ||
'Prefers a non-null assertion over explicit type cast when possible', | ||
recommended: false, | ||
requiresTypeChecking: true, | ||
suggestion: true, | ||
}, | ||
fixable: 'code', | ||
messages: { | ||
preferNonNullAssertion: | ||
'Use a ! assertion to more succintly remove null and undefined from the type.', | ||
}, | ||
schema: [], | ||
type: 'suggestion', | ||
}, | ||
defaultOptions: [], | ||
|
||
create(context) { | ||
const parserServices = util.getParserServices(context); | ||
const checker = parserServices.program.getTypeChecker(); | ||
const sourceCode = context.getSourceCode(); | ||
|
||
const getTypesIfNotLoose = (node: TSESTree.Node): ts.Type[] | undefined => { | ||
const type = checker.getTypeAtLocation( | ||
parserServices.esTreeNodeToTSNodeMap.get(node), | ||
); | ||
|
||
return tsutils.isTypeFlagSet( | ||
type, | ||
ts.TypeFlags.Any | ts.TypeFlags.Unknown, | ||
) | ||
? undefined | ||
: tsutils.unionTypeParts(type); | ||
}; | ||
|
||
const sameTypeWithoutNullish = ( | ||
assertedTypes: ts.Type[], | ||
originalTypes: ts.Type[], | ||
): boolean => { | ||
const assertedTypeIds = new Set(assertedTypes.map(type => type.id)); | ||
const nonNullishOriginalTypes = originalTypes.filter( | ||
type => | ||
type.flags !== ts.TypeFlags.Null && | ||
type.flags !== ts.TypeFlags.Undefined, | ||
); | ||
const nonNullishOriginalTypeIds = new Set( | ||
nonNullishOriginalTypes.map(type => type.id), | ||
); | ||
|
||
for (const assertedType of assertedTypes) { | ||
if (!nonNullishOriginalTypeIds.has(assertedType.id)) { | ||
return false; | ||
} | ||
} | ||
|
||
for (const originalType of nonNullishOriginalTypes) { | ||
if (!assertedTypeIds.has(originalType.id)) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
}; | ||
|
||
return { | ||
'TSAsExpression, TSTypeAssertion'( | ||
node: TSESTree.TSTypeAssertion | TSESTree.TSAsExpression, | ||
): void { | ||
const originalTypes = getTypesIfNotLoose(node.expression); | ||
if (!originalTypes) { | ||
return; | ||
} | ||
|
||
const assertedTypes = getTypesIfNotLoose(node.typeAnnotation); | ||
if (!assertedTypes) { | ||
return; | ||
} | ||
|
||
if (sameTypeWithoutNullish(assertedTypes, originalTypes)) { | ||
context.report({ | ||
fix(fixer) { | ||
return fixer.replaceText( | ||
node, | ||
`${sourceCode.getText(node.expression)}!`, | ||
); | ||
}, | ||
messageId: 'preferNonNullAssertion', | ||
node, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
139 changes: 139 additions & 0 deletions
139
packages/eslint-plugin/tests/rules/non-nullable-type-assertion-style.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import path from 'path'; | ||
import rule from '../../src/rules/non-nullable-type-assertion-style'; | ||
import { RuleTester } from '../RuleTester'; | ||
|
||
const rootDir = path.resolve(__dirname, '../fixtures/'); | ||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
sourceType: 'module', | ||
tsconfigRootDir: rootDir, | ||
project: './tsconfig.json', | ||
}, | ||
parser: '@typescript-eslint/parser', | ||
}); | ||
|
||
ruleTester.run('non-nullable-type-assertion-style', rule, { | ||
valid: [ | ||
` | ||
declare const original: number | string; | ||
const cast = original as string; | ||
`, | ||
` | ||
declare const original: number | undefined; | ||
const cast = original as string | number | undefined; | ||
`, | ||
` | ||
declare const original: number | any; | ||
const cast = original as string | number | undefined; | ||
`, | ||
` | ||
declare const original: number | undefined; | ||
const cast = original as any; | ||
`, | ||
` | ||
declare const original: number | null | undefined; | ||
const cast = original as number | null; | ||
`, | ||
` | ||
type Type = { value: string }; | ||
declare const original: Type | number; | ||
const cast = original as Type; | ||
`, | ||
], | ||
|
||
invalid: [ | ||
{ | ||
code: ` | ||
declare const maybe: string | undefined; | ||
const bar = maybe as string; | ||
`, | ||
errors: [ | ||
{ | ||
column: 13, | ||
line: 3, | ||
messageId: 'preferNonNullAssertion', | ||
}, | ||
], | ||
output: ` | ||
declare const maybe: string | undefined; | ||
const bar = maybe!; | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
declare const maybe: string | null; | ||
const bar = maybe as string; | ||
`, | ||
errors: [ | ||
{ | ||
column: 13, | ||
line: 3, | ||
messageId: 'preferNonNullAssertion', | ||
}, | ||
], | ||
output: ` | ||
declare const maybe: string | null; | ||
const bar = maybe!; | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
declare const maybe: string | null | undefined; | ||
const bar = maybe as string; | ||
`, | ||
errors: [ | ||
{ | ||
column: 13, | ||
line: 3, | ||
messageId: 'preferNonNullAssertion', | ||
}, | ||
], | ||
output: ` | ||
declare const maybe: string | null | undefined; | ||
const bar = maybe!; | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
type Type = { value: string }; | ||
declare const maybe: Type | undefined; | ||
const bar = maybe as Type; | ||
`, | ||
errors: [ | ||
{ | ||
column: 13, | ||
line: 4, | ||
messageId: 'preferNonNullAssertion', | ||
}, | ||
], | ||
output: ` | ||
type Type = { value: string }; | ||
declare const maybe: Type | undefined; | ||
const bar = maybe!; | ||
`, | ||
}, | ||
{ | ||
code: ` | ||
interface Interface { | ||
value: string; | ||
} | ||
declare const maybe: Interface | undefined; | ||
const bar = maybe as Interface; | ||
`, | ||
errors: [ | ||
{ | ||
column: 13, | ||
line: 6, | ||
messageId: 'preferNonNullAssertion', | ||
}, | ||
], | ||
output: ` | ||
interface Interface { | ||
value: string; | ||
} | ||
declare const maybe: Interface | undefined; | ||
const bar = maybe!; | ||
`, | ||
}, | ||
], | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters