mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
[red-knot] Remove Type::Unbound
(#13980)
<!-- Thank you for contributing to Ruff! To help us out with reviewing, please consider the following: - Does this pull request include a summary of the change? (See below.) - Does this pull request include a descriptive title? - Does this pull request include references to any relevant issues? --> ## Summary - Remove `Type::Unbound` - Handle (potential) unboundness as a concept orthogonal to the type system (see new `Symbol` type) - Improve existing and add new diagnostics related to (potential) unboundness closes #13671 ## Test Plan - Update existing markdown-based tests - Add new tests for added/modified functionality
This commit is contained in:
parent
d1189c20df
commit
53fa32a389
24 changed files with 767 additions and 516 deletions
|
@ -33,6 +33,8 @@ b: tuple[int] = (42,)
|
|||
c: tuple[str, int] = ("42", 42)
|
||||
d: tuple[tuple[str, str], tuple[int, int]] = (("foo", "foo"), (42, 42))
|
||||
e: tuple[str, ...] = ()
|
||||
# TODO: we should not emit this error
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[tuple]` is possibly unbound"
|
||||
f: tuple[str, *tuple[int, ...], bytes] = ("42", b"42")
|
||||
g: tuple[str, Unpack[tuple[int, ...]], bytes] = ("42", b"42")
|
||||
h: tuple[list[int], list[int]] = ([], [])
|
||||
|
|
|
@ -80,9 +80,11 @@ class Foo:
|
|||
return 42
|
||||
|
||||
f = Foo()
|
||||
|
||||
# TODO: We should emit an `unsupported-operator` error here, possibly with the information
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
# TODO should emit a diagnostic warning that `Foo` might not have an `__iadd__` method
|
||||
reveal_type(f) # revealed: int
|
||||
```
|
||||
|
||||
|
|
|
@ -6,11 +6,20 @@
|
|||
x = foo # error: [unresolved-reference] "Name `foo` used when not defined"
|
||||
foo = 1
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
# No error `unresolved-reference` diagnostic is reported for `x`. This is
|
||||
# desirable because we would get a lot of cascading errors even though there
|
||||
# is only one root cause (the unbound variable `foo`).
|
||||
|
||||
# revealed: Unknown
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
Note: in this particular example, one could argue that the most likely error would
|
||||
be a wrong order of the `x`/`foo` definitions, and so it could be desirable to infer
|
||||
`Literal[1]` for the type of `x`. On the other hand, there might be a variable `fob`
|
||||
a little higher up in this file, and the actual error might have been just a typo.
|
||||
Inferring `Unknown` thus seems like the safest option.
|
||||
|
||||
## Unbound class variable
|
||||
|
||||
Name lookups within a class scope fall back to globals, but lookups of class attributes don't.
|
||||
|
@ -30,3 +39,22 @@ class C:
|
|||
reveal_type(C.x) # revealed: Literal[2]
|
||||
reveal_type(C.y) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Possibly unbound in class and global scope
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x = "abc"
|
||||
|
||||
class C:
|
||||
if bool_instance():
|
||||
x = 1
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
y = x
|
||||
|
||||
reveal_type(C.y) # revealed: Literal[1] | Literal["abc"]
|
||||
```
|
||||
|
|
|
@ -12,11 +12,11 @@ def bool_instance() -> bool:
|
|||
|
||||
if bool_instance() or (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if bool_instance() and (x := 1):
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## First expression is always evaluated
|
||||
|
@ -36,14 +36,14 @@ if (x := 1) and bool_instance():
|
|||
|
||||
```py
|
||||
if True or (x := 1):
|
||||
# TODO: infer that the second arm is never executed so type should be just "Unbound".
|
||||
# TODO: infer that the second arm is never executed, and raise `unresolved-reference`.
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if True and (x := 1):
|
||||
# TODO: infer that the second arm is always executed so type should be just "Literal[1]".
|
||||
# TODO: infer that the second arm is always executed, do not raise a diagnostic
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Later expressions can always use variables from earlier expressions
|
||||
|
@ -55,7 +55,7 @@ def bool_instance() -> bool:
|
|||
bool_instance() or (x := 1) or reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unbound
|
||||
bool_instance() or reveal_type(y) or (y := 1) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Nested expressions
|
||||
|
@ -65,14 +65,14 @@ def bool_instance() -> bool:
|
|||
return True
|
||||
|
||||
if bool_instance() or ((x := 1) and bool_instance()):
|
||||
# error: "Name `x` used when possibly not defined"
|
||||
reveal_type(x) # revealed: Unbound | Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
if ((y := 1) and bool_instance()) or bool_instance():
|
||||
reveal_type(y) # revealed: Literal[1]
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Unbound | Literal[1]
|
||||
if (bool_instance() and (z := 1)) or reveal_type(z): # revealed: Literal[1]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(z) # revealed: Unbound | Literal[1]
|
||||
reveal_type(z) # revealed: Literal[1]
|
||||
```
|
||||
|
|
|
@ -37,11 +37,11 @@ x = y
|
|||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
|
||||
# revealed: Unbound | Literal[2]
|
||||
# revealed: Literal[2]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(r)
|
||||
|
||||
# revealed: Unbound | Literal[5]
|
||||
# revealed: Literal[5]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(s)
|
||||
```
|
||||
|
|
|
@ -21,7 +21,7 @@ match 0:
|
|||
case 2:
|
||||
y = 3
|
||||
|
||||
# revealed: Unbound | Literal[2, 3]
|
||||
# revealed: Literal[2, 3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
|
|
@ -41,7 +41,12 @@ except EXCEPTIONS as f:
|
|||
## Dynamic exception types
|
||||
|
||||
```py
|
||||
def foo(x: type[AttributeError], y: tuple[type[OSError], type[RuntimeError]], z: tuple[type[BaseException], ...]):
|
||||
# TODO: we should not emit these `call-possibly-unbound-method` errors for `tuple.__class_getitem__`
|
||||
def foo(
|
||||
x: type[AttributeError],
|
||||
y: tuple[type[OSError], type[RuntimeError]], # error: [call-possibly-unbound-method]
|
||||
z: tuple[type[BaseException], ...], # error: [call-possibly-unbound-method]
|
||||
):
|
||||
try:
|
||||
help()
|
||||
except x as e:
|
||||
|
|
|
@ -12,11 +12,10 @@ if flag:
|
|||
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Literal[3]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# revealed: Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
@ -40,11 +39,10 @@ if flag:
|
|||
y: int = 3
|
||||
x = y # error: [possibly-unresolved-reference]
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Literal[3]
|
||||
reveal_type(x)
|
||||
|
||||
# revealed: Unbound | Literal[3]
|
||||
# revealed: Literal[3]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(y)
|
||||
```
|
||||
|
@ -58,6 +56,24 @@ reveal_type(x) # revealed: Literal[3]
|
|||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
## Maybe undeclared
|
||||
|
||||
Importing a possibly undeclared name still gives us its declared type:
|
||||
|
||||
```py path=maybe_undeclared.py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
if bool_instance():
|
||||
x: int
|
||||
```
|
||||
|
||||
```py
|
||||
from maybe_undeclared import x
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Reimport
|
||||
|
||||
```py path=c.py
|
||||
|
|
|
@ -17,8 +17,8 @@ async def foo():
|
|||
async for x in Iterator():
|
||||
pass
|
||||
|
||||
# TODO: should reveal `Unbound | Unknown` because `__aiter__` is not defined
|
||||
# revealed: Unbound | @Todo
|
||||
# TODO: should reveal `Unknown` because `__aiter__` is not defined
|
||||
# revealed: @Todo
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
@ -40,6 +40,6 @@ async def foo():
|
|||
pass
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: Unbound | @Todo
|
||||
# revealed: @Todo
|
||||
reveal_type(x)
|
||||
```
|
||||
|
|
|
@ -14,7 +14,7 @@ class IntIterable:
|
|||
for x in IntIterable():
|
||||
pass
|
||||
|
||||
# revealed: Unbound | int
|
||||
# revealed: int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
@ -87,7 +87,7 @@ class OldStyleIterable:
|
|||
for x in OldStyleIterable():
|
||||
pass
|
||||
|
||||
# revealed: Unbound | int
|
||||
# revealed: int
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
@ -98,7 +98,7 @@ reveal_type(x)
|
|||
for x in (1, "a", b"foo"):
|
||||
pass
|
||||
|
||||
# revealed: Unbound | Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# revealed: Literal[1] | Literal["a"] | Literal[b"foo"]
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
@ -120,7 +120,7 @@ class NotIterable:
|
|||
for x in NotIterable(): # error: "Object of type `NotIterable` is not iterable"
|
||||
pass
|
||||
|
||||
# revealed: Unbound | Unknown
|
||||
# revealed: Unknown
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
|
@ -240,7 +240,7 @@ def coinflip() -> bool:
|
|||
|
||||
# TODO: we should emit a diagnostic here (it might not be iterable)
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
## Union type as iterable where one union element has invalid `__iter__` method
|
||||
|
@ -261,9 +261,9 @@ class Test2:
|
|||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# TODO: we should emit a diagnostic here (it might not be iterable)
|
||||
# error: "Object of type `Test | Test2` is not iterable"
|
||||
for x in Test() if coinflip() else Test2():
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union type as iterator where one union element has no `__next__` method
|
||||
|
@ -277,7 +277,7 @@ class Test:
|
|||
def __iter__(self) -> TestIter | int:
|
||||
return TestIter()
|
||||
|
||||
# TODO: we should emit a diagnostic here (it might not be iterable)
|
||||
# error: [not-iterable] "Object of type `Test` is not iterable"
|
||||
for x in Test():
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
|
|
@ -19,7 +19,7 @@ reveal_type(__path__) # revealed: @Todo
|
|||
|
||||
# TODO: this should probably be added to typeshed; not sure why it isn't?
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
# revealed: Unknown
|
||||
reveal_type(__doc__)
|
||||
|
||||
class X:
|
||||
|
@ -34,15 +34,15 @@ module globals; these are excluded:
|
|||
|
||||
```py path=unbound_dunders.py
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
# revealed: Unknown
|
||||
reveal_type(__getattr__)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
# revealed: Unknown
|
||||
reveal_type(__dict__)
|
||||
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unbound
|
||||
# revealed: Unknown
|
||||
reveal_type(__init__)
|
||||
```
|
||||
|
||||
|
@ -61,10 +61,10 @@ reveal_type(typing.__init__) # revealed: Literal[__init__]
|
|||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
# TODO: we don't currently understand `types.ModuleType` as inheriting from `object`;
|
||||
# these should not reveal `Unbound`:
|
||||
reveal_type(typing.__eq__) # revealed: Unbound
|
||||
reveal_type(typing.__class__) # revealed: Unbound
|
||||
reveal_type(typing.__module__) # revealed: Unbound
|
||||
# these should not reveal `Unknown`:
|
||||
reveal_type(typing.__eq__) # revealed: Unknown
|
||||
reveal_type(typing.__class__) # revealed: Unknown
|
||||
reveal_type(typing.__module__) # revealed: Unknown
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
|
@ -78,7 +78,7 @@ where we know exactly which module we're dealing with:
|
|||
```py path=__getattr__.py
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__getattr__) # revealed: Unbound
|
||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## `types.ModuleType.__dict__` takes precedence over global variable `__dict__`
|
||||
|
@ -120,7 +120,7 @@ if returns_bool():
|
|||
__name__ = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: str | Literal[1]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute, with annotation
|
||||
|
@ -137,5 +137,5 @@ if returns_bool():
|
|||
__name__: int = 1
|
||||
|
||||
reveal_type(__file__) # revealed: Literal[42]
|
||||
reveal_type(__name__) # revealed: str | Literal[1]
|
||||
reveal_type(__name__) # revealed: Literal[1] | str
|
||||
```
|
||||
|
|
|
@ -68,8 +68,8 @@ if flag:
|
|||
else:
|
||||
class Spam: ...
|
||||
|
||||
# error: [call-non-callable] "Method `__class_getitem__` of type `Literal[__class_getitem__] | Unbound` is not callable on object of type `Literal[Spam, Spam]`"
|
||||
# revealed: str | Unknown
|
||||
# error: [call-possibly-unbound-method] "Method `__class_getitem__` of type `Literal[Spam, Spam]` is possibly unbound"
|
||||
# revealed: str
|
||||
reveal_type(Spam[42])
|
||||
```
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ with context_expr as f:
|
|||
```py
|
||||
class Manager: ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -57,7 +57,7 @@ with Manager():
|
|||
class Manager:
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because it doesn't implement `__enter__`"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__enter__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -68,7 +68,7 @@ with Manager():
|
|||
class Manager:
|
||||
def __enter__(self): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because it doesn't implement `__exit__`"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it doesn't implement `__exit__`"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -81,7 +81,7 @@ class Manager:
|
|||
|
||||
def __exit__(self, exc_tpe, exc_value, traceback): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because the method `__enter__` of type Literal[42] is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` of type `Literal[42]` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
@ -94,12 +94,12 @@ class Manager:
|
|||
|
||||
__exit__ = 32
|
||||
|
||||
# error: [invalid-context-manager] "Object of type Manager cannot be used with `with` because the method `__exit__` of type Literal[32] is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `Literal[32]` is not callable"
|
||||
with Manager():
|
||||
...
|
||||
```
|
||||
|
||||
## Context expression with non-callable union variants
|
||||
## Context expression with possibly-unbound union variants
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
|
@ -115,10 +115,10 @@ class NotAContextManager: ...
|
|||
|
||||
context_expr = Manager1() if coinflip() else NotAContextManager()
|
||||
|
||||
# error: [invalid-context-manager] "Object of type Manager1 | NotAContextManager cannot be used with `with` because the method `__enter__` of type Literal[__enter__] | Unbound is not callable"
|
||||
# error: [invalid-context-manager] "Object of type Manager1 | NotAContextManager cannot be used with `with` because the method `__exit__` of type Literal[__exit__] | Unbound is not callable"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
# error: [invalid-context-manager] "Object of type `Manager1 | NotAContextManager` cannot be used with `with` because the method `__exit__` is possibly unbound"
|
||||
with context_expr as f:
|
||||
reveal_type(f) # revealed: str | Unknown
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
||||
## Context expression with "sometimes" callable `__enter__` method
|
||||
|
@ -134,7 +134,7 @@ class Manager:
|
|||
|
||||
def __exit__(self, *args): ...
|
||||
|
||||
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__enter__` is possibly unbound"
|
||||
with Manager() as f:
|
||||
# TODO: This should emit an error that `__enter__` is possibly unbound.
|
||||
reveal_type(f) # revealed: str
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue