-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(eslint-plugin): new rule no-unsafe-type-assertion
#10051
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
Merged
JoshuaKGoldberg
merged 28 commits into
typescript-eslint:main
from
ronami:no-unsafe-type-assertion
Nov 14, 2024
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
8f38cec
initial implementation
ronami 037978c
tests
ronami a50625b
docs
ronami 15ce7f8
more tests
ronami 7c44606
use checker.typeToString() over getTypeName()
ronami cc4ea8b
use link
ronami e31cf39
oops
ronami 8a8965f
add tests
ronami 5bbe0c9
Merge branch 'main' into no-unsafe-type-assertion
ronami f0c429e
Merge branch 'main' into no-unsafe-type-assertion
ronami 08577a8
remove unnecessary typescript 5.4 warning
ronami 2ab1c87
adjust format to new rules
ronami 8f29e89
Merge branch 'main' into no-unsafe-type-assertion
ronami a5dbd5b
update error message to be more concise
ronami ff73c4d
match implementation to be inline with no-unsafe-* rules
ronami 913529f
rework tests
ronami 88d15de
refactor
ronami c4f7ce7
update snapshots
ronami c34f7c1
fix error message showing original type instead of asserted type
ronami e93cca5
update snapshots
ronami 1d32280
add a warning for object stubbing on test files
ronami d4c5236
fix linting
ronami 03ae3bc
adjust test to lint fixes
ronami 9980c9e
simplify type comparison
ronami 8b0912f
rework code-comments and rename variables
ronami 07ae890
rework the opening paragraph to make it more beginner-friendly
ronami 5d8c521
Update packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx
ronami 033fd0b
fix: narrow/widen in description
JoshuaKGoldberg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Diff view
There are no files selected for viewing
63 changes: 63 additions & 0 deletions
63
packages/eslint-plugin/docs/rules/no-unsafe-type-assertion.mdx
This file contains hidden or 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,63 @@ | ||
--- | ||
description: 'Disallow type assertions that narrow a type.' | ||
--- | ||
|
||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
> 🛑 This file is source code, not the primary documentation location! 🛑 | ||
> | ||
> See **https://typescript-eslint.io/rules/no-unsafe-type-assertion** for documentation. | ||
|
||
Type assertions are a way to tell TypeScript what the type of a value is. This can be useful but also unsafe if you use type assertions to narrow down a type. | ||
|
||
This rule forbids using type assertions to narrow a type, as this bypasses TypeScript's type-checking. Type assertions that broaden a type are safe because TypeScript essentially knows _less_ about a type. | ||
|
||
Instead of using type assertions to narrow a type, it's better to rely on type guards, which help avoid potential runtime errors caused by unsafe type assertions. | ||
|
||
## Examples | ||
|
||
<Tabs> | ||
<TabItem value="❌ Incorrect"> | ||
|
||
```ts | ||
function f() { | ||
return Math.random() < 0.5 ? 42 : 'oops'; | ||
} | ||
|
||
const z = f() as number; | ||
|
||
const items = [1, '2', 3, '4']; | ||
|
||
const number = items[0] as number; | ||
``` | ||
|
||
</TabItem> | ||
<TabItem value="✅ Correct"> | ||
|
||
```ts | ||
function f() { | ||
return Math.random() < 0.5 ? 42 : 'oops'; | ||
} | ||
|
||
const z = f() as number | string | boolean; | ||
|
||
const items = [1, '2', 3, '4']; | ||
|
||
const number = items[0] as number | string | undefined; | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
## When Not To Use It | ||
|
||
If your codebase has many unsafe type assertions, then it may be difficult to enable this rule. | ||
It may be easier to skip the `no-unsafe-*` rules pending increasing type safety in unsafe areas of your project. | ||
You might consider using [ESLint disable comments](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1) for those specific situations instead of completely disabling this rule. | ||
|
||
If your project frequently stubs objects in test files, the rule may trigger a lot of reports. Consider disabling the rule for such files to reduce frequent warnings. | ||
|
||
## Further Reading | ||
|
||
- More on TypeScript's [type assertions](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-assertions) |
This file contains hidden or 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 hidden or 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 hidden or 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
146 changes: 146 additions & 0 deletions
146
packages/eslint-plugin/src/rules/no-unsafe-type-assertion.ts
JoshuaKGoldberg marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or 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,146 @@ | ||
import type { TSESTree } from '@typescript-eslint/utils'; | ||
|
||
import * as tsutils from 'ts-api-utils'; | ||
import * as ts from 'typescript'; | ||
|
||
import { | ||
createRule, | ||
getConstrainedTypeAtLocation, | ||
getParserServices, | ||
isTypeAnyType, | ||
isTypeUnknownType, | ||
isUnsafeAssignment, | ||
} from '../util'; | ||
|
||
export default createRule({ | ||
name: 'no-unsafe-type-assertion', | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Disallow type assertions that narrow a type', | ||
requiresTypeChecking: true, | ||
}, | ||
messages: { | ||
unsafeOfAnyTypeAssertion: | ||
'Unsafe cast from {{type}} detected: consider using type guards or a safer cast.', | ||
unsafeToAnyTypeAssertion: | ||
'Unsafe cast to {{type}} detected: consider using a more specific type to ensure safety.', | ||
unsafeTypeAssertion: | ||
"Unsafe type assertion: type '{{type}}' is more narrow than the original type.", | ||
}, | ||
schema: [], | ||
}, | ||
defaultOptions: [], | ||
create(context) { | ||
const services = getParserServices(context); | ||
const checker = services.program.getTypeChecker(); | ||
|
||
function getAnyTypeName(type: ts.Type): string { | ||
return tsutils.isIntrinsicErrorType(type) ? 'error typed' : '`any`'; | ||
} | ||
|
||
function isObjectLiteralType(type: ts.Type): boolean { | ||
return ( | ||
tsutils.isObjectType(type) && | ||
tsutils.isObjectFlagSet(type, ts.ObjectFlags.ObjectLiteral) | ||
); | ||
} | ||
|
||
function checkExpression( | ||
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, | ||
): void { | ||
const expressionType = getConstrainedTypeAtLocation( | ||
services, | ||
node.expression, | ||
); | ||
const assertedType = getConstrainedTypeAtLocation( | ||
services, | ||
node.typeAnnotation, | ||
); | ||
|
||
if (expressionType === assertedType) { | ||
return; | ||
} | ||
|
||
// handle cases when casting unknown ==> any. | ||
if (isTypeAnyType(assertedType) && isTypeUnknownType(expressionType)) { | ||
context.report({ | ||
node, | ||
messageId: 'unsafeToAnyTypeAssertion', | ||
data: { | ||
type: '`any`', | ||
}, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
const unsafeExpressionAny = isUnsafeAssignment( | ||
expressionType, | ||
assertedType, | ||
checker, | ||
node.expression, | ||
); | ||
|
||
if (unsafeExpressionAny) { | ||
context.report({ | ||
node, | ||
messageId: 'unsafeOfAnyTypeAssertion', | ||
data: { | ||
type: getAnyTypeName(unsafeExpressionAny.sender), | ||
}, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
const unsafeAssertedAny = isUnsafeAssignment( | ||
assertedType, | ||
expressionType, | ||
checker, | ||
node.typeAnnotation, | ||
); | ||
|
||
if (unsafeAssertedAny) { | ||
context.report({ | ||
node, | ||
messageId: 'unsafeToAnyTypeAssertion', | ||
data: { | ||
type: getAnyTypeName(unsafeAssertedAny.sender), | ||
}, | ||
}); | ||
|
||
return; | ||
} | ||
|
||
// Use the widened type in case of an object literal so `isTypeAssignableTo()` | ||
// won't fail on excess property check. | ||
const nodeWidenedType = isObjectLiteralType(expressionType) | ||
? checker.getWidenedType(expressionType) | ||
: expressionType; | ||
|
||
const isAssertionSafe = checker.isTypeAssignableTo( | ||
nodeWidenedType, | ||
assertedType, | ||
); | ||
|
||
if (!isAssertionSafe) { | ||
context.report({ | ||
node, | ||
messageId: 'unsafeTypeAssertion', | ||
data: { | ||
type: checker.typeToString(assertedType), | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
return { | ||
'TSAsExpression, TSTypeAssertion'( | ||
node: TSESTree.TSAsExpression | TSESTree.TSTypeAssertion, | ||
): void { | ||
checkExpression(node); | ||
}, | ||
}; | ||
}, | ||
}); |
33 changes: 33 additions & 0 deletions
33
packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-unsafe-type-assertion.shot
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.