Skip to content

Commit 433494f

Browse files
committed
[red-knot] Add support for assert_never
1 parent 3657f79 commit 433494f

File tree

5 files changed

+144
-2
lines changed

5 files changed

+144
-2
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# `assert_never`
2+
3+
## Basic functionality
4+
5+
`assert_type` makes sure that the type of the argument is `Never`. If it is not, a
6+
`type-assertion-failure` diagnostic is emitted.
7+
8+
```py
9+
from typing_extensions import assert_never, Never, Any
10+
from knot_extensions import Unknown
11+
12+
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
13+
assert_never(never) # fine
14+
15+
assert_never(0) # error: [type-assertion-failure]
16+
assert_never("") # error: [type-assertion-failure]
17+
assert_never(None) # error: [type-assertion-failure]
18+
assert_never([]) # error: [type-assertion-failure]
19+
assert_never({}) # error: [type-assertion-failure]
20+
assert_never(()) # error: [type-assertion-failure]
21+
assert_never(1 if flag else never) # error: [type-assertion-failure]
22+
23+
assert_never(any_) # error: [type-assertion-failure]
24+
assert_never(unknown) # error: [type-assertion-failure]
25+
```
26+
27+
## Use case: Type narrowing and exhaustiveness checking
28+
29+
`assert_type` can be used in combination with type narrowing as a way to make sure that all cases
30+
are handled in a series of `isinstance` checks or other narrowing patterns that are supported.
31+
32+
```py
33+
from typing_extensions import assert_never, Literal
34+
35+
class A: ...
36+
class B: ...
37+
class C: ...
38+
39+
def if_else_isinstance_success(obj: A | B):
40+
if isinstance(obj, A):
41+
pass
42+
elif isinstance(obj, B):
43+
pass
44+
elif isinstance(obj, C):
45+
pass
46+
else:
47+
assert_never(obj)
48+
49+
def if_else_isinstance_error(obj: A | B):
50+
if isinstance(obj, A):
51+
pass
52+
# B is missing
53+
elif isinstance(obj, C):
54+
pass
55+
else:
56+
# error: [type-assertion-failure] "Expected type `Never`, got `B & ~A & ~C` instead"
57+
assert_never(obj)
58+
59+
def if_else_singletons_success(obj: Literal[1, "a"] | None):
60+
if obj == 1:
61+
pass
62+
elif obj == "a":
63+
pass
64+
elif obj is None:
65+
pass
66+
else:
67+
assert_never(obj)
68+
69+
def if_else_singletons_error(obj: Literal[1, "a"] | None):
70+
if obj == 1:
71+
pass
72+
elif obj is "A": # "A" instead of "a"
73+
pass
74+
elif obj is None:
75+
pass
76+
else:
77+
# error: [type-assertion-failure] "Expected type `Never`, got `Literal["a"]` instead"
78+
assert_never(obj)
79+
80+
def match_singletons_success(obj: Literal[1, "a"] | None):
81+
match obj:
82+
case 1:
83+
pass
84+
case "a":
85+
pass
86+
case None:
87+
pass
88+
case _ as obj:
89+
# TODO: Ideally, we would not emit an error here
90+
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo(`match` pattern definition types)` instead"
91+
assert_never(obj)
92+
93+
def match_singletons_error(obj: Literal[1, "a"] | None):
94+
match obj:
95+
case 1:
96+
pass
97+
case "A": # "A" instead of "a"
98+
pass
99+
case None:
100+
pass
101+
case _ as obj:
102+
# TODO: We should emit an error here, but the message should
103+
# show the type `Literal["a"]` instead of `@Todo(…)`.
104+
# error: [type-assertion-failure] "Expected type `Never`, got `@Todo(`match` pattern definition types)` instead"
105+
assert_never(obj)
106+
```

crates/red_knot_python_semantic/src/types.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3024,6 +3024,24 @@ impl<'db> Type<'db> {
30243024
Signatures::single(signature)
30253025
}
30263026

3027+
Some(KnownFunction::AssertNever) => {
3028+
let signature = CallableSignature::single(
3029+
self,
3030+
Signature::new(
3031+
Parameters::new([Parameter::positional_only(Some(Name::new_static(
3032+
"arg",
3033+
)))
3034+
// We need to set the type to `Any` here (instead of `Never`),
3035+
// in order for every `assert_never` call to pass the argument
3036+
// check. If we set it to `Never`, we'll get invalid-argument-type
3037+
// errors instead of `type-assertion-failure` errors.
3038+
.with_annotated_type(Type::any())]),
3039+
Some(Type::none(db)),
3040+
),
3041+
);
3042+
Signatures::single(signature)
3043+
}
3044+
30273045
Some(KnownFunction::Cast) => {
30283046
let signature = CallableSignature::single(
30293047
self,
@@ -4890,6 +4908,8 @@ pub enum KnownFunction {
48904908

48914909
/// `typing(_extensions).assert_type`
48924910
AssertType,
4911+
/// `typing(_extensions).assert_never`
4912+
AssertNever,
48934913
/// `typing(_extensions).cast`
48944914
Cast,
48954915
/// `typing(_extensions).overload`
@@ -4947,6 +4967,7 @@ impl KnownFunction {
49474967
match self {
49484968
Self::IsInstance | Self::IsSubclass | Self::Len | Self::Repr => module.is_builtins(),
49494969
Self::AssertType
4970+
| Self::AssertNever
49504971
| Self::Cast
49514972
| Self::Overload
49524973
| Self::RevealType
@@ -6407,6 +6428,7 @@ pub(crate) mod tests {
64076428
| KnownFunction::Overload
64086429
| KnownFunction::RevealType
64096430
| KnownFunction::AssertType
6431+
| KnownFunction::AssertNever
64106432
| KnownFunction::NoTypeCheck => KnownModule::TypingExtensions,
64116433

64126434
KnownFunction::IsSingleton

crates/red_knot_python_semantic/src/types/diagnostic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -682,7 +682,7 @@ declare_lint! {
682682

683683
declare_lint! {
684684
/// ## What it does
685-
/// Checks for `assert_type()` calls where the actual type
685+
/// Checks for `assert_type()` and `assert_never()` calls where the actual type
686686
/// is not the same as the asserted type.
687687
///
688688
/// ## Why is this bad?

crates/red_knot_python_semantic/src/types/infer.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4022,6 +4022,20 @@ impl<'db> TypeInferenceBuilder<'db> {
40224022
}
40234023
}
40244024
}
4025+
KnownFunction::AssertNever => {
4026+
if let [Some(actual_ty)] = overload.parameter_types() {
4027+
if !actual_ty.is_equivalent_to(self.db(), Type::Never) {
4028+
self.context.report_lint(
4029+
&TYPE_ASSERTION_FAILURE,
4030+
call_expression,
4031+
format_args!(
4032+
"Expected type `Never`, got `{}` instead",
4033+
actual_ty.display(self.db()),
4034+
),
4035+
);
4036+
}
4037+
}
4038+
}
40254039
KnownFunction::StaticAssert => {
40264040
if let [Some(parameter_ty), message] = overload.parameter_types() {
40274041
let truthiness = match parameter_ty.try_bool(self.db()) {

knot.schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@
652652
},
653653
"type-assertion-failure": {
654654
"title": "detects failed type assertions",
655-
"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```",
655+
"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```",
656656
"default": "error",
657657
"oneOf": [
658658
{

0 commit comments

Comments
 (0)