[red-knot] Add support for `assert_never` (#17287)

## Summary

We already have partial "support" for `assert_never`, because it is
annotated as
```pyi
def assert_never(arg: Never, /) -> Never: ...
```
in typeshed. So we already emit a `invalid-argument-type` diagnostic if
the argument type to `assert_never` is not assignable to `Never`.

That is not enough, however. Gradual types like `Any`, `Unknown`,
`@Todo(…)` or `Any & int` can be assignable to `Never`. Which means that
we didn't issue any diagnostic in those cases.

Also, it seems like `assert_never` deserves a dedicated diagnostic
message, not just a generic "invalid argument type" error.

## Test Plan

New Markdown tests.
This commit is contained in:
David Peter 2025-04-08 09:31:49 +02:00 committed by GitHub
parent 97dd6d120c
commit b662c3ff7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 144 additions and 2 deletions

View File

@ -0,0 +1,106 @@
# `assert_never`
## Basic functionality
`assert_never` makes sure that the type of the argument is `Never`. If it is not, a
`type-assertion-failure` diagnostic is emitted.
```py
from typing_extensions import assert_never, Never, Any
from knot_extensions import Unknown
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
assert_never(never) # fine
assert_never(0) # error: [type-assertion-failure]
assert_never("") # error: [type-assertion-failure]
assert_never(None) # error: [type-assertion-failure]
assert_never([]) # error: [type-assertion-failure]
assert_never({}) # error: [type-assertion-failure]
assert_never(()) # error: [type-assertion-failure]
assert_never(1 if flag else never) # error: [type-assertion-failure]
assert_never(any_) # error: [type-assertion-failure]
assert_never(unknown) # error: [type-assertion-failure]
```
## Use case: Type narrowing and exhaustiveness checking
`assert_never` can be used in combination with type narrowing as a way to make sure that all cases
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
```py
from typing_extensions import assert_never, Literal
class A: ...
class B: ...
class C: ...
def if_else_isinstance_success(obj: A | B):
if isinstance(obj, A):
pass
elif isinstance(obj, B):
pass
elif isinstance(obj, C):
pass
else:
assert_never(obj)
def if_else_isinstance_error(obj: A | B):
if isinstance(obj, A):
pass
# B is missing
elif isinstance(obj, C):
pass
else:
# error: [type-assertion-failure] "Expected type `Never`, got `B & ~A & ~C` instead"
assert_never(obj)
def if_else_singletons_success(obj: Literal[1, "a"] | None):
if obj == 1:
pass
elif obj == "a":
pass
elif obj is None:
pass
else:
assert_never(obj)
def if_else_singletons_error(obj: Literal[1, "a"] | None):
if obj == 1:
pass
elif obj is "A": # "A" instead of "a"
pass
elif obj is None:
pass
else:
# error: [type-assertion-failure] "Expected type `Never`, got `Literal["a"]` instead"
assert_never(obj)
def match_singletons_success(obj: Literal[1, "a"] | None):
match obj:
case 1:
pass
case "a":
pass
case None:
pass
case _ as obj:
# TODO: Ideally, we would not emit an error here
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo"
assert_never(obj)
def match_singletons_error(obj: Literal[1, "a"] | None):
match obj:
case 1:
pass
case "A": # "A" instead of "a"
pass
case None:
pass
case _ as obj:
# TODO: We should emit an error here, but the message should
# show the type `Literal["a"]` instead of `@Todo(…)`.
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo"
assert_never(obj)
```

View File

@ -3024,6 +3024,24 @@ impl<'db> Type<'db> {
Signatures::single(signature)
}
Some(KnownFunction::AssertNever) => {
let signature = CallableSignature::single(
self,
Signature::new(
Parameters::new([Parameter::positional_only(Some(Name::new_static(
"arg",
)))
// We need to set the type to `Any` here (instead of `Never`),
// in order for every `assert_never` call to pass the argument
// check. If we set it to `Never`, we'll get invalid-argument-type
// errors instead of `type-assertion-failure` errors.
.with_annotated_type(Type::any())]),
Some(Type::none(db)),
),
);
Signatures::single(signature)
}
Some(KnownFunction::Cast) => {
let signature = CallableSignature::single(
self,
@ -4890,6 +4908,8 @@ pub enum KnownFunction {
/// `typing(_extensions).assert_type`
AssertType,
/// `typing(_extensions).assert_never`
AssertNever,
/// `typing(_extensions).cast`
Cast,
/// `typing(_extensions).overload`
@ -4947,6 +4967,7 @@ impl KnownFunction {
match self {
Self::IsInstance | Self::IsSubclass | Self::Len | Self::Repr => module.is_builtins(),
Self::AssertType
| Self::AssertNever
| Self::Cast
| Self::Overload
| Self::RevealType
@ -6407,6 +6428,7 @@ pub(crate) mod tests {
| KnownFunction::Overload
| KnownFunction::RevealType
| KnownFunction::AssertType
| KnownFunction::AssertNever
| KnownFunction::NoTypeCheck => KnownModule::TypingExtensions,
KnownFunction::IsSingleton

View File

@ -682,7 +682,7 @@ declare_lint! {
declare_lint! {
/// ## What it does
/// Checks for `assert_type()` calls where the actual type
/// Checks for `assert_type()` and `assert_never()` calls where the actual type
/// is not the same as the asserted type.
///
/// ## Why is this bad?

View File

@ -4022,6 +4022,20 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}
}
KnownFunction::AssertNever => {
if let [Some(actual_ty)] = overload.parameter_types() {
if !actual_ty.is_equivalent_to(self.db(), Type::Never) {
self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
call_expression,
format_args!(
"Expected type `Never`, got `{}` instead",
actual_ty.display(self.db()),
),
);
}
}
}
KnownFunction::StaticAssert => {
if let [Some(parameter_ty), message] = overload.parameter_types() {
let truthiness = match parameter_ty.try_bool(self.db()) {

View File

@ -652,7 +652,7 @@
},
"type-assertion-failure": {
"title": "detects failed type assertions",
"description": "## What it does\nChecks for `assert_type()` calls where the actual type\nis not the same as the asserted type.\n\n## Why is this bad?\n`assert_type()` allows confirming the inferred type of a certain value.\n\n## Example\n\n```python\ndef _(x: int):\n assert_type(x, int) # fine\n assert_type(x, str) # error: Actual type does not match asserted type\n```",
"description": "## What it does\nChecks for `assert_type()` and `assert_never()` calls where the actual type\nis not the same as the asserted type.\n\n## Why is this bad?\n`assert_type()` allows confirming the inferred type of a certain value.\n\n## Example\n\n```python\ndef _(x: int):\n assert_type(x, int) # fine\n assert_type(x, str) # error: Actual type does not match asserted type\n```",
"default": "error",
"oneOf": [
{