mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 10:22: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
|
||||
```
|
||||
|
|
|
@ -20,6 +20,7 @@ pub mod semantic_index;
|
|||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub(crate) mod symbol;
|
||||
pub mod types;
|
||||
mod util;
|
||||
|
||||
|
|
|
@ -228,6 +228,7 @@ use self::symbol_state::{
|
|||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::symbol::Boundness;
|
||||
use ruff_index::IndexVec;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
|
@ -274,8 +275,12 @@ impl<'db> UseDefMap<'db> {
|
|||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||
}
|
||||
|
||||
pub(crate) fn use_may_be_unbound(&self, use_id: ScopedUseId) -> bool {
|
||||
self.bindings_by_use[use_id].may_be_unbound()
|
||||
pub(crate) fn use_boundness(&self, use_id: ScopedUseId) -> Boundness {
|
||||
if self.bindings_by_use[use_id].may_be_unbound() {
|
||||
Boundness::MayBeUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn public_bindings(
|
||||
|
@ -285,8 +290,12 @@ impl<'db> UseDefMap<'db> {
|
|||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||
}
|
||||
|
||||
pub(crate) fn public_may_be_unbound(&self, symbol: ScopedSymbolId) -> bool {
|
||||
self.public_symbols[symbol].may_be_unbound()
|
||||
pub(crate) fn public_boundness(&self, symbol: ScopedSymbolId) -> Boundness {
|
||||
if self.public_symbols[symbol].may_be_unbound() {
|
||||
Boundness::MayBeUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::module_name::ModuleName;
|
|||
use crate::module_resolver::{resolve_module, Module};
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::{binding_ty, global_symbol_ty, infer_scope_types, Type};
|
||||
use crate::types::{binding_ty, infer_scope_types, Type};
|
||||
use crate::Db;
|
||||
|
||||
pub struct SemanticModel<'db> {
|
||||
|
@ -38,10 +38,6 @@ impl<'db> SemanticModel<'db> {
|
|||
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module> {
|
||||
resolve_module(self.db, module_name)
|
||||
}
|
||||
|
||||
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
|
||||
global_symbol_ty(self.db, module.file(), symbol_name)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasTy {
|
||||
|
|
|
@ -2,7 +2,8 @@ use crate::module_name::ModuleName;
|
|||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{global_symbol_ty, Type};
|
||||
use crate::symbol::Symbol;
|
||||
use crate::types::global_symbol;
|
||||
use crate::Db;
|
||||
|
||||
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
|
||||
|
@ -33,62 +34,55 @@ impl CoreStdlibModule {
|
|||
|
||||
/// Lookup the type of `symbol` in a given core module
|
||||
///
|
||||
/// Returns `Unbound` if the given core module cannot be resolved for some reason
|
||||
fn core_module_symbol_ty<'db>(
|
||||
/// Returns `Symbol::Unbound` if the given core module cannot be resolved for some reason
|
||||
fn core_module_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
core_module: CoreStdlibModule,
|
||||
symbol: &str,
|
||||
) -> Type<'db> {
|
||||
) -> Symbol<'db> {
|
||||
resolve_module(db, &core_module.name())
|
||||
.map(|module| global_symbol_ty(db, module.file(), symbol))
|
||||
.map(|ty| {
|
||||
if ty.is_unbound() {
|
||||
ty
|
||||
} else {
|
||||
ty.replace_unbound_with(db, Type::Never)
|
||||
}
|
||||
})
|
||||
.unwrap_or(Type::Unbound)
|
||||
.map(|module| global_symbol(db, module.file(), symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `builtins` module isn't available for some reason.
|
||||
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn builtins_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Builtins, symbol)
|
||||
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::Builtins, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `types` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `types` module isn't available for some reason.
|
||||
/// Returns `Symbol::Unbound` if the `types` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn types_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Types, symbol)
|
||||
pub(crate) fn types_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::Types, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `typing` module isn't available for some reason.
|
||||
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.
|
||||
#[inline]
|
||||
#[allow(dead_code)] // currently only used in tests
|
||||
pub(crate) fn typing_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Typing, symbol)
|
||||
pub(crate) fn typing_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::Typing, symbol)
|
||||
}
|
||||
/// Lookup the type of `symbol` in the `_typeshed` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `_typeshed` module isn't available for some reason.
|
||||
/// Returns `Symbol::Unbound` if the `_typeshed` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
|
||||
pub(crate) fn typeshed_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::Typeshed, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
/// Returns `Symbol::Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::TypingExtensions, symbol)
|
||||
pub(crate) fn typing_extensions_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
|
||||
core_module_symbol(db, CoreStdlibModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
|
|
92
crates/red_knot_python_semantic/src/symbol.rs
Normal file
92
crates/red_knot_python_semantic/src/symbol.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
use crate::{
|
||||
types::{Type, UnionType},
|
||||
Db,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub(crate) enum Boundness {
|
||||
Bound,
|
||||
MayBeUnbound,
|
||||
}
|
||||
|
||||
/// The result of a symbol lookup, which can either be a (possibly unbound) type
|
||||
/// or a completely unbound symbol.
|
||||
///
|
||||
/// Consider this example:
|
||||
/// ```py
|
||||
/// bound = 1
|
||||
///
|
||||
/// if flag:
|
||||
/// maybe_unbound = 2
|
||||
/// ```
|
||||
///
|
||||
/// If we look up symbols in this scope, we would get the following results:
|
||||
/// ```rs
|
||||
/// bound: Symbol::Type(Type::IntLiteral(1), Boundness::Bound),
|
||||
/// maybe_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::MayBeUnbound),
|
||||
/// non_existent: Symbol::Unbound,
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Symbol<'db> {
|
||||
Type(Type<'db>, Boundness),
|
||||
Unbound,
|
||||
}
|
||||
|
||||
impl<'db> Symbol<'db> {
|
||||
pub(crate) fn is_unbound(&self) -> bool {
|
||||
matches!(self, Symbol::Unbound)
|
||||
}
|
||||
|
||||
pub(crate) fn may_be_unbound(&self) -> bool {
|
||||
match self {
|
||||
Symbol::Type(_, Boundness::MayBeUnbound) | Symbol::Unbound => true,
|
||||
Symbol::Type(_, Boundness::Bound) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or(&self, other: Type<'db>) -> Type<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, _) => *ty,
|
||||
Symbol::Unbound => other,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or_unknown(&self) -> Type<'db> {
|
||||
self.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
pub(crate) fn as_type(&self) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Symbol::Type(ty, _) => Some(*ty),
|
||||
Symbol::Unbound => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub(crate) fn expect_type(self) -> Type<'db> {
|
||||
self.as_type()
|
||||
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn replace_unbound_with(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
replacement: &Symbol<'db>,
|
||||
) -> Symbol<'db> {
|
||||
match replacement {
|
||||
Symbol::Type(replacement, _) => Symbol::Type(
|
||||
match self {
|
||||
Symbol::Type(ty, Boundness::Bound) => ty,
|
||||
Symbol::Type(ty, Boundness::MayBeUnbound) => {
|
||||
UnionType::from_elements(db, [*replacement, ty])
|
||||
}
|
||||
Symbol::Unbound => *replacement,
|
||||
},
|
||||
Boundness::Bound,
|
||||
),
|
||||
Symbol::Unbound => self,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,14 +4,13 @@ use ruff_python_ast as ast;
|
|||
use crate::module_resolver::file_to_module;
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, Symbol};
|
||||
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
|
||||
use crate::semantic_index::{
|
||||
global_scope, semantic_index, symbol_table, use_def_map, BindingWithConstraints,
|
||||
BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
use crate::stdlib::{
|
||||
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
|
||||
};
|
||||
use crate::stdlib::{builtins_symbol, types_symbol, typeshed_symbol, typing_extensions_symbol};
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::diagnostic::TypeCheckDiagnosticsBuilder;
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet, HasTy, Module, SemanticModel};
|
||||
|
@ -44,7 +43,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
|
|||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymbolId) -> Type<'db> {
|
||||
fn symbol_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymbolId) -> Symbol<'db> {
|
||||
let _span = tracing::trace_span!("symbol_ty_by_id", ?symbol).entered();
|
||||
|
||||
let use_def = use_def_map(db, scope);
|
||||
|
@ -55,37 +54,56 @@ fn symbol_ty_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymb
|
|||
let declarations = use_def.public_declarations(symbol);
|
||||
// If the symbol is undeclared in some paths, include the inferred type in the public type.
|
||||
let undeclared_ty = if declarations.may_be_undeclared() {
|
||||
Some(bindings_ty(
|
||||
db,
|
||||
use_def.public_bindings(symbol),
|
||||
use_def
|
||||
.public_may_be_unbound(symbol)
|
||||
.then_some(Type::Unbound),
|
||||
))
|
||||
Some(
|
||||
bindings_ty(db, use_def.public_bindings(symbol))
|
||||
.map(|bindings_ty| Symbol::Type(bindings_ty, use_def.public_boundness(symbol)))
|
||||
.unwrap_or(Symbol::Unbound),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Intentionally ignore conflicting declared types; that's not our problem, it's the
|
||||
// problem of the module we are importing from.
|
||||
declarations_ty(db, declarations, undeclared_ty).unwrap_or_else(|(ty, _)| ty)
|
||||
|
||||
// TODO: Our handling of boundness currently only depends on bindings, and ignores
|
||||
// declarations. This is inconsistent, since we only look at bindings if the symbol
|
||||
// may be undeclared. Consider the following example:
|
||||
// ```py
|
||||
// x: int
|
||||
//
|
||||
// if flag:
|
||||
// y: int
|
||||
// else
|
||||
// y = 3
|
||||
// ```
|
||||
// If we import from this module, we will currently report `x` as a definitely-bound
|
||||
// symbol (even though it has no bindings at all!) but report `y` as possibly-unbound
|
||||
// (even though every path has either a binding or a declaration for it.)
|
||||
|
||||
match undeclared_ty {
|
||||
Some(Symbol::Type(ty, boundness)) => Symbol::Type(
|
||||
declarations_ty(db, declarations, Some(ty)).unwrap_or_else(|(ty, _)| ty),
|
||||
boundness,
|
||||
),
|
||||
None | Some(Symbol::Unbound) => Symbol::Type(
|
||||
declarations_ty(db, declarations, None).unwrap_or_else(|(ty, _)| ty),
|
||||
Boundness::Bound,
|
||||
),
|
||||
}
|
||||
} else {
|
||||
bindings_ty(
|
||||
db,
|
||||
use_def.public_bindings(symbol),
|
||||
use_def
|
||||
.public_may_be_unbound(symbol)
|
||||
.then_some(Type::Unbound),
|
||||
)
|
||||
bindings_ty(db, use_def.public_bindings(symbol))
|
||||
.map(|bindings_ty| Symbol::Type(bindings_ty, use_def.public_boundness(symbol)))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty_by_id` that takes a symbol name instead of an ID.
|
||||
fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
|
||||
/// Shorthand for `symbol_by_id` that takes a symbol name instead of an ID.
|
||||
fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db> {
|
||||
let table = symbol_table(db, scope);
|
||||
table
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_ty_by_id(db, scope, symbol))
|
||||
.unwrap_or(Type::Unbound)
|
||||
.map(|symbol| symbol_by_id(db, scope, symbol))
|
||||
.unwrap_or(Symbol::Unbound)
|
||||
}
|
||||
|
||||
/// Return a list of the symbols that typeshed declares in the body scope of
|
||||
|
@ -117,17 +135,18 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N
|
|||
module_type_symbol_table
|
||||
.symbols()
|
||||
.filter(|symbol| symbol.is_declared())
|
||||
.map(Symbol::name)
|
||||
.map(symbol::Symbol::name)
|
||||
.filter(|symbol_name| !matches!(&***symbol_name, "__dict__" | "__getattr__" | "__init__"))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Looks up a module-global symbol by name in a file.
|
||||
pub(crate) fn global_symbol_ty<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
|
||||
let explicit_ty = symbol_ty(db, global_scope(db, file), name);
|
||||
if !explicit_ty.may_be_unbound(db) {
|
||||
return explicit_ty;
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
let explicit_symbol = symbol(db, global_scope(db, file), name);
|
||||
|
||||
if !explicit_symbol.may_be_unbound() {
|
||||
return explicit_symbol;
|
||||
}
|
||||
|
||||
// Not defined explicitly in the global scope?
|
||||
|
@ -140,12 +159,10 @@ pub(crate) fn global_symbol_ty<'db>(db: &'db dyn Db, file: File, name: &str) ->
|
|||
// TODO: this should use `.to_instance(db)`. but we don't understand attribute access
|
||||
// on instance types yet.
|
||||
let module_type_member = KnownClass::ModuleType.to_class(db).member(db, name);
|
||||
if !module_type_member.is_unbound() {
|
||||
return explicit_ty.replace_unbound_with(db, module_type_member);
|
||||
}
|
||||
return explicit_symbol.replace_unbound_with(db, &module_type_member);
|
||||
}
|
||||
|
||||
explicit_ty
|
||||
explicit_symbol
|
||||
}
|
||||
|
||||
/// Infer the type of a binding.
|
||||
|
@ -178,27 +195,14 @@ fn definition_expression_ty<'db>(
|
|||
}
|
||||
}
|
||||
|
||||
/// Infer the combined type of an iterator of bindings, plus one optional "unbound type".
|
||||
/// Infer the combined type of an iterator of bindings.
|
||||
///
|
||||
/// Will return a union if there is more than one binding, or at least one plus an unbound
|
||||
/// type.
|
||||
///
|
||||
/// The "unbound type" represents the type in case control flow may not have passed through any
|
||||
/// bindings in this scope. If this isn't possible, then it will be `None`. If it is possible, and
|
||||
/// the result in that case should be Unbound (e.g. an unbound function local), then it will be
|
||||
/// `Some(Type::Unbound)`. If it is possible and the result should be something else (e.g. an
|
||||
/// implicit global lookup), then `unbound_type` will be `Some(the_global_symbol_type)`.
|
||||
///
|
||||
/// # Panics
|
||||
/// Will panic if called with zero bindings and no `unbound_ty`. This is a logic error, as any
|
||||
/// symbol with zero visible bindings clearly may be unbound, and the caller should provide an
|
||||
/// `unbound_ty`.
|
||||
/// Will return a union if there is more than one binding.
|
||||
fn bindings_ty<'db>(
|
||||
db: &'db dyn Db,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
unbound_ty: Option<Type<'db>>,
|
||||
) -> Type<'db> {
|
||||
let def_types = bindings_with_constraints.map(
|
||||
) -> Option<Type<'db>> {
|
||||
let mut def_types = bindings_with_constraints.map(
|
||||
|BindingWithConstraints {
|
||||
binding,
|
||||
constraints,
|
||||
|
@ -220,16 +224,18 @@ fn bindings_ty<'db>(
|
|||
}
|
||||
},
|
||||
);
|
||||
let mut all_types = unbound_ty.into_iter().chain(def_types);
|
||||
|
||||
let first = all_types
|
||||
.next()
|
||||
.expect("bindings_ty should never be called with zero definitions and no unbound_ty");
|
||||
|
||||
if let Some(second) = all_types.next() {
|
||||
UnionType::from_elements(db, [first, second].into_iter().chain(all_types))
|
||||
if let Some(first) = def_types.next() {
|
||||
if let Some(second) = def_types.next() {
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[first, second].into_iter().chain(def_types),
|
||||
))
|
||||
} else {
|
||||
Some(first)
|
||||
}
|
||||
} else {
|
||||
first
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -301,9 +307,6 @@ pub enum Type<'db> {
|
|||
/// Unknown type (either no annotation, or some kind of type error).
|
||||
/// Equivalent to Any, or possibly to object in strict mode
|
||||
Unknown,
|
||||
/// Name does not exist or is not bound to any value (this represents an error, but with some
|
||||
/// leniency options it could be silently resolved to Unknown in some cases)
|
||||
Unbound,
|
||||
/// The None object -- TODO remove this in favor of Instance(types.NoneType)
|
||||
None,
|
||||
/// Temporary type for symbols that can't be inferred yet because of missing implementations.
|
||||
|
@ -347,10 +350,6 @@ pub enum Type<'db> {
|
|||
}
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
pub const fn is_unbound(&self) -> bool {
|
||||
matches!(self, Type::Unbound)
|
||||
}
|
||||
|
||||
pub const fn is_never(&self) -> bool {
|
||||
matches!(self, Type::Never)
|
||||
}
|
||||
|
@ -462,27 +461,6 @@ impl<'db> Type<'db> {
|
|||
matches!(self, Type::LiteralString)
|
||||
}
|
||||
|
||||
pub fn may_be_unbound(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Unbound => true,
|
||||
Type::Union(union) => union.elements(db).contains(&Type::Unbound),
|
||||
// Unbound can't appear in an intersection, because an intersection with Unbound
|
||||
// simplifies to just Unbound.
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> {
|
||||
match self {
|
||||
Type::Unbound => replacement,
|
||||
Type::Union(union) => {
|
||||
union.map(db, |element| element.replace_unbound_with(db, replacement))
|
||||
}
|
||||
_ => *self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
match self {
|
||||
Type::ClassLiteral(class) => class.is_stdlib_symbol(db, module_name, name),
|
||||
|
@ -601,7 +579,6 @@ impl<'db> Type<'db> {
|
|||
|
||||
(Type::Any, _) | (_, Type::Any) => false,
|
||||
(Type::Unknown, _) | (_, Type::Unknown) => false,
|
||||
(Type::Unbound, _) | (_, Type::Unbound) => false,
|
||||
(Type::Todo, _) | (_, Type::Todo) => false,
|
||||
|
||||
(Type::Union(union), other) | (other, Type::Union(union)) => union
|
||||
|
@ -754,7 +731,6 @@ impl<'db> Type<'db> {
|
|||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Todo
|
||||
| Type::Unbound
|
||||
| Type::Instance(..) // TODO some instance types can be singleton types (EllipsisType, NotImplementedType)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
|
@ -837,7 +813,6 @@ impl<'db> Type<'db> {
|
|||
Type::Any
|
||||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Unbound
|
||||
| Type::Todo
|
||||
| Type::Union(..)
|
||||
| Type::Intersection(..)
|
||||
|
@ -850,31 +825,22 @@ impl<'db> Type<'db> {
|
|||
/// For example, if `foo` is `Type::Instance(<Bar>)`,
|
||||
/// `foo.member(&db, "baz")` returns the type of `baz` attributes
|
||||
/// as accessed from instances of the `Bar` class.
|
||||
///
|
||||
/// TODO: use of this method currently requires manually checking
|
||||
/// whether the returned type is `Unknown`/`Unbound`
|
||||
/// (or a union with `Unknown`/`Unbound`) in many places.
|
||||
/// Ideally we'd use a more type-safe pattern, such as returning
|
||||
/// an `Option` or a `Result` from this method, which would force
|
||||
/// us to explicitly consider whether to handle an error or propagate
|
||||
/// it up the call stack.
|
||||
#[must_use]
|
||||
pub fn member(&self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
match self {
|
||||
Type::Any => Type::Any,
|
||||
Type::Any => Type::Any.into(),
|
||||
Type::Never => {
|
||||
// TODO: attribute lookup on Never type
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::Unknown => Type::Unknown,
|
||||
Type::Unbound => Type::Unbound,
|
||||
Type::Unknown => Type::Unknown.into(),
|
||||
Type::None => {
|
||||
// TODO: attribute lookup on None type
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::FunctionLiteral(_) => {
|
||||
// TODO: attribute lookup on function type
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::ModuleLiteral(file) => {
|
||||
// `__dict__` is a very special member that is never overridden by module globals;
|
||||
|
@ -886,7 +852,7 @@ impl<'db> Type<'db> {
|
|||
.member(db, "__dict__");
|
||||
}
|
||||
|
||||
let global_lookup = symbol_ty(db, global_scope(db, *file), name);
|
||||
let global_lookup = symbol(db, global_scope(db, *file), name);
|
||||
|
||||
// If it's unbound, check if it's present as an instance on `types.ModuleType`
|
||||
// or `builtins.object`.
|
||||
|
@ -903,15 +869,11 @@ impl<'db> Type<'db> {
|
|||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType`
|
||||
// to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types
|
||||
// where we know exactly which module we're dealing with.
|
||||
if name != "__getattr__" && global_lookup.may_be_unbound(db) {
|
||||
if name != "__getattr__" && global_lookup.may_be_unbound() {
|
||||
// TODO: this should use `.to_instance()`, but we don't understand instance attribute yet
|
||||
let module_type_instance_member =
|
||||
KnownClass::ModuleType.to_class(db).member(db, name);
|
||||
if module_type_instance_member.is_unbound() {
|
||||
global_lookup
|
||||
} else {
|
||||
global_lookup.replace_unbound_with(db, module_type_instance_member)
|
||||
}
|
||||
global_lookup.replace_unbound_with(db, &module_type_instance_member)
|
||||
} else {
|
||||
global_lookup
|
||||
}
|
||||
|
@ -919,42 +881,77 @@ impl<'db> Type<'db> {
|
|||
Type::ClassLiteral(class) => class.class_member(db, name),
|
||||
Type::Instance(_) => {
|
||||
// TODO MRO? get_own_instance_member, get_instance_member
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::Union(union) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
|
||||
let mut all_unbound = true;
|
||||
let mut may_be_unbound = false;
|
||||
for ty in union.elements(db) {
|
||||
let ty_member = ty.member(db, name);
|
||||
match ty_member {
|
||||
Symbol::Unbound => {
|
||||
may_be_unbound = true;
|
||||
}
|
||||
Symbol::Type(ty_member, member_boundness) => {
|
||||
// TODO: raise a diagnostic if member_boundness indicates potential unboundness
|
||||
if member_boundness == Boundness::MayBeUnbound {
|
||||
may_be_unbound = true;
|
||||
}
|
||||
|
||||
all_unbound = false;
|
||||
builder = builder.add(ty_member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if all_unbound {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
Symbol::Type(
|
||||
builder.build(),
|
||||
if may_be_unbound {
|
||||
Boundness::MayBeUnbound
|
||||
} else {
|
||||
Boundness::Bound
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Type::Union(union) => union.map(db, |element| element.member(db, name)),
|
||||
Type::Intersection(_) => {
|
||||
// TODO perform the get_member on each type in the intersection
|
||||
// TODO return the intersection of those results
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::IntLiteral(_) => {
|
||||
// TODO raise error
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::BooleanLiteral(_) => Type::Todo,
|
||||
Type::BooleanLiteral(_) => Type::Todo.into(),
|
||||
Type::StringLiteral(_) => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::LiteralString => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::BytesLiteral(_) => {
|
||||
// TODO defer to Type::Instance(<bytes from typeshed>).member
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::SliceLiteral(_) => {
|
||||
// TODO defer to `builtins.slice` methods
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::Tuple(_) => {
|
||||
// TODO: implement tuple methods
|
||||
Type::Todo
|
||||
Type::Todo.into()
|
||||
}
|
||||
Type::Todo => Type::Todo,
|
||||
Type::Todo => Type::Todo.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -964,9 +961,7 @@ impl<'db> Type<'db> {
|
|||
/// when `bool(x)` is called on an object `x`.
|
||||
fn bool(&self, db: &'db dyn Db) -> Truthiness {
|
||||
match self {
|
||||
Type::Any | Type::Todo | Type::Never | Type::Unknown | Type::Unbound => {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
Type::Any | Type::Todo | Type::Never | Type::Unknown => Truthiness::Ambiguous,
|
||||
Type::None => Truthiness::AlwaysFalse,
|
||||
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
|
||||
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
|
||||
|
@ -1042,14 +1037,14 @@ impl<'db> Type<'db> {
|
|||
Type::Instance(class) => {
|
||||
// Since `__call__` is a dunder, we need to access it as an attribute on the class
|
||||
// rather than the instance (matching runtime semantics).
|
||||
let dunder_call_method = class.class_member(db, "__call__");
|
||||
if dunder_call_method.is_unbound() {
|
||||
CallOutcome::not_callable(self)
|
||||
} else {
|
||||
let args = std::iter::once(self)
|
||||
.chain(arg_types.iter().copied())
|
||||
.collect::<Vec<_>>();
|
||||
dunder_call_method.call(db, &args)
|
||||
match class.class_member(db, "__call__") {
|
||||
Symbol::Type(dunder_call_method, Boundness::Bound) => {
|
||||
let args = std::iter::once(self)
|
||||
.chain(arg_types.iter().copied())
|
||||
.collect::<Vec<_>>();
|
||||
dunder_call_method.call(db, &args)
|
||||
}
|
||||
_ => CallOutcome::not_callable(self),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1101,21 +1096,29 @@ impl<'db> Type<'db> {
|
|||
let iterable_meta_type = self.to_meta_type(db);
|
||||
|
||||
let dunder_iter_method = iterable_meta_type.member(db, "__iter__");
|
||||
if !dunder_iter_method.is_unbound() {
|
||||
if let Symbol::Type(dunder_iter_method, _) = dunder_iter_method {
|
||||
let Some(iterator_ty) = dunder_iter_method.call(db, &[self]).return_ty(db) else {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
};
|
||||
};
|
||||
|
||||
let dunder_next_method = iterator_ty.to_meta_type(db).member(db, "__next__");
|
||||
return dunder_next_method
|
||||
.call(db, &[iterator_ty])
|
||||
.return_ty(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
});
|
||||
match iterator_ty.to_meta_type(db).member(db, "__next__") {
|
||||
Symbol::Type(dunder_next_method, Boundness::Bound) => {
|
||||
return dunder_next_method
|
||||
.call(db, &[iterator_ty])
|
||||
.return_ty(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Although it's not considered great practice,
|
||||
|
@ -1124,15 +1127,18 @@ impl<'db> Type<'db> {
|
|||
//
|
||||
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
|
||||
// accepting `int` or `SupportsIndex`
|
||||
let dunder_get_item_method = iterable_meta_type.member(db, "__getitem__");
|
||||
|
||||
dunder_get_item_method
|
||||
.call(db, &[self, KnownClass::Int.to_instance(db)])
|
||||
.return_ty(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
match iterable_meta_type.member(db, "__getitem__") {
|
||||
Symbol::Type(dunder_get_item_method, Boundness::Bound) => dunder_get_item_method
|
||||
.call(db, &[self, KnownClass::Int.to_instance(db)])
|
||||
.return_ty(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
}),
|
||||
_ => IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
|
@ -1141,7 +1147,6 @@ impl<'db> Type<'db> {
|
|||
Type::Any => Type::Any,
|
||||
Type::Todo => Type::Todo,
|
||||
Type::Unknown => Type::Unknown,
|
||||
Type::Unbound => Type::Unknown,
|
||||
Type::Never => Type::Never,
|
||||
Type::ClassLiteral(class) => Type::Instance(*class),
|
||||
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
|
||||
|
@ -1168,7 +1173,6 @@ impl<'db> Type<'db> {
|
|||
#[must_use]
|
||||
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Type::Unbound => Type::Unbound,
|
||||
Type::Never => Type::Never,
|
||||
Type::Instance(class) => Type::ClassLiteral(*class),
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
|
@ -1235,6 +1239,12 @@ impl<'db> From<&Type<'db>> for Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Type<'db>> for Symbol<'db> {
|
||||
fn from(value: Type<'db>) -> Self {
|
||||
Symbol::Type(value, Boundness::Bound)
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
|
||||
/// for easier syntax when interacting with very common classes.
|
||||
///
|
||||
|
@ -1306,12 +1316,12 @@ impl<'db> KnownClass {
|
|||
| Self::Tuple
|
||||
| Self::Set
|
||||
| Self::Dict
|
||||
| Self::Slice => builtins_symbol_ty(db, self.as_str()),
|
||||
| Self::Slice => builtins_symbol(db, self.as_str()).unwrap_or_unknown(),
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => {
|
||||
types_symbol_ty(db, self.as_str())
|
||||
types_symbol(db, self.as_str()).unwrap_or_unknown()
|
||||
}
|
||||
|
||||
Self::NoneType => typeshed_symbol_ty(db, self.as_str()),
|
||||
Self::NoneType => typeshed_symbol(db, self.as_str()).unwrap_or_unknown(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1834,23 +1844,22 @@ impl<'db> ClassType<'db> {
|
|||
/// Returns the class member of this class named `name`.
|
||||
///
|
||||
/// The member resolves to a member of the class itself or any of its bases.
|
||||
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub(crate) fn class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let member = self.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
// TODO diagnostic if maybe unbound?
|
||||
return member.replace_unbound_with(db, Type::Never);
|
||||
return member;
|
||||
}
|
||||
|
||||
self.inherited_class_member(db, name)
|
||||
}
|
||||
|
||||
/// Returns the inferred type of the class member named `name`.
|
||||
pub fn own_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub(crate) fn own_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
symbol_ty(db, scope, name)
|
||||
symbol(db, scope, name)
|
||||
}
|
||||
|
||||
pub fn inherited_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub(crate) fn inherited_class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
for base in self.bases(db) {
|
||||
let member = base.member(db, name);
|
||||
if !member.is_unbound() {
|
||||
|
@ -1858,7 +1867,7 @@ impl<'db> ClassType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
Type::Unbound
|
||||
Symbol::Unbound
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2024,7 +2033,7 @@ mod tests {
|
|||
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
|
||||
Ty::LiteralString => Type::LiteralString,
|
||||
Ty::BytesLiteral(s) => Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes())),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
|
||||
Ty::BuiltinInstance(s) => builtins_symbol(db, s).expect_type().to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
}
|
||||
|
@ -2141,8 +2150,8 @@ mod tests {
|
|||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let type_a = super::global_symbol_ty(&db, module, "A");
|
||||
let type_u = super::global_symbol_ty(&db, module, "U");
|
||||
let type_a = super::global_symbol(&db, module, "A").expect_type();
|
||||
let type_u = super::global_symbol(&db, module, "U").expect_type();
|
||||
|
||||
assert!(type_a.is_class_literal());
|
||||
assert!(type_a.is_subtype_of(&db, Ty::BuiltinInstance("type").into_type(&db)));
|
||||
|
@ -2241,8 +2250,8 @@ mod tests {
|
|||
.unwrap();
|
||||
let module = ruff_db::files::system_path_to_file(&db, "/src/module.py").unwrap();
|
||||
|
||||
let type_a = super::global_symbol_ty(&db, module, "A");
|
||||
let type_u = super::global_symbol_ty(&db, module, "U");
|
||||
let type_a = super::global_symbol(&db, module, "A").expect_type();
|
||||
let type_u = super::global_symbol(&db, module, "U").expect_type();
|
||||
|
||||
assert!(type_a.is_class_literal());
|
||||
assert!(type_u.is_union());
|
||||
|
|
|
@ -316,7 +316,6 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
self.add_positive(db, *neg);
|
||||
}
|
||||
}
|
||||
Type::Unbound => {}
|
||||
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
|
||||
// Adding any of these types to the negative side of an intersection
|
||||
// is equivalent to adding it to the positive side. We do this to
|
||||
|
@ -367,15 +366,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
fn simplify_unbound(&mut self) {
|
||||
if self.positive.contains(&Type::Unbound) {
|
||||
self.positive.retain(Type::is_unbound);
|
||||
self.negative.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.simplify_unbound();
|
||||
match (self.positive.len(), self.negative.len()) {
|
||||
(0, 0) => KnownClass::Object.to_instance(db),
|
||||
(1, 0) => self.positive[0],
|
||||
|
@ -394,7 +385,7 @@ mod tests {
|
|||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::stdlib::typing_symbol_ty;
|
||||
use crate::stdlib::typing_symbol;
|
||||
use crate::types::{KnownClass, StringLiteralType, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
@ -626,8 +617,10 @@ mod tests {
|
|||
#[test]
|
||||
fn intersection_negation_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let st = typing_symbol_ty(&db, "Sized").to_instance(&db);
|
||||
let ht = typing_symbol_ty(&db, "Hashable").to_instance(&db);
|
||||
let st = typing_symbol(&db, "Sized").expect_type().to_instance(&db);
|
||||
let ht = typing_symbol(&db, "Hashable")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
// sh_t: Sized & Hashable
|
||||
let sh_t = IntersectionBuilder::new(&db)
|
||||
.add_positive(st)
|
||||
|
@ -653,8 +646,10 @@ mod tests {
|
|||
fn mixed_intersection_negation_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let it = KnownClass::Int.to_instance(&db);
|
||||
let st = typing_symbol_ty(&db, "Sized").to_instance(&db);
|
||||
let ht = typing_symbol_ty(&db, "Hashable").to_instance(&db);
|
||||
let st = typing_symbol(&db, "Sized").expect_type().to_instance(&db);
|
||||
let ht = typing_symbol(&db, "Hashable")
|
||||
.expect_type()
|
||||
.to_instance(&db);
|
||||
// s_not_h_t: Sized & ~Hashable
|
||||
let s_not_h_t = IntersectionBuilder::new(&db)
|
||||
.add_positive(st)
|
||||
|
@ -709,28 +704,6 @@ mod tests {
|
|||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_unbound() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unbound)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Unbound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_unbound() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Unbound)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_none() {
|
||||
let db = setup_db();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use ruff_db::files::File;
|
||||
use ruff_python_ast::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
|
@ -233,6 +233,26 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_possibly_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add(
|
||||
expr_name_node.into(),
|
||||
"possibly-unresolved-reference",
|
||||
format_args!("Name `{id}` used when possibly not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn add_unresolved_reference(&mut self, expr_name_node: &ast::ExprName) {
|
||||
let ast::ExprName { id, .. } = expr_name_node;
|
||||
|
||||
self.add(
|
||||
expr_name_node.into(),
|
||||
"unresolved-reference",
|
||||
format_args!("Name `{id}` used when not defined"),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new diagnostic.
|
||||
///
|
||||
/// The diagnostic does not get added if the rule isn't enabled for this file.
|
||||
|
|
|
@ -64,7 +64,6 @@ impl Display for DisplayRepresentation<'_> {
|
|||
Type::Any => f.write_str("Any"),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Unbound => f.write_str("Unbound"),
|
||||
Type::None => f.write_str("None"),
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
|
@ -324,7 +323,7 @@ mod tests {
|
|||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::types::{
|
||||
global_symbol_ty, BytesLiteralType, SliceLiteralType, StringLiteralType, Type, UnionType,
|
||||
global_symbol, BytesLiteralType, SliceLiteralType, StringLiteralType, Type, UnionType,
|
||||
};
|
||||
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
|
||||
|
@ -370,16 +369,16 @@ mod tests {
|
|||
let union_elements = &[
|
||||
Type::Unknown,
|
||||
Type::IntLiteral(-1),
|
||||
global_symbol_ty(&db, mod_file, "A"),
|
||||
global_symbol(&db, mod_file, "A").expect_type(),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, "A")),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, [0u8].as_slice())),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, [7u8].as_slice())),
|
||||
Type::IntLiteral(0),
|
||||
Type::IntLiteral(1),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, "B")),
|
||||
global_symbol_ty(&db, mod_file, "foo"),
|
||||
global_symbol_ty(&db, mod_file, "bar"),
|
||||
global_symbol_ty(&db, mod_file, "B"),
|
||||
global_symbol(&db, mod_file, "foo").expect_type(),
|
||||
global_symbol(&db, mod_file, "bar").expect_type(),
|
||||
global_symbol(&db, mod_file, "B").expect_type(),
|
||||
Type::BooleanLiteral(true),
|
||||
Type::None,
|
||||
];
|
||||
|
|
|
@ -53,9 +53,9 @@ use crate::types::diagnostic::{
|
|||
TypeCheckDiagnostic, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder,
|
||||
};
|
||||
use crate::types::{
|
||||
bindings_ty, builtins_symbol_ty, declarations_ty, global_symbol_ty, symbol_ty,
|
||||
typing_extensions_symbol_ty, BytesLiteralType, ClassType, FunctionType, IterationOutcome,
|
||||
KnownClass, KnownFunction, SliceLiteralType, StringLiteralType, Truthiness, TupleType, Type,
|
||||
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol,
|
||||
Boundness, BytesLiteralType, ClassType, FunctionType, IterationOutcome, KnownClass,
|
||||
KnownFunction, SliceLiteralType, StringLiteralType, Symbol, Truthiness, TupleType, Type,
|
||||
TypeArrayDisplay, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
|
@ -586,7 +586,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let use_def = self.index.use_def_map(declaration.file_scope(self.db));
|
||||
let prior_bindings = use_def.bindings_at_declaration(declaration);
|
||||
// unbound_ty is Never because for this check we don't care about unbound
|
||||
let inferred_ty = bindings_ty(self.db, prior_bindings, Some(Type::Never));
|
||||
let inferred_ty = bindings_ty(self.db, prior_bindings).unwrap_or(Type::Never);
|
||||
let ty = if inferred_ty.is_assignable_to(self.db, ty) {
|
||||
ty
|
||||
} else {
|
||||
|
@ -1042,78 +1042,113 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
let context_manager_ty = context_expression_ty.to_meta_type(self.db);
|
||||
|
||||
let enter_ty = context_manager_ty.member(self.db, "__enter__");
|
||||
let exit_ty = context_manager_ty.member(self.db, "__exit__");
|
||||
let enter = context_manager_ty.member(self.db, "__enter__");
|
||||
let exit = context_manager_ty.member(self.db, "__exit__");
|
||||
|
||||
// TODO: Make use of Protocols when we support it (the manager be assignable to `contextlib.AbstractContextManager`).
|
||||
if enter_ty.is_unbound() && exit_ty.is_unbound() {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type {} cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
|
||||
context_expression_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
Type::Unknown
|
||||
} else if enter_ty.is_unbound() {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type {} cannot be used with `with` because it doesn't implement `__enter__`",
|
||||
context_expression_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
Type::Unknown
|
||||
} else {
|
||||
let target_ty = enter_ty
|
||||
.call(self.db, &[context_expression_ty])
|
||||
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!("
|
||||
Object of type {context_expression} cannot be used with `with` because the method `__enter__` of type {enter_ty} is not callable",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
enter_ty = enter_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
});
|
||||
|
||||
if exit_ty.is_unbound() {
|
||||
match (enter, exit) {
|
||||
(Symbol::Unbound, Symbol::Unbound) => {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type {} cannot be used with `with` because it doesn't implement `__exit__`",
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__` and `__exit__`",
|
||||
context_expression_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
|
||||
else if exit_ty
|
||||
.call(
|
||||
self.db,
|
||||
&[context_manager_ty, Type::None, Type::None, Type::None],
|
||||
)
|
||||
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
|
||||
.is_err()
|
||||
{
|
||||
(Symbol::Unbound, _) => {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type {context_expression} cannot be used with `with` because the method `__exit__` of type {exit_ty} is not callable",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
exit_ty = exit_ty.display(self.db),
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__enter__`",
|
||||
context_expression_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
(Symbol::Type(enter_ty, enter_boundness), exit) => {
|
||||
if enter_boundness == Boundness::MayBeUnbound {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` is possibly unbound",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
target_ty
|
||||
let target_ty = enter_ty
|
||||
.call(self.db, &[context_expression_ty])
|
||||
.return_ty_result(self.db, context_expression.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!("
|
||||
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
enter_ty = enter_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
});
|
||||
|
||||
match exit {
|
||||
Symbol::Unbound => {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{}` cannot be used with `with` because it doesn't implement `__exit__`",
|
||||
context_expression_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
Symbol::Type(exit_ty, exit_boundness) => {
|
||||
// TODO: Use the `exit_ty` to determine if any raised exception is suppressed.
|
||||
|
||||
if exit_boundness == Boundness::MayBeUnbound {
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` is possibly unbound",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if exit_ty
|
||||
.call(
|
||||
self.db,
|
||||
&[context_manager_ty, Type::None, Type::None, Type::None],
|
||||
)
|
||||
.return_ty_result(
|
||||
self.db,
|
||||
context_expression.into(),
|
||||
&mut self.diagnostics,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
self.diagnostics.add(
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!(
|
||||
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
exit_ty = exit_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target_ty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1134,7 +1169,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
//
|
||||
// TODO should infer `ExceptionGroup` if all caught exceptions
|
||||
// are subclasses of `Exception` --Alex
|
||||
builtins_symbol_ty(self.db, "BaseExceptionGroup").to_instance(self.db)
|
||||
builtins_symbol(self.db, "BaseExceptionGroup")
|
||||
.unwrap_or_unknown()
|
||||
.to_instance(self.db)
|
||||
} else {
|
||||
// TODO: anything that's a consistent subtype of
|
||||
// `type[BaseException] | tuple[type[BaseException], ...]` should be valid;
|
||||
|
@ -1513,8 +1550,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
// If the target defines, e.g., `__iadd__`, infer the augmented assignment as a call to that
|
||||
// dunder.
|
||||
if let Type::Instance(class) = target_type {
|
||||
let class_member = class.class_member(self.db, op.in_place_dunder());
|
||||
if !class_member.is_unbound() {
|
||||
if let Some(class_member) = class.class_member(self.db, op.in_place_dunder()).as_type()
|
||||
{
|
||||
// TODO: Handle the case where boundness is `MayBeUnbound`: fall back
|
||||
// to the binary-op behavior below and union the result with calling
|
||||
// the possibly-unbound in-place dunder.
|
||||
|
||||
let call = class_member.call(self.db, &[target_type, value_type]);
|
||||
return match call.return_ty_result(
|
||||
self.db,
|
||||
|
@ -1776,21 +1817,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
asname: _,
|
||||
} = alias;
|
||||
|
||||
let member_ty = module_ty.member(self.db, &ast::name::Name::new(&name.id));
|
||||
// For possibly-unbound names, just eliminate Unbound from the type; we
|
||||
// must be in a bound path. TODO diagnostic for maybe-unbound import?
|
||||
module_ty
|
||||
.member(self.db, &ast::name::Name::new(&name.id))
|
||||
.as_type()
|
||||
.unwrap_or_else(|| {
|
||||
self.diagnostics.add(
|
||||
AnyNodeRef::Alias(alias),
|
||||
"unresolved-import",
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
|
||||
if member_ty.is_unbound() {
|
||||
self.diagnostics.add(
|
||||
AnyNodeRef::Alias(alias),
|
||||
"unresolved-import",
|
||||
format_args!("Module `{module_name}` has no member `{name}`",),
|
||||
);
|
||||
|
||||
Type::Unknown
|
||||
} else {
|
||||
// For possibly-unbound names, just eliminate Unbound from the type; we
|
||||
// must be in a bound path. TODO diagnostic for maybe-unbound import?
|
||||
member_ty.replace_unbound_with(self.db, Type::Never)
|
||||
}
|
||||
Type::Unknown
|
||||
})
|
||||
} else {
|
||||
self.diagnostics
|
||||
.add_unresolved_module(import_from, *level, module);
|
||||
|
@ -1940,9 +1980,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.map(Type::IntLiteral)
|
||||
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db)),
|
||||
ast::Number::Float(_) => KnownClass::Float.to_instance(self.db),
|
||||
ast::Number::Complex { .. } => {
|
||||
builtins_symbol_ty(self.db, "complex").to_instance(self.db)
|
||||
}
|
||||
ast::Number::Complex { .. } => builtins_symbol(self.db, "complex")
|
||||
.unwrap_or_unknown()
|
||||
.to_instance(self.db),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2022,7 +2062,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
&mut self,
|
||||
_literal: &ast::ExprEllipsisLiteral,
|
||||
) -> Type<'db> {
|
||||
builtins_symbol_ty(self.db, "Ellipsis")
|
||||
builtins_symbol(self.db, "Ellipsis").unwrap_or_unknown()
|
||||
}
|
||||
|
||||
fn infer_tuple_expression(&mut self, tuple: &ast::ExprTuple) -> Type<'db> {
|
||||
|
@ -2398,7 +2438,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
}
|
||||
|
||||
/// Look up a name reference that isn't bound in the local scope.
|
||||
fn lookup_name(&mut self, name_node: &ast::ExprName) -> Type<'db> {
|
||||
fn lookup_name(&mut self, name_node: &ast::ExprName) -> Symbol<'db> {
|
||||
let ast::ExprName { id: name, .. } = name_node;
|
||||
let file_scope_id = self.scope().file_scope_id(self.db);
|
||||
let is_bound = self
|
||||
|
@ -2432,36 +2472,39 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
// runtime, it is the scope that creates the cell for our closure.) If the name
|
||||
// isn't bound in that scope, we should get an unbound name, not continue
|
||||
// falling back to other scopes / globals / builtins.
|
||||
return symbol_ty(self.db, enclosing_scope_id, name);
|
||||
return symbol(self.db, enclosing_scope_id, name);
|
||||
}
|
||||
}
|
||||
|
||||
// No nonlocal binding, check module globals. Avoid infinite recursion if `self.scope`
|
||||
// already is module globals.
|
||||
let ty = if file_scope_id.is_global() {
|
||||
Type::Unbound
|
||||
let global_symbol = if file_scope_id.is_global() {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
global_symbol_ty(self.db, self.file, name)
|
||||
global_symbol(self.db, self.file, name)
|
||||
};
|
||||
|
||||
// Fallback to builtins (without infinite recursion if we're already in builtins.)
|
||||
if ty.may_be_unbound(self.db) && Some(self.scope()) != builtins_module_scope(self.db) {
|
||||
let mut builtin_ty = builtins_symbol_ty(self.db, name);
|
||||
if builtin_ty.is_unbound() && name == "reveal_type" {
|
||||
if global_symbol.may_be_unbound()
|
||||
&& Some(self.scope()) != builtins_module_scope(self.db)
|
||||
{
|
||||
let mut symbol = builtins_symbol(self.db, name);
|
||||
if symbol.is_unbound() && name == "reveal_type" {
|
||||
self.diagnostics.add(
|
||||
name_node.into(),
|
||||
"undefined-reveal",
|
||||
format_args!(
|
||||
"`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"),
|
||||
);
|
||||
builtin_ty = typing_extensions_symbol_ty(self.db, name);
|
||||
symbol = typing_extensions_symbol(self.db, name);
|
||||
}
|
||||
ty.replace_unbound_with(self.db, builtin_ty)
|
||||
|
||||
global_symbol.replace_unbound_with(self.db, &symbol)
|
||||
} else {
|
||||
ty
|
||||
global_symbol
|
||||
}
|
||||
} else {
|
||||
Type::Unbound
|
||||
Symbol::Unbound
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2481,41 +2524,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.symbol_id_by_name(id)
|
||||
.expect("Expected the symbol table to create a symbol for every Name node");
|
||||
// if we're inferring types of deferred expressions, always treat them as public symbols
|
||||
let (definitions, may_be_unbound) = if self.is_deferred() {
|
||||
let (definitions, boundness) = if self.is_deferred() {
|
||||
(
|
||||
use_def.public_bindings(symbol),
|
||||
use_def.public_may_be_unbound(symbol),
|
||||
use_def.public_boundness(symbol),
|
||||
)
|
||||
} else {
|
||||
let use_id = name.scoped_use_id(self.db, self.scope());
|
||||
(
|
||||
use_def.bindings_at_use(use_id),
|
||||
use_def.use_may_be_unbound(use_id),
|
||||
use_def.use_boundness(use_id),
|
||||
)
|
||||
};
|
||||
|
||||
let unbound_ty = if may_be_unbound {
|
||||
Some(self.lookup_name(name))
|
||||
let bindings_ty = bindings_ty(self.db, definitions);
|
||||
|
||||
if boundness == Boundness::MayBeUnbound {
|
||||
match self.lookup_name(name) {
|
||||
Symbol::Type(looked_up_ty, looked_up_boundness) => {
|
||||
if looked_up_boundness == Boundness::MayBeUnbound {
|
||||
self.diagnostics.add_possibly_unresolved_reference(name);
|
||||
}
|
||||
|
||||
bindings_ty
|
||||
.map(|ty| UnionType::from_elements(self.db, [ty, looked_up_ty]))
|
||||
.unwrap_or(looked_up_ty)
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
if bindings_ty.is_some() {
|
||||
self.diagnostics.add_possibly_unresolved_reference(name);
|
||||
} else {
|
||||
self.diagnostics.add_unresolved_reference(name);
|
||||
}
|
||||
bindings_ty.unwrap_or(Type::Unknown)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ty = bindings_ty(self.db, definitions, unbound_ty);
|
||||
if ty.is_unbound() {
|
||||
self.diagnostics.add(
|
||||
name.into(),
|
||||
"unresolved-reference",
|
||||
format_args!("Name `{id}` used when not defined"),
|
||||
);
|
||||
} else if ty.may_be_unbound(self.db) {
|
||||
self.diagnostics.add(
|
||||
name.into(),
|
||||
"possibly-unresolved-reference",
|
||||
format_args!("Name `{id}` used when possibly not defined"),
|
||||
);
|
||||
bindings_ty.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
ty
|
||||
}
|
||||
|
||||
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
|
||||
|
@ -2536,7 +2582,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
} = attribute;
|
||||
|
||||
let value_ty = self.infer_expression(value);
|
||||
value_ty.member(self.db, &Name::new(&attr.id))
|
||||
value_ty
|
||||
.member(self.db, &Name::new(&attr.id))
|
||||
.unwrap_or_unknown()
|
||||
}
|
||||
|
||||
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
|
||||
|
@ -2590,26 +2638,41 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
unreachable!("Not operator is handled in its own case");
|
||||
}
|
||||
};
|
||||
let class_member = class.class_member(self.db, unary_dunder_method);
|
||||
let call = class_member.call(self.db, &[operand_type]);
|
||||
|
||||
match call.return_ty_result(
|
||||
self.db,
|
||||
AnyNodeRef::ExprUnaryOp(unary),
|
||||
&mut self.diagnostics,
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.diagnostics.add(
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
),
|
||||
);
|
||||
e.return_ty()
|
||||
if let Symbol::Type(class_member, _) =
|
||||
class.class_member(self.db, unary_dunder_method)
|
||||
{
|
||||
let call = class_member.call(self.db, &[operand_type]);
|
||||
|
||||
match call.return_ty_result(
|
||||
self.db,
|
||||
AnyNodeRef::ExprUnaryOp(unary),
|
||||
&mut self.diagnostics,
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
self.diagnostics.add(
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
),
|
||||
);
|
||||
e.return_ty()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.diagnostics.add(
|
||||
unary.into(),
|
||||
"unsupported-operator",
|
||||
format_args!(
|
||||
"Unary operator `{op}` is unsupported for type `{}`",
|
||||
operand_type.display(self.db),
|
||||
),
|
||||
);
|
||||
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
_ => Type::Todo, // TODO other unary op types
|
||||
|
@ -2820,30 +2883,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
&& rhs_reflected != left_class.class_member(self.db, reflected_dunder)
|
||||
{
|
||||
return rhs_reflected
|
||||
.unwrap_or(Type::Never)
|
||||
.call(self.db, &[right_ty, left_ty])
|
||||
.return_ty(self.db)
|
||||
.or_else(|| {
|
||||
left_class
|
||||
.class_member(self.db, op.dunder())
|
||||
.unwrap_or(Type::Never)
|
||||
.call(self.db, &[left_ty, right_ty])
|
||||
.return_ty(self.db)
|
||||
});
|
||||
}
|
||||
}
|
||||
left_class
|
||||
.class_member(self.db, op.dunder())
|
||||
.call(self.db, &[left_ty, right_ty])
|
||||
.return_ty(self.db)
|
||||
.or_else(|| {
|
||||
if left_class == right_class {
|
||||
None
|
||||
} else {
|
||||
right_class
|
||||
.class_member(self.db, op.reflected_dunder())
|
||||
|
||||
let call_on_left_instance = if let Symbol::Type(class_member, _) =
|
||||
left_class.class_member(self.db, op.dunder())
|
||||
{
|
||||
class_member
|
||||
.call(self.db, &[left_ty, right_ty])
|
||||
.return_ty(self.db)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
call_on_left_instance.or_else(|| {
|
||||
if left_class == right_class {
|
||||
None
|
||||
} else {
|
||||
if let Symbol::Type(class_member, _) =
|
||||
right_class.class_member(self.db, op.reflected_dunder())
|
||||
{
|
||||
class_member
|
||||
.call(self.db, &[right_ty, left_ty])
|
||||
.return_ty(self.db)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
(
|
||||
|
@ -3428,9 +3505,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
// If the class defines `__getitem__`, return its return type.
|
||||
//
|
||||
// See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem
|
||||
let dunder_getitem_method = value_meta_ty.member(self.db, "__getitem__");
|
||||
if !dunder_getitem_method.is_unbound() {
|
||||
return dunder_getitem_method
|
||||
match value_meta_ty.member(self.db, "__getitem__") {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(dunder_getitem_method, boundness) => {
|
||||
if boundness == Boundness::MayBeUnbound {
|
||||
self.diagnostics.add(
|
||||
value_node.into(),
|
||||
"call-possibly-unbound-method",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return dunder_getitem_method
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
|
@ -3445,6 +3534,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
);
|
||||
err.return_ty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, if the value is itself a class and defines `__class_getitem__`,
|
||||
|
@ -3458,22 +3548,37 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
// method in these `sys.version_info` branches.
|
||||
if value_ty.is_subtype_of(self.db, KnownClass::Type.to_instance(self.db)) {
|
||||
let dunder_class_getitem_method = value_ty.member(self.db, "__class_getitem__");
|
||||
if !dunder_class_getitem_method.is_unbound() {
|
||||
return dunder_class_getitem_method
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
|
||||
match dunder_class_getitem_method {
|
||||
Symbol::Unbound => {}
|
||||
Symbol::Type(ty, boundness) => {
|
||||
if boundness == Boundness::MayBeUnbound {
|
||||
self.diagnostics.add(
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
"call-possibly-unbound-method",
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
"Method `__class_getitem__` of type `{}` is possibly unbound",
|
||||
value_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
});
|
||||
}
|
||||
|
||||
return ty
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
value_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(value_ty, Type::ClassLiteral(class) if class.is_known(self.db, KnownClass::Type))
|
||||
|
@ -3996,13 +4101,15 @@ fn perform_rich_comparison<'db>(
|
|||
|
||||
let call_dunder =
|
||||
|op: RichCompareOperator, left_class: ClassType<'db>, right_class: ClassType<'db>| {
|
||||
left_class
|
||||
.class_member(db, op.dunder())
|
||||
.call(
|
||||
db,
|
||||
&[Type::Instance(left_class), Type::Instance(right_class)],
|
||||
)
|
||||
.return_ty(db)
|
||||
match left_class.class_member(db, op.dunder()) {
|
||||
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
|
||||
.call(
|
||||
db,
|
||||
&[Type::Instance(left_class), Type::Instance(right_class)],
|
||||
)
|
||||
.return_ty(db),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
|
||||
|
@ -4043,18 +4150,20 @@ fn perform_membership_test_comparison<'db>(
|
|||
let (left_instance, right_instance) = (Type::Instance(left_class), Type::Instance(right_class));
|
||||
|
||||
let contains_dunder = right_class.class_member(db, "__contains__");
|
||||
|
||||
let compare_result_opt = if contains_dunder.is_unbound() {
|
||||
// iteration-based membership test
|
||||
match right_instance.iterate(db) {
|
||||
IterationOutcome::Iterable { .. } => Some(KnownClass::Bool.to_instance(db)),
|
||||
IterationOutcome::NotIterable { .. } => None,
|
||||
let compare_result_opt = match contains_dunder {
|
||||
Symbol::Type(contains_dunder, Boundness::Bound) => {
|
||||
// If `__contains__` is available, it is used directly for the membership test.
|
||||
contains_dunder
|
||||
.call(db, &[right_instance, left_instance])
|
||||
.return_ty(db)
|
||||
}
|
||||
_ => {
|
||||
// iteration-based membership test
|
||||
match right_instance.iterate(db) {
|
||||
IterationOutcome::Iterable { .. } => Some(KnownClass::Bool.to_instance(db)),
|
||||
IterationOutcome::NotIterable { .. } => None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If `__contains__` is available, it is used directly for the membership test.
|
||||
contains_dunder
|
||||
.call(db, &[right_instance, left_instance])
|
||||
.return_ty(db)
|
||||
};
|
||||
|
||||
compare_result_opt
|
||||
|
@ -4087,7 +4196,7 @@ mod tests {
|
|||
use crate::semantic_index::symbol::FileScopeId;
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
use crate::types::{
|
||||
check_types, global_symbol_ty, infer_definition_types, symbol_ty, TypeCheckDiagnostics,
|
||||
check_types, global_symbol, infer_definition_types, symbol, TypeCheckDiagnostics,
|
||||
};
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
|
@ -4147,7 +4256,7 @@ mod tests {
|
|||
fn assert_public_ty(db: &TestDb, file_name: &str, symbol_name: &str, expected: &str) {
|
||||
let file = system_path_to_file(db, file_name).expect("file to exist");
|
||||
|
||||
let ty = global_symbol_ty(db, file, symbol_name);
|
||||
let ty = global_symbol(db, file, symbol_name).expect_type();
|
||||
assert_eq!(
|
||||
ty.display(db).to_string(),
|
||||
expected,
|
||||
|
@ -4177,7 +4286,7 @@ mod tests {
|
|||
assert_eq!(scope.name(db), *expected_scope_name);
|
||||
}
|
||||
|
||||
let ty = symbol_ty(db, scope, symbol_name);
|
||||
let ty = symbol(db, scope, symbol_name).unwrap_or_unknown();
|
||||
assert_eq!(ty.display(db).to_string(), expected);
|
||||
}
|
||||
|
||||
|
@ -4224,7 +4333,7 @@ mod tests {
|
|||
)?;
|
||||
|
||||
let mod_file = system_path_to_file(&db, "src/mod.py").expect("file to exist");
|
||||
let ty = global_symbol_ty(&db, mod_file, "Sub");
|
||||
let ty = global_symbol(&db, mod_file, "Sub").expect_type();
|
||||
|
||||
let class = ty.expect_class_literal();
|
||||
|
||||
|
@ -4251,9 +4360,11 @@ mod tests {
|
|||
)?;
|
||||
|
||||
let mod_file = system_path_to_file(&db, "src/mod.py").unwrap();
|
||||
let ty = global_symbol_ty(&db, mod_file, "C");
|
||||
let ty = global_symbol(&db, mod_file, "C").expect_type();
|
||||
let class_id = ty.expect_class_literal();
|
||||
let member_ty = class_id.class_member(&db, &Name::new_static("f"));
|
||||
let member_ty = class_id
|
||||
.class_member(&db, &Name::new_static("f"))
|
||||
.expect_type();
|
||||
let func = member_ty.expect_function_literal();
|
||||
|
||||
assert_eq!(func.name(&db), "f");
|
||||
|
@ -4432,7 +4543,9 @@ mod tests {
|
|||
db.write_file("src/a.py", "def example() -> int: return 42")?;
|
||||
|
||||
let mod_file = system_path_to_file(&db, "src/a.py").unwrap();
|
||||
let function = global_symbol_ty(&db, mod_file, "example").expect_function_literal();
|
||||
let function = global_symbol(&db, mod_file, "example")
|
||||
.expect_type()
|
||||
.expect_function_literal();
|
||||
let returns = function.return_type(&db);
|
||||
assert_eq!(returns.display(&db).to_string(), "int");
|
||||
|
||||
|
@ -4460,7 +4573,7 @@ mod tests {
|
|||
)?;
|
||||
|
||||
let a = system_path_to_file(&db, "src/a.py").expect("file to exist");
|
||||
let c_ty = global_symbol_ty(&db, a, "C");
|
||||
let c_ty = global_symbol(&db, a, "C").expect_type();
|
||||
let c_class = c_ty.expect_class_literal();
|
||||
let mut c_bases = c_class.bases(&db);
|
||||
let b_ty = c_bases.next().unwrap();
|
||||
|
@ -4497,10 +4610,10 @@ mod tests {
|
|||
.unwrap()
|
||||
.0
|
||||
.to_scope_id(&db, file);
|
||||
let y_ty = symbol_ty(&db, function_scope, "y");
|
||||
let x_ty = symbol_ty(&db, function_scope, "x");
|
||||
let y_ty = symbol(&db, function_scope, "y").expect_type();
|
||||
let x_ty = symbol(&db, function_scope, "x").expect_type();
|
||||
|
||||
assert_eq!(y_ty.display(&db).to_string(), "Unbound");
|
||||
assert_eq!(y_ty.display(&db).to_string(), "Unknown");
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[2]");
|
||||
|
||||
Ok(())
|
||||
|
@ -4528,10 +4641,11 @@ mod tests {
|
|||
.unwrap()
|
||||
.0
|
||||
.to_scope_id(&db, file);
|
||||
let y_ty = symbol_ty(&db, function_scope, "y");
|
||||
let x_ty = symbol_ty(&db, function_scope, "x");
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Unbound");
|
||||
let x_ty = symbol(&db, function_scope, "x");
|
||||
assert!(x_ty.is_unbound());
|
||||
|
||||
let y_ty = symbol(&db, function_scope, "y").expect_type();
|
||||
assert_eq!(y_ty.display(&db).to_string(), "Literal[1]");
|
||||
|
||||
Ok(())
|
||||
|
@ -4597,7 +4711,7 @@ mod tests {
|
|||
],
|
||||
)?;
|
||||
|
||||
assert_public_ty(&db, "/src/a.py", "x", "Unbound");
|
||||
assert_public_ty(&db, "/src/a.py", "x", "Unknown");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -4615,7 +4729,7 @@ mod tests {
|
|||
let mut db = setup_db();
|
||||
db.write_file("/src/a.pyi", "class C(object): pass")?;
|
||||
let file = system_path_to_file(&db, "/src/a.pyi").unwrap();
|
||||
let ty = global_symbol_ty(&db, file, "C");
|
||||
let ty = global_symbol(&db, file, "C").expect_type();
|
||||
|
||||
let base = ty
|
||||
.expect_class_literal()
|
||||
|
@ -4906,20 +5020,12 @@ mod tests {
|
|||
|
||||
db.write_dedented("src/a.py", "[z for z in x]")?;
|
||||
|
||||
assert_scope_ty(&db, "src/a.py", &["<listcomp>"], "x", "Unbound");
|
||||
assert_scope_ty(&db, "src/a.py", &["<listcomp>"], "x", "Unknown");
|
||||
|
||||
// Iterating over an `Unbound` yields `Unknown`:
|
||||
assert_scope_ty(&db, "src/a.py", &["<listcomp>"], "z", "Unknown");
|
||||
|
||||
// TODO: not the greatest error message in the world! --Alex
|
||||
assert_file_diagnostics(
|
||||
&db,
|
||||
"src/a.py",
|
||||
&[
|
||||
"Name `x` used when not defined",
|
||||
"Object of type `Unbound` is not iterable",
|
||||
],
|
||||
);
|
||||
assert_file_diagnostics(&db, "src/a.py", &["Name `x` used when not defined"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -5050,7 +5156,7 @@ mod tests {
|
|||
",
|
||||
)?;
|
||||
|
||||
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "z", "Unbound");
|
||||
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "z", "Unknown");
|
||||
|
||||
// (There is a diagnostic for invalid syntax that's emitted, but it's not listed by `assert_file_diagnostics`)
|
||||
assert_file_diagnostics(&db, "src/a.py", &["Name `z` used when not defined"]);
|
||||
|
@ -5175,7 +5281,7 @@ mod tests {
|
|||
])?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let x_ty = global_symbol_ty(&db, a, "x");
|
||||
let x_ty = global_symbol(&db, a, "x").expect_type();
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
|
@ -5184,7 +5290,7 @@ mod tests {
|
|||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
|
||||
let x_ty_2 = global_symbol_ty(&db, a, "x");
|
||||
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
|
||||
|
||||
assert_eq!(x_ty_2.display(&db).to_string(), "Literal[20]");
|
||||
|
||||
|
@ -5201,7 +5307,7 @@ mod tests {
|
|||
])?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let x_ty = global_symbol_ty(&db, a, "x");
|
||||
let x_ty = global_symbol(&db, a, "x").expect_type();
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
|
@ -5211,7 +5317,7 @@ mod tests {
|
|||
|
||||
db.clear_salsa_events();
|
||||
|
||||
let x_ty_2 = global_symbol_ty(&db, a, "x");
|
||||
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
|
||||
|
||||
assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
|
@ -5237,7 +5343,7 @@ mod tests {
|
|||
])?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let x_ty = global_symbol_ty(&db, a, "x");
|
||||
let x_ty = global_symbol(&db, a, "x").expect_type();
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
|
@ -5247,7 +5353,7 @@ mod tests {
|
|||
|
||||
db.clear_salsa_events();
|
||||
|
||||
let x_ty_2 = global_symbol_ty(&db, a, "x");
|
||||
let x_ty_2 = global_symbol(&db, a, "x").expect_type();
|
||||
|
||||
assert_eq!(x_ty_2.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue