[ty] Reachability analysis for isinstance(…) branches (#19503)
## Summary
Add more precise type inference for a limited set of `isinstance(…)`
calls, i.e. return `Literal[True]` if we can be sure that this is the
correct result. This improves exhaustiveness checking / reachability
analysis for if-elif-else chains with `isinstance` checks. For example:
```py
def is_number(x: int | str) -> bool: # no "can implicitly return `None` error here anymore
if isinstance(x, int):
return True
elif isinstance(x, str):
return False
# code here is now detected as being unreachable
```
This PR also adds a new test suite for exhaustiveness checking.
## Test Plan
New Markdown tests
### Ecosystem analysis
The removed diagnostics look good. There's [one
case](f52c4f1afd/torchvision/io/video_reader.py (L125-L143))
where a "true positive" is removed in unreachable code. `src` is
annotated as being of type `str`, but there is an `elif isinstance(src,
bytes)` branch, which we now detect as unreachable. And so the
diagnostic inside that branch is silenced. I don't think this is a
problem, especially once we have a "graying out" feature, or a lint that
warns about unreachable code.
This commit is contained in:
@@ -105,3 +105,59 @@ str("Müsli", "utf-8")
|
||||
# error: [invalid-argument-type] "Argument to class `str` is incorrect: Expected `str`, found `Literal[b"utf-8"]`"
|
||||
str(b"M\xc3\xbcsli", b"utf-8")
|
||||
```
|
||||
|
||||
## Calls to `isinstance`
|
||||
|
||||
We infer `Literal[True]` for a limited set of cases where we can be sure that the answer is correct,
|
||||
but fall back to `bool` otherwise.
|
||||
|
||||
```py
|
||||
from enum import Enum
|
||||
from types import FunctionType
|
||||
|
||||
class Answer(Enum):
|
||||
NO = 0
|
||||
YES = 1
|
||||
|
||||
reveal_type(isinstance(True, bool)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(True, int)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(True, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance("", str)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(1, int)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(b"", bytes)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(Answer.NO, Answer)) # revealed: Literal[True]
|
||||
|
||||
reveal_type(isinstance((1, 2), tuple)) # revealed: Literal[True]
|
||||
|
||||
def f(): ...
|
||||
|
||||
reveal_type(isinstance(f, FunctionType)) # revealed: Literal[True]
|
||||
|
||||
reveal_type(isinstance("", int)) # revealed: bool
|
||||
|
||||
class A: ...
|
||||
class SubclassOfA(A): ...
|
||||
class B: ...
|
||||
|
||||
reveal_type(isinstance(A, type)) # revealed: Literal[True]
|
||||
|
||||
a = A()
|
||||
|
||||
reveal_type(isinstance(a, A)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(a, object)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(a, SubclassOfA)) # revealed: bool
|
||||
reveal_type(isinstance(a, B)) # revealed: bool
|
||||
|
||||
s = SubclassOfA()
|
||||
reveal_type(isinstance(s, SubclassOfA)) # revealed: Literal[True]
|
||||
reveal_type(isinstance(s, A)) # revealed: Literal[True]
|
||||
|
||||
def _(x: A | B):
|
||||
reveal_type(isinstance(x, A)) # revealed: bool
|
||||
|
||||
if isinstance(x, A):
|
||||
pass
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
reveal_type(isinstance(x, B)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user