mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-23 16:51:58 +00:00

## Summary `ty` does not understand that calls to functions which have been annotated as having a return type of `Never` / `NoReturn` are terminal. This PR fixes that, by adding new reachability constraints when call expressions are seen. If the call expression evaluates to `Never`, the code following it will be considered to be unreachable. Note that, for adding these constraints, we only consider call expressions at the statement level, and that too only inside function scopes. This is because otherwise, the number of such constraints becomes too high, and evaluating them later on during type inference results in a major performance degradation. Fixes https://github.com/astral-sh/ty/issues/180 ## Test Plan New mdtests. ## Ecosystem changes This PR removes the following false-positives: - "Function can implicitly return `None`, which is not assignable to ...". - "Name `foo` used when possibly not defind" - because the branch in which it is not defined has a `NoReturn` call, or when `foo` was imported in a `try`, and the except had a `NoReturn` call. --------- Co-authored-by: David Peter <mail@david-peter.de>
3.3 KiB
3.3 KiB
assert_never
Basic functionality
assert_never
makes sure that the type of the argument is Never
.
Correct usage
from typing_extensions import assert_never, Never, Any
from ty_extensions import Unknown
def _(never: Never):
assert_never(never) # fine
Diagnostics
If it is not, a type-assertion-failure
diagnostic is emitted.
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]
Use case: Type narrowing and exhaustiveness checking
[environment]
python-version = "3.10"
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.
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] "Argument does not have asserted type `Never`"
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] "Argument does not have asserted type `Never`"
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] "Argument does not have asserted type `Never`"
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] "Argument does not have asserted type `Never`"
assert_never(obj)