mirror of https://github.com/astral-sh/ruff
[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:
parent
97dd6d120c
commit
b662c3ff7e
|
|
@ -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)
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue