mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 13:24:57 +00:00
[ty] more precise lazy scope place lookup (#19932)
## Summary This is a follow-up to https://github.com/astral-sh/ruff/pull/19321. Now lazy snapshots are updated to take into account new bindings on every symbol reassignment. ```python def outer(x: A | None): if x is None: x = A() reveal_type(x) # revealed: A def inner() -> None: # lazy snapshot: {x: A} reveal_type(x) # revealed: A inner() def outer() -> None: x = None x = 1 def inner() -> None: # lazy snapshot: {x: Literal[1]} -> {x: Literal[1, 2]} reveal_type(x) # revealed: Literal[1, 2] inner() x = 2 ``` Closes astral-sh/ty#559. ## Test Plan Some TODOs in `public_types.md` now work properly. --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
aa5d665d52
commit
08a561fc05
7 changed files with 208 additions and 84 deletions
|
@ -248,6 +248,13 @@ def f(x: str | None):
|
|||
reveal_type(x) # revealed: str | None
|
||||
x = None
|
||||
|
||||
def f(x: str | None):
|
||||
def _(x: str | None):
|
||||
if x is not None:
|
||||
def closure():
|
||||
reveal_type(x) # revealed: str
|
||||
x = None
|
||||
|
||||
def f(x: str | None):
|
||||
class C:
|
||||
def _():
|
||||
|
@ -303,13 +310,17 @@ no longer valid in the inner lazy scope.
|
|||
def f(l: list[str | None]):
|
||||
if l[0] is not None:
|
||||
def _():
|
||||
reveal_type(l[0]) # revealed: str | None
|
||||
# TODO: should be `str | None`
|
||||
reveal_type(l[0]) # revealed: str | None | @Todo(list literal element type)
|
||||
# TODO: should be of type `list[None]`
|
||||
l = [None]
|
||||
|
||||
def f(l: list[str | None]):
|
||||
l[0] = "a"
|
||||
def _():
|
||||
reveal_type(l[0]) # revealed: str | None
|
||||
# TODO: should be `str | None`
|
||||
reveal_type(l[0]) # revealed: str | None | @Todo(list literal element type)
|
||||
# TODO: should be of type `list[None]`
|
||||
l = [None]
|
||||
|
||||
def f(l: list[str | None]):
|
||||
|
|
|
@ -83,6 +83,18 @@ def outer(flag: bool) -> None:
|
|||
|
||||
x = C()
|
||||
inner()
|
||||
|
||||
def outer(flag: bool) -> None:
|
||||
if flag:
|
||||
x = A()
|
||||
else:
|
||||
x = B() # this binding of `x` is invisible to `inner`
|
||||
return
|
||||
|
||||
def inner() -> None:
|
||||
reveal_type(x) # revealed: A | C
|
||||
x = C()
|
||||
inner()
|
||||
```
|
||||
|
||||
If a symbol is only conditionally bound, we do not raise any errors:
|
||||
|
@ -186,6 +198,59 @@ def outer(x: A | None):
|
|||
inner()
|
||||
```
|
||||
|
||||
"Reassignment" here refers to a thing that happens after the closure is defined that can actually
|
||||
change the inferred type of a captured symbol. Something done before the closure definition is more
|
||||
of a shadowing, and doesn't actually invalidate narrowing.
|
||||
|
||||
```py
|
||||
def outer() -> None:
|
||||
x = None
|
||||
|
||||
def inner() -> None:
|
||||
# In this scope, `x` may refer to `x = None` or `x = 1`.
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
inner()
|
||||
|
||||
x = 1
|
||||
|
||||
inner()
|
||||
|
||||
def inner2() -> None:
|
||||
# In this scope, `x = None` appears as being shadowed by `x = 1`.
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
inner2()
|
||||
|
||||
def outer() -> None:
|
||||
x = None
|
||||
|
||||
x = 1
|
||||
|
||||
def inner() -> None:
|
||||
reveal_type(x) # revealed: Literal[1, 2]
|
||||
inner()
|
||||
|
||||
x = 2
|
||||
|
||||
def outer(x: A | None):
|
||||
if x is None:
|
||||
x = A()
|
||||
|
||||
reveal_type(x) # revealed: A
|
||||
|
||||
def inner() -> None:
|
||||
reveal_type(x) # revealed: A
|
||||
inner()
|
||||
|
||||
def outer(x: A | None):
|
||||
x = x or A()
|
||||
|
||||
reveal_type(x) # revealed: A
|
||||
|
||||
def inner() -> None:
|
||||
reveal_type(x) # revealed: A
|
||||
inner()
|
||||
```
|
||||
|
||||
## At module level
|
||||
|
||||
The behavior is the same if the outer scope is the global scope of a module:
|
||||
|
@ -265,37 +330,17 @@ def outer() -> None:
|
|||
inner()
|
||||
```
|
||||
|
||||
This is currently even true if the `inner` function is only defined after the second assignment to
|
||||
`x`:
|
||||
And, in the current implementation, shadowing of module symbols (i.e., symbols exposed to other
|
||||
modules) cannot be recognized from lazy scopes.
|
||||
|
||||
```py
|
||||
def outer() -> None:
|
||||
x = None
|
||||
class A: ...
|
||||
class A: ...
|
||||
|
||||
# [additional code here]
|
||||
|
||||
x = 1
|
||||
|
||||
def inner() -> None:
|
||||
# TODO: this should be `Literal[1]`. Mypy and pyright support this.
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
inner()
|
||||
```
|
||||
|
||||
A similar case derived from an ecosystem example, involving declared types:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
def outer(x: C | None):
|
||||
x = x or C()
|
||||
|
||||
reveal_type(x) # revealed: C
|
||||
|
||||
def inner() -> None:
|
||||
# TODO: this should ideally be `C`
|
||||
reveal_type(x) # revealed: C | None
|
||||
inner()
|
||||
def f(x: A):
|
||||
# TODO: no error
|
||||
# error: [invalid-assignment] "Object of type `A | A` is not assignable to `A`"
|
||||
x = A()
|
||||
```
|
||||
|
||||
### Assignments to nonlocal variables
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue