[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:
David Peter 2024-10-31 20:05:53 +01:00 committed by GitHub
parent d1189c20df
commit 53fa32a389
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 767 additions and 516 deletions

View file

@ -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]] = ([], [])

View file

@ -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
```

View file

@ -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"]
```

View file

@ -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]
```

View file

@ -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)
```

View file

@ -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)
```

View file

@ -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:

View file

@ -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

View file

@ -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)
```

View file

@ -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
```

View file

@ -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
```

View file

@ -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])
```

View file

@ -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
```

View file

@ -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;

View file

@ -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(

View file

@ -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 {

View file

@ -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.

View 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,
}
}
}

View file

@ -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());

View file

@ -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();

View file

@ -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.

View 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,
];

View file

@ -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]");