mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
handle NoReturn
This commit is contained in:
parent
316c1b21e2
commit
ca8f75b2f4
11 changed files with 216 additions and 65 deletions
|
@ -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]
|
||||
```
|
||||
|
||||
|
|
|
@ -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`
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>),
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue