mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:35:58 +00:00
[red-knot] Consider all definitions after terminal statements unreachable (#15676)
`FlowSnapshot` now tracks a `reachable` bool, which indicates whether we have encountered a terminal statement on that control flow path. When merging flow states together, we skip any that have been marked unreachable. This ensures that bindings that can only be reached through unreachable paths are not considered visible. ## Test Plan The new mdtests failed (with incorrect `reveal_type` results, and spurious `possibly-unresolved-reference` errors) before adding the new visibility constraints. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
e1c9d10863
commit
15d886a502
4 changed files with 686 additions and 22 deletions
|
@ -0,0 +1,640 @@
|
||||||
|
# Terminal statements
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Terminal statements complicate a naive control-flow analysis.
|
||||||
|
|
||||||
|
As a simple example:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f(cond: bool) -> str:
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
return x
|
||||||
|
|
||||||
|
def g(cond: bool):
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
else:
|
||||||
|
x = "terminal"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal"]
|
||||||
|
raise ValueError
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
```
|
||||||
|
|
||||||
|
In `f`, we should be able to determine that the `else` branch ends in a terminal statement, and that
|
||||||
|
the `return` statement can only be executed when the condition is true. We should therefore consider
|
||||||
|
the reference always bound, even though `x` is only bound in the true branch.
|
||||||
|
|
||||||
|
Similarly, in `g`, we should see that the assignment of the value `"terminal"` can never be seen by
|
||||||
|
the final `reveal_type`.
|
||||||
|
|
||||||
|
## `return`
|
||||||
|
|
||||||
|
A `return` statement is terminal; bindings that occur before it are not visible after it.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def resolved_reference(cond: bool) -> str:
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
else:
|
||||||
|
return "early"
|
||||||
|
return x # no possibly-unresolved-reference diagnostic!
|
||||||
|
|
||||||
|
def return_in_then_branch(cond: bool):
|
||||||
|
if cond:
|
||||||
|
x = "terminal"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal"]
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x = "test"
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
|
||||||
|
def return_in_else_branch(cond: bool):
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
else:
|
||||||
|
x = "terminal"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal"]
|
||||||
|
return
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
|
||||||
|
def return_in_both_branches(cond: bool):
|
||||||
|
if cond:
|
||||||
|
x = "terminal1"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal1"]
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x = "terminal2"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal2"]
|
||||||
|
return
|
||||||
|
|
||||||
|
def return_in_try(cond: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
# TODO: Literal["before"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "test"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["before"]
|
||||||
|
finally:
|
||||||
|
reveal_type(x) # revealed: Literal["before", "test"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "test"]
|
||||||
|
|
||||||
|
def return_in_nested_then_branch(cond1: bool, cond2: bool):
|
||||||
|
if cond1:
|
||||||
|
x = "test1"
|
||||||
|
reveal_type(x) # revealed: Literal["test1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "terminal"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal"]
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x = "test2"
|
||||||
|
reveal_type(x) # revealed: Literal["test2"]
|
||||||
|
reveal_type(x) # revealed: Literal["test2"]
|
||||||
|
reveal_type(x) # revealed: Literal["test1", "test2"]
|
||||||
|
|
||||||
|
def return_in_nested_else_branch(cond1: bool, cond2: bool):
|
||||||
|
if cond1:
|
||||||
|
x = "test1"
|
||||||
|
reveal_type(x) # revealed: Literal["test1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "test2"
|
||||||
|
reveal_type(x) # revealed: Literal["test2"]
|
||||||
|
else:
|
||||||
|
x = "terminal"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal"]
|
||||||
|
return
|
||||||
|
reveal_type(x) # revealed: Literal["test2"]
|
||||||
|
reveal_type(x) # revealed: Literal["test1", "test2"]
|
||||||
|
|
||||||
|
def return_in_both_nested_branches(cond1: bool, cond2: bool):
|
||||||
|
if cond1:
|
||||||
|
x = "test"
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
else:
|
||||||
|
x = "terminal0"
|
||||||
|
if cond2:
|
||||||
|
x = "terminal1"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal1"]
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x = "terminal2"
|
||||||
|
reveal_type(x) # revealed: Literal["terminal2"]
|
||||||
|
return
|
||||||
|
reveal_type(x) # revealed: Literal["test"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `continue`
|
||||||
|
|
||||||
|
A `continue` statement jumps back to the top of the innermost loop. This makes it terminal within
|
||||||
|
the loop body: definitions before it are not visible after it within the rest of the loop body. They
|
||||||
|
are likely visible after the loop body, since loops do not introduce new scopes. (Statically known
|
||||||
|
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
|
||||||
|
loop are not visible outside of it.)
|
||||||
|
|
||||||
|
TODO: We are not currently modeling the cyclic control flow for loops, pending fixpoint support in
|
||||||
|
Salsa. The false positives in this section are because of that, and not our terminal statement
|
||||||
|
support. See [ruff#14160](https://github.com/astral-sh/ruff/issues/14160) for more details.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def resolved_reference(cond: bool) -> str:
|
||||||
|
while True:
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
return x
|
||||||
|
|
||||||
|
def continue_in_then_branch(cond: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond:
|
||||||
|
x = "continue"
|
||||||
|
reveal_type(x) # revealed: Literal["continue"]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
x = "loop"
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
# TODO: Should be Literal["before", "loop", "continue"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||||
|
|
||||||
|
def continue_in_else_branch(cond: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond:
|
||||||
|
x = "loop"
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
else:
|
||||||
|
x = "continue"
|
||||||
|
reveal_type(x) # revealed: Literal["continue"]
|
||||||
|
continue
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
# TODO: Should be Literal["before", "loop", "continue"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||||
|
|
||||||
|
def continue_in_both_branches(cond: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond:
|
||||||
|
x = "continue1"
|
||||||
|
reveal_type(x) # revealed: Literal["continue1"]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
x = "continue2"
|
||||||
|
reveal_type(x) # revealed: Literal["continue2"]
|
||||||
|
continue
|
||||||
|
# TODO: Should be Literal["before", "continue1", "continue2"]
|
||||||
|
reveal_type(x) # revealed: Literal["before"]
|
||||||
|
|
||||||
|
def continue_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond1:
|
||||||
|
x = "loop1"
|
||||||
|
reveal_type(x) # revealed: Literal["loop1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "continue"
|
||||||
|
reveal_type(x) # revealed: Literal["continue"]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
x = "loop2"
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||||
|
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
|
||||||
|
|
||||||
|
def continue_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond1:
|
||||||
|
x = "loop1"
|
||||||
|
reveal_type(x) # revealed: Literal["loop1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "loop2"
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
else:
|
||||||
|
x = "continue"
|
||||||
|
reveal_type(x) # revealed: Literal["continue"]
|
||||||
|
continue
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||||
|
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
|
||||||
|
|
||||||
|
def continue_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond1:
|
||||||
|
x = "loop"
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "continue1"
|
||||||
|
reveal_type(x) # revealed: Literal["continue1"]
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
x = "continue2"
|
||||||
|
reveal_type(x) # revealed: Literal["continue2"]
|
||||||
|
continue
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
# TODO: Should be Literal["before", "loop", "continue1", "continue2"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `break`
|
||||||
|
|
||||||
|
A `break` statement jumps to the end of the innermost loop. This makes it terminal within the loop
|
||||||
|
body: definitions before it are not visible after it within the rest of the loop body. They are
|
||||||
|
likely visible after the loop body, since loops do not introduce new scopes. (Statically known
|
||||||
|
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
|
||||||
|
loop are not visible outside of it.)
|
||||||
|
|
||||||
|
```py
|
||||||
|
def resolved_reference(cond: bool) -> str:
|
||||||
|
while True:
|
||||||
|
if cond:
|
||||||
|
x = "test"
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return x
|
||||||
|
return x # error: [unresolved-reference]
|
||||||
|
|
||||||
|
def break_in_then_branch(cond: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond:
|
||||||
|
x = "break"
|
||||||
|
reveal_type(x) # revealed: Literal["break"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
x = "loop"
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "break", "loop"]
|
||||||
|
|
||||||
|
def break_in_else_branch(cond: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond:
|
||||||
|
x = "loop"
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
else:
|
||||||
|
x = "break"
|
||||||
|
reveal_type(x) # revealed: Literal["break"]
|
||||||
|
break
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop", "break"]
|
||||||
|
|
||||||
|
def break_in_both_branches(cond: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond:
|
||||||
|
x = "break1"
|
||||||
|
reveal_type(x) # revealed: Literal["break1"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
x = "break2"
|
||||||
|
reveal_type(x) # revealed: Literal["break2"]
|
||||||
|
break
|
||||||
|
reveal_type(x) # revealed: Literal["before", "break1", "break2"]
|
||||||
|
|
||||||
|
def break_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond1:
|
||||||
|
x = "loop1"
|
||||||
|
reveal_type(x) # revealed: Literal["loop1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "break"
|
||||||
|
reveal_type(x) # revealed: Literal["break"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
x = "loop2"
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop1", "break", "loop2"]
|
||||||
|
|
||||||
|
def break_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond1:
|
||||||
|
x = "loop1"
|
||||||
|
reveal_type(x) # revealed: Literal["loop1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "loop2"
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
else:
|
||||||
|
x = "break"
|
||||||
|
reveal_type(x) # revealed: Literal["break"]
|
||||||
|
break
|
||||||
|
reveal_type(x) # revealed: Literal["loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop1", "loop2", "break"]
|
||||||
|
|
||||||
|
def break_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
|
||||||
|
x = "before"
|
||||||
|
for _ in range(i):
|
||||||
|
if cond1:
|
||||||
|
x = "loop"
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "break1"
|
||||||
|
reveal_type(x) # revealed: Literal["break1"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
x = "break2"
|
||||||
|
reveal_type(x) # revealed: Literal["break2"]
|
||||||
|
break
|
||||||
|
reveal_type(x) # revealed: Literal["loop"]
|
||||||
|
reveal_type(x) # revealed: Literal["before", "loop", "break1", "break2"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## `raise`
|
||||||
|
|
||||||
|
A `raise` statement is terminal. If it occurs in a lexically containing `try` statement, it will
|
||||||
|
jump to one of the `except` clauses (if it matches the value being raised), or to the `else` clause
|
||||||
|
(if none match). Currently, we assume definitions from before the `raise` are visible in all
|
||||||
|
`except` and `else` clauses. (In the future, we might analyze the `except` clauses to see which ones
|
||||||
|
match the value being raised, and limit visibility to those clauses.) Definitions from before the
|
||||||
|
`raise` are not visible in any `else` clause, but are visible in `except` clauses or after the
|
||||||
|
containing `try` statement (since control flow may have passed through an `except`).
|
||||||
|
|
||||||
|
Currently we assume that an exception could be raised anywhere within a `try` block. We may want to
|
||||||
|
implement a more precise understanding of where exceptions (barring `KeyboardInterrupt` and
|
||||||
|
`MemoryError`) can and cannot actually be raised.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def raise_in_then_branch(cond: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond:
|
||||||
|
x = "raise"
|
||||||
|
reveal_type(x) # revealed: Literal["raise"]
|
||||||
|
raise ValueError
|
||||||
|
else:
|
||||||
|
x = "else"
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
except ValueError:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||||
|
except:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
finally:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||||
|
|
||||||
|
def raise_in_else_branch(cond: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond:
|
||||||
|
x = "else"
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
else:
|
||||||
|
x = "raise"
|
||||||
|
reveal_type(x) # revealed: Literal["raise"]
|
||||||
|
raise ValueError
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
except ValueError:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||||
|
except:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
finally:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||||
|
|
||||||
|
def raise_in_both_branches(cond: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond:
|
||||||
|
x = "raise1"
|
||||||
|
reveal_type(x) # revealed: Literal["raise1"]
|
||||||
|
raise ValueError
|
||||||
|
else:
|
||||||
|
x = "raise2"
|
||||||
|
reveal_type(x) # revealed: Literal["raise2"]
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||||
|
except:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||||
|
else:
|
||||||
|
x = "unreachable"
|
||||||
|
finally:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||||
|
|
||||||
|
def raise_in_nested_then_branch(cond1: bool, cond2: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond1:
|
||||||
|
x = "else1"
|
||||||
|
reveal_type(x) # revealed: Literal["else1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "raise"
|
||||||
|
reveal_type(x) # revealed: Literal["raise"]
|
||||||
|
raise ValueError
|
||||||
|
else:
|
||||||
|
x = "else2"
|
||||||
|
reveal_type(x) # revealed: Literal["else2"]
|
||||||
|
reveal_type(x) # revealed: Literal["else2"]
|
||||||
|
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||||
|
except ValueError:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||||
|
except:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||||
|
finally:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||||
|
|
||||||
|
def raise_in_nested_else_branch(cond1: bool, cond2: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond1:
|
||||||
|
x = "else1"
|
||||||
|
reveal_type(x) # revealed: Literal["else1"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "else2"
|
||||||
|
reveal_type(x) # revealed: Literal["else2"]
|
||||||
|
else:
|
||||||
|
x = "raise"
|
||||||
|
reveal_type(x) # revealed: Literal["raise"]
|
||||||
|
raise ValueError
|
||||||
|
reveal_type(x) # revealed: Literal["else2"]
|
||||||
|
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||||
|
except ValueError:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||||
|
except:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||||
|
finally:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||||
|
|
||||||
|
def raise_in_both_nested_branches(cond1: bool, cond2: bool):
|
||||||
|
x = "before"
|
||||||
|
try:
|
||||||
|
if cond1:
|
||||||
|
x = "else"
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
else:
|
||||||
|
if cond2:
|
||||||
|
x = "raise1"
|
||||||
|
reveal_type(x) # revealed: Literal["raise1"]
|
||||||
|
raise ValueError
|
||||||
|
else:
|
||||||
|
x = "raise2"
|
||||||
|
reveal_type(x) # revealed: Literal["raise2"]
|
||||||
|
raise ValueError
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
except ValueError:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||||
|
except:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||||
|
else:
|
||||||
|
reveal_type(x) # revealed: Literal["else"]
|
||||||
|
finally:
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||||
|
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||||
|
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Terminal in `try` with `finally` clause
|
||||||
|
|
||||||
|
TODO: we don't yet model that a `break` or `continue` in a `try` block will jump to a `finally`
|
||||||
|
clause before it jumps to end/start of the loop.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f():
|
||||||
|
x = 1
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
break
|
||||||
|
finally:
|
||||||
|
x = 2
|
||||||
|
# TODO: should be Literal[2]
|
||||||
|
reveal_type(x) # revealed: Literal[1]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nested functions
|
||||||
|
|
||||||
|
Free references inside of a function body refer to variables defined in the containing scope.
|
||||||
|
Function bodies are _lazy scopes_: at runtime, these references are not resolved immediately at the
|
||||||
|
point of the function definition. Instead, they are resolved _at the time of the call_, which means
|
||||||
|
that their values (and types) can be different for different invocations. For simplicity, we instead
|
||||||
|
resolve free references _at the end of the containing scope_. That means that in the examples below,
|
||||||
|
all of the `x` bindings should be visible to the `reveal_type`, regardless of where we place the
|
||||||
|
`return` statements.
|
||||||
|
|
||||||
|
TODO: These currently produce the wrong results, but not because of our terminal statement support.
|
||||||
|
See [ruff#15777](https://github.com/astral-sh/ruff/issues/15777) for more details.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def top_level_return(cond1: bool, cond2: bool):
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def g():
|
||||||
|
# TODO eliminate Unknown
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
|
||||||
|
if cond1:
|
||||||
|
if cond2:
|
||||||
|
x = 2
|
||||||
|
else:
|
||||||
|
x = 3
|
||||||
|
return
|
||||||
|
|
||||||
|
def return_from_if(cond1: bool, cond2: bool):
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def g():
|
||||||
|
# TODO: Literal[1, 2, 3]
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||||
|
if cond1:
|
||||||
|
if cond2:
|
||||||
|
x = 2
|
||||||
|
else:
|
||||||
|
x = 3
|
||||||
|
return
|
||||||
|
|
||||||
|
def return_from_nested_if(cond1: bool, cond2: bool):
|
||||||
|
x = 1
|
||||||
|
|
||||||
|
def g():
|
||||||
|
# TODO: Literal[1, 2, 3]
|
||||||
|
reveal_type(x) # revealed: Unknown | Literal[1, 3]
|
||||||
|
if cond1:
|
||||||
|
if cond2:
|
||||||
|
x = 2
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
x = 3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statically known terminal statements
|
||||||
|
|
||||||
|
Terminal statements do not yet interact correctly with statically known bounds. In this example, we
|
||||||
|
should see that the `return` statement is always executed, and therefore that the `"b"` assignment
|
||||||
|
is not visible to the `reveal_type`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(cond: bool):
|
||||||
|
x = "a"
|
||||||
|
if cond:
|
||||||
|
x = "b"
|
||||||
|
if True:
|
||||||
|
return
|
||||||
|
|
||||||
|
# TODO: Literal["a"]
|
||||||
|
reveal_type(x) # revealed: Literal["a", "b"]
|
||||||
|
```
|
|
@ -368,6 +368,12 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||||
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
|
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Records that all remaining statements in the current block are unreachable, and therefore
|
||||||
|
/// not visible.
|
||||||
|
fn mark_unreachable(&mut self) {
|
||||||
|
self.current_use_def_map_mut().mark_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
|
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
|
||||||
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
|
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
|
||||||
self.current_use_def_map_mut()
|
self.current_use_def_map_mut()
|
||||||
|
@ -1019,11 +1025,6 @@ where
|
||||||
}
|
}
|
||||||
self.visit_body(body);
|
self.visit_body(body);
|
||||||
}
|
}
|
||||||
ast::Stmt::Break(_) => {
|
|
||||||
if self.loop_state().is_inside() {
|
|
||||||
self.loop_break_states.push(self.flow_snapshot());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ast::Stmt::For(
|
ast::Stmt::For(
|
||||||
for_stmt @ ast::StmtFor {
|
for_stmt @ ast::StmtFor {
|
||||||
|
@ -1270,6 +1271,21 @@ where
|
||||||
// - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702
|
// - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702
|
||||||
self.visit_body(finalbody);
|
self.visit_body(finalbody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ast::Stmt::Raise(_) | ast::Stmt::Return(_) | ast::Stmt::Continue(_) => {
|
||||||
|
walk_stmt(self, stmt);
|
||||||
|
// Everything in the current block after a terminal statement is unreachable.
|
||||||
|
self.mark_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
|
ast::Stmt::Break(_) => {
|
||||||
|
if self.loop_state().is_inside() {
|
||||||
|
self.loop_break_states.push(self.flow_snapshot());
|
||||||
|
}
|
||||||
|
// Everything in the current block after a terminal statement is unreachable.
|
||||||
|
self.mark_unreachable();
|
||||||
|
}
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
walk_stmt(self, stmt);
|
walk_stmt(self, stmt);
|
||||||
}
|
}
|
||||||
|
|
|
@ -476,6 +476,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
|
||||||
pub(super) struct FlowSnapshot {
|
pub(super) struct FlowSnapshot {
|
||||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||||
scope_start_visibility: ScopedVisibilityConstraintId,
|
scope_start_visibility: ScopedVisibilityConstraintId,
|
||||||
|
reachable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -503,6 +504,8 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||||
|
|
||||||
/// Currently live bindings and declarations for each symbol.
|
/// Currently live bindings and declarations for each symbol.
|
||||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||||
|
|
||||||
|
reachable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UseDefMapBuilder<'_> {
|
impl Default for UseDefMapBuilder<'_> {
|
||||||
|
@ -515,11 +518,16 @@ impl Default for UseDefMapBuilder<'_> {
|
||||||
bindings_by_use: IndexVec::new(),
|
bindings_by_use: IndexVec::new(),
|
||||||
definitions_by_definition: FxHashMap::default(),
|
definitions_by_definition: FxHashMap::default(),
|
||||||
symbol_states: IndexVec::new(),
|
symbol_states: IndexVec::new(),
|
||||||
|
reachable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'db> UseDefMapBuilder<'db> {
|
impl<'db> UseDefMapBuilder<'db> {
|
||||||
|
pub(super) fn mark_unreachable(&mut self) {
|
||||||
|
self.reachable = false;
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||||
let new_symbol = self
|
let new_symbol = self
|
||||||
.symbol_states
|
.symbol_states
|
||||||
|
@ -656,6 +664,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||||
FlowSnapshot {
|
FlowSnapshot {
|
||||||
symbol_states: self.symbol_states.clone(),
|
symbol_states: self.symbol_states.clone(),
|
||||||
scope_start_visibility: self.scope_start_visibility,
|
scope_start_visibility: self.scope_start_visibility,
|
||||||
|
reachable: self.reachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -678,12 +687,25 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||||
num_symbols,
|
num_symbols,
|
||||||
SymbolState::undefined(self.scope_start_visibility),
|
SymbolState::undefined(self.scope_start_visibility),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.reachable = snapshot.reachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge the given snapshot into the current state, reflecting that we might have taken either
|
/// Merge the given snapshot into the current state, reflecting that we might have taken either
|
||||||
/// path to get here. The new state for each symbol should include definitions from both the
|
/// path to get here. The new state for each symbol should include definitions from both the
|
||||||
/// prior state and the snapshot.
|
/// prior state and the snapshot.
|
||||||
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
|
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
|
||||||
|
// Unreachable snapshots should not be merged: If the current snapshot is unreachable, it
|
||||||
|
// should be completely overwritten by the snapshot we're merging in. If the other snapshot
|
||||||
|
// is unreachable, we should return without merging.
|
||||||
|
if !snapshot.reachable {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if !self.reachable {
|
||||||
|
self.restore(snapshot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
|
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
|
||||||
// IDs must line up), so the current number of known symbols must always be equal to or
|
// IDs must line up), so the current number of known symbols must always be equal to or
|
||||||
// greater than the number of known symbols in a previously-taken snapshot.
|
// greater than the number of known symbols in a previously-taken snapshot.
|
||||||
|
@ -705,6 +727,9 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||||
self.scope_start_visibility = self
|
self.scope_start_visibility = self
|
||||||
.visibility_constraints
|
.visibility_constraints
|
||||||
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
|
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
|
||||||
|
|
||||||
|
// Both of the snapshots are reachable, so the merged result is too.
|
||||||
|
self.reachable = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
||||||
|
|
|
@ -26,25 +26,8 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
|
||||||
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||||
// We don't support `*` imports yet:
|
// We don't support `*` imports yet:
|
||||||
"error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
|
"error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
|
||||||
// We don't support terminal statements in control flow yet:
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
|
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
|
|
||||||
// We don't handle intersections in `is_assignable_to` yet
|
// We don't handle intersections in `is_assignable_to` yet
|
||||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
|
||||||
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
|
|
||||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
|
||||||
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
|
||||||
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive",
|
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue