handle NoReturn

This commit is contained in:
Abhijeet Prasad Bodas 2025-05-27 17:45:12 +05:30
parent 316c1b21e2
commit ca8f75b2f4
11 changed files with 216 additions and 65 deletions

View file

@ -2,27 +2,53 @@
## Basic functionality
<!-- snapshot-diagnostics -->
`assert_never` makes sure that the type of the argument is `Never`.
`assert_never` makes sure that the type of the argument is `Never`. If it is not, a
`type-assertion-failure` diagnostic is emitted.
### Correct usage
```py
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
def _(never: Never):
assert_never(never) # fine
```
### Diagnostics
<!-- snapshot-diagnostics -->
If it is not, a `type-assertion-failure` diagnostic is emitted.
```py
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _():
assert_never(0) # error: [type-assertion-failure]
def _():
assert_never("") # error: [type-assertion-failure]
def _():
assert_never(None) # error: [type-assertion-failure]
def _():
assert_never([]) # error: [type-assertion-failure]
def _():
assert_never({}) # error: [type-assertion-failure]
def _():
assert_never(()) # error: [type-assertion-failure]
def _(flag: bool, never: Never):
assert_never(1 if flag else never) # error: [type-assertion-failure]
def _(any_: Any):
assert_never(any_) # error: [type-assertion-failure]
def _(unknown: Unknown):
assert_never(unknown) # error: [type-assertion-failure]
```

View file

@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: assert_never.md - `assert_never` - Basic functionality
mdtest name: assert_never.md - `assert_never` - Basic functionality - Diagnostics
mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.md
---
@ -15,35 +15,47 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/directives/assert_never.
1 | from typing_extensions import assert_never, Never, Any
2 | from ty_extensions import Unknown
3 |
4 | def _(never: Never, any_: Any, unknown: Unknown, flag: bool):
5 | assert_never(never) # fine
4 | def _():
5 | assert_never(0) # error: [type-assertion-failure]
6 |
7 | assert_never(0) # error: [type-assertion-failure]
7 | def _():
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
16 | assert_never(unknown) # error: [type-assertion-failure]
9 |
10 | def _():
11 | assert_never(None) # error: [type-assertion-failure]
12 |
13 | def _():
14 | assert_never([]) # error: [type-assertion-failure]
15 |
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
18 |
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
21 |
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
24 |
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
27 |
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
```
# Diagnostics
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:7:5
--> src/mdtest_snippet.py:5:5
|
5 | assert_never(never) # fine
6 |
7 | assert_never(0) # error: [type-assertion-failure]
4 | def _():
5 | assert_never(0) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-^
| |
| Inferred type of argument is `Literal[0]`
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
6 |
7 | def _():
|
info: `Never` and `Literal[0]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -54,13 +66,13 @@ info: rule `type-assertion-failure` is enabled by default
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:8:5
|
7 | assert_never(0) # error: [type-assertion-failure]
7 | def _():
8 | assert_never("") # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `Literal[""]`
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
9 |
10 | def _():
|
info: `Never` and `Literal[""]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -69,16 +81,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:9:5
--> src/mdtest_snippet.py:11:5
|
7 | assert_never(0) # error: [type-assertion-failure]
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | def _():
11 | assert_never(None) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `None`
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 |
13 | def _():
|
info: `Never` and `None` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -87,16 +98,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:10:5
--> src/mdtest_snippet.py:14:5
|
8 | assert_never("") # error: [type-assertion-failure]
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
13 | def _():
14 | assert_never([]) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `list[Unknown]`
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
15 |
16 | def _():
|
info: `Never` and `list[Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -105,16 +115,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:11:5
--> src/mdtest_snippet.py:17:5
|
9 | assert_never(None) # error: [type-assertion-failure]
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
16 | def _():
17 | assert_never({}) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `dict[Unknown, Unknown]`
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
18 |
19 | def _():
|
info: `Never` and `dict[Unknown, Unknown]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -123,15 +132,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:12:5
--> src/mdtest_snippet.py:20:5
|
10 | assert_never([]) # error: [type-assertion-failure]
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
19 | def _():
20 | assert_never(()) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--^
| |
| Inferred type of argument is `tuple[()]`
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
21 |
22 | def _(flag: bool, never: Never):
|
info: `Never` and `tuple[()]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -140,16 +149,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:13:5
--> src/mdtest_snippet.py:23:5
|
11 | assert_never({}) # error: [type-assertion-failure]
12 | assert_never(()) # error: [type-assertion-failure]
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
22 | def _(flag: bool, never: Never):
23 | assert_never(1 if flag else never) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^--------------------^
| |
| Inferred type of argument is `Literal[1]`
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
24 |
25 | def _(any_: Any):
|
info: `Never` and `Literal[1]` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -158,15 +166,15 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:15:5
--> src/mdtest_snippet.py:26:5
|
13 | assert_never(1 if flag else never) # error: [type-assertion-failure]
14 |
15 | assert_never(any_) # error: [type-assertion-failure]
25 | def _(any_: Any):
26 | assert_never(any_) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^----^
| |
| Inferred type of argument is `Any`
16 | assert_never(unknown) # error: [type-assertion-failure]
27 |
28 | def _(unknown: Unknown):
|
info: `Never` and `Any` are not equivalent types
info: rule `type-assertion-failure` is enabled by default
@ -175,10 +183,10 @@ info: rule `type-assertion-failure` is enabled by default
```
error[type-assertion-failure]: Argument does not have asserted type `Never`
--> src/mdtest_snippet.py:16:5
--> src/mdtest_snippet.py:29:5
|
15 | assert_never(any_) # error: [type-assertion-failure]
16 | assert_never(unknown) # error: [type-assertion-failure]
28 | def _(unknown: Unknown):
29 | assert_never(unknown) # error: [type-assertion-failure]
| ^^^^^^^^^^^^^-------^
| |
| Inferred type of argument is `Unknown`

View file

@ -570,6 +570,65 @@ def f():
reveal_type(x) # revealed: Literal[1]
```
## Calls to functions returning `Never` / `NoReturn`
### No implicit return
If we see a call to a function returning `Never`, we should be able to understand that the function
cannot implicitly return `None`. In the below examples, verify that there are no errors emitted for
invalid return type.
```py
from typing import NoReturn
import sys
def f() -> NoReturn:
sys.exit(1)
```
Let's try cases where the function annotated with `NoReturn` is some sub-expression.
```py
from typing import NoReturn
import sys
def _() -> NoReturn:
3 + sys.exit(1)
def _() -> NoReturn:
3 if sys.exit(1) else 4
```
### Type narrowing
```py
from typing import NoReturn
import sys
def g(x: int | None):
if x is None:
sys.exit(1)
# TODO: should be just int, not int | None
reveal_type(x) # revealed: int | None
```
### Bindings after call
These should be understood to be unreachable.
```py
import sys
def _():
x = 3
sys.exit(1)
x = 4
reveal_type(x) # revealed: Never
```
## Nested functions
Free references inside of a function body refer to variables defined in the containing scope.

View file

@ -8,6 +8,7 @@ hydpy # too many iterations
ibis # too many iterations
jax # too many iterations
mypy # too many iterations (self-recursive type alias)
nox # too many iterations (because of packaging)
packaging # too many iterations
pandas # slow (9s)
pandera # too many iterations
@ -19,4 +20,6 @@ setuptools # vendors packaging, see above
spack # slow, success, but mypy-primer hangs processing the output
spark # too many iterations
steam.py # hangs (single threaded)
streamlit # too many iterations (because of packaging)
tornado # bad use-def map (https://github.com/astral-sh/ty/issues/365)
xarray # too many iterations

View file

@ -63,7 +63,6 @@ more-itertools
mypy-protobuf
mypy_primer
nionutils
nox
openlibrary
operator
optuna
@ -107,7 +106,6 @@ starlette
static-frame
stone
strawberry
streamlit
svcs
sympy
tornado

View file

@ -2225,6 +2225,18 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
}
walk_expr(self, expr);
}
ast::Expr::Call(ast::ExprCall { func, .. }) if !self.source_type.is_stub() => {
let expression = self.add_standalone_expression(func);
let predicate = Predicate {
node: PredicateNode::ReturnsNever(expression),
is_positive: false,
};
walk_expr(self, expr);
self.record_reachability_constraint(PredicateOrLiteral::Predicate(predicate));
}
_ => {
walk_expr(self, expr);
}

View file

@ -105,6 +105,7 @@ impl PredicateOrLiteral<'_> {
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub(crate) enum PredicateNode<'db> {
Expression(Expression<'db>),
ReturnsNever(Expression<'db>),
Pattern(PatternPredicate<'db>),
StarImportPlaceholder(StarImportPlaceholderPredicate<'db>),
}

View file

@ -684,6 +684,35 @@ impl ReachabilityConstraints {
let ty = infer_expression_type(db, test_expr);
ty.bool(db).negate_if(!predicate.is_positive)
}
PredicateNode::ReturnsNever(test_expr) => {
let ty = infer_expression_type(db, test_expr);
if let Type::FunctionLiteral(function_literal) = ty {
let returns_never =
if function_literal
.signature(db)
.overloads
.iter()
.all(|overload| {
// HACK: for now, require that *all* overloads are annotated with
// returning `Never`
// Ideally, if only some overloads return `Never`, we should consider
// the types of the arguments.
overload.return_ty.is_some_and(|return_type| {
return_type.is_equivalent_to(db, Type::Never)
})
})
{
Truthiness::AlwaysTrue
} else {
Truthiness::AlwaysFalse
};
returns_never.negate_if(!predicate.is_positive)
} else {
// Should I add a panic here?
// What about methods / other callables which are not functions?
Truthiness::AlwaysTrue
}
}
PredicateNode::Pattern(inner) => Self::analyze_single_pattern_predicate(db, inner),
PredicateNode::StarImportPlaceholder(star_import) => {
let place_table = place_table(db, star_import.scope(db));

View file

@ -192,6 +192,17 @@
//! for that place that we need for that use or definition. When we reach the end of the scope, it
//! records the state for each place as the public definitions of that place.
//!
//! ```python
//! x = 1
//! x = 2
//! y = x
//! if flag:
//! x = 3
//! else:
//! x = 4
//! z = x
//! ```
//!
//! Let's walk through the above example. Initially we do not have any record of `x`. When we add
//! the new place (before we process the first binding), we create a new undefined `PlaceState`
//! which has a single live binding (the "unbound" definition) and a single live declaration (the

View file

@ -5285,7 +5285,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// arguments after matching them to parameters, but before checking that the argument types
// are assignable to any parameter annotations.
let call_arguments = Self::parse_arguments(arguments);
let callable_type = self.infer_expression(func);
let callable_type = self.infer_maybe_standalone_expression(func);
if let Type::FunctionLiteral(function) = callable_type {
// Make sure that the `function.definition` is only called when the function is defined

View file

@ -60,6 +60,7 @@ pub(crate) fn infer_narrowing_constraint<'db>(
all_negative_narrowing_constraints_for_pattern(db, pattern)
}
}
PredicateNode::ReturnsNever(_) => return None,
PredicateNode::StarImportPlaceholder(_) => return None,
};
if let Some(constraints) = constraints {
@ -347,6 +348,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
PredicateNode::Pattern(pattern) => {
self.evaluate_pattern_predicate(pattern, self.is_positive)
}
PredicateNode::ReturnsNever(_) => return None,
PredicateNode::StarImportPlaceholder(_) => return None,
};
if let Some(mut constraints) = constraints {
@ -430,6 +432,7 @@ impl<'db, 'ast> NarrowingConstraintsBuilder<'db, 'ast> {
match self.predicate {
PredicateNode::Expression(expression) => expression.scope(self.db),
PredicateNode::Pattern(pattern) => pattern.scope(self.db),
PredicateNode::ReturnsNever(expression) => expression.scope(self.db),
PredicateNode::StarImportPlaceholder(definition) => definition.scope(self.db),
}
}