Refactor CallOutcome to Result (#16161)

This commit is contained in:
Micha Reiser 2025-02-18 12:34:39 +00:00 committed by GitHub
parent 5cd0de3e4c
commit 4ed5db0d42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 717 additions and 737 deletions

View file

@ -40,7 +40,7 @@ class C:
return 42
x = C()
# error: [invalid-argument-type]
# error: [unsupported-operator] "Operator `-=` is unsupported between objects of type `C` and `Literal[1]`"
x -= 1
reveal_type(x) # revealed: int

View file

@ -244,10 +244,7 @@ class B:
def __rsub__(self, other: A) -> B:
return B()
# TODO: this should be `B` (the return annotation of `B.__rsub__`),
# because `A.__sub__` is annotated as only accepting `A`,
# but `B.__rsub__` will accept `A`.
reveal_type(A() - B()) # revealed: A
reveal_type(A() - B()) # revealed: B
```
## Callable instances as dunders
@ -263,7 +260,10 @@ class B:
__add__ = A()
# TODO: this could be `int` if we declare `B.__add__` using a `Callable` type
reveal_type(B() + B()) # revealed: Unknown | int
# TODO: Should not be an error: `A` instance is not a method descriptor, don't prepend `self` arg.
# Revealed type should be `Unknown | int`.
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `B` and `B`"
reveal_type(B() + B()) # revealed: Unknown
```
## Integration test: numbers from typeshed
@ -277,22 +277,14 @@ return annotations from the widening, and preserve a bit more precision here?
reveal_type(3j + 3.14) # revealed: int | float | complex
reveal_type(4.2 + 42) # revealed: int | float
reveal_type(3j + 3) # revealed: int | float | complex
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3.14 + 3j) # revealed: int | float
# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(42 + 4.2) # revealed: int
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
reveal_type(3.14 + 3j) # revealed: int | float | complex
reveal_type(42 + 4.2) # revealed: int | float
reveal_type(3 + 3j) # revealed: int | float | complex
def _(x: bool, y: int):
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: int | float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int
reveal_type(y + 4.12) # revealed: int | float
```
## With literal types
@ -309,8 +301,7 @@ class A:
return self
reveal_type(A() + 1) # revealed: A
# TODO should be `A` since `int.__add__` doesn't support `A` instances
reveal_type(1 + A()) # revealed: int
reveal_type(1 + A()) # revealed: A
reveal_type(A() + "foo") # revealed: A
# TODO should be `A` since `str.__add__` doesn't support `A` instances

View file

@ -10,9 +10,10 @@ reveal_type(-3 // 3) # revealed: Literal[-1]
reveal_type(-3 / 3) # revealed: float
reveal_type(5 % 3) # revealed: Literal[2]
# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: int
# TODO: This should emit an unsupported-operator error but we don't currently
# verify that the actual parameter to `int.__add__` matches the declared
# formal parameter type.
reveal_type(2 + "f") # revealed: Unknown
def lhs(x: int):
reveal_type(x + 1) # revealed: int

View file

@ -52,7 +52,7 @@ class NonCallable:
__call__ = 1
a = NonCallable()
# error: "Object of type `Unknown | Literal[1]` is not callable (due to union element `Literal[1]`)"
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(a()) # revealed: Unknown
```
@ -67,8 +67,8 @@ def _(flag: bool):
def __call__(self) -> int: ...
a = NonCallable()
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
reveal_type(a()) # revealed: Unknown | int
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(a()) # revealed: int | Unknown
```
## Call binding errors
@ -99,3 +99,26 @@ c = C()
# error: 13 [invalid-argument-type] "Object of type `C` cannot be assigned to parameter 1 (`self`) of function `__call__`; expected type `int`"
reveal_type(c()) # revealed: int
```
## Union over callables
### Possibly unbound `__call__`
```py
def outer(cond1: bool):
class Test:
if cond1:
def __call__(self): ...
class Other:
def __call__(self): ...
def inner(cond2: bool):
if cond2:
a = Test()
else:
a = Other()
# error: [call-non-callable] "Object of type `Test` is not callable (possibly unbound `__call__` method)"
a()
```

View file

@ -278,10 +278,10 @@ proper diagnostics in case of missing or superfluous arguments.
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type() # revealed: Unknown
reveal_type()
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2) # revealed: Literal[1]
reveal_type(1, 2)
```
### `static_assert`
@ -290,7 +290,6 @@ reveal_type(1, 2) # revealed: Literal[1]
from knot_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
# error: [static-assert-error]
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"

View file

@ -39,8 +39,8 @@ def _(flag: bool):
else:
def f() -> int:
return 1
x = f() # error: "Object of type `Literal[1] | Literal[f]` is not callable (due to union element `Literal[1]`)"
reveal_type(x) # revealed: Unknown | int
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
reveal_type(x) # revealed: int | Unknown
```
## Multiple non-callable elements in a union
@ -56,8 +56,8 @@ def _(flag: bool, flag2: bool):
else:
def f() -> int:
return 1
# error: "Object of type `Literal[1, "foo"] | Literal[f]` is not callable (due to union elements Literal[1], Literal["foo"])"
# revealed: Unknown | int
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
# revealed: int | Unknown
reveal_type(f())
```
@ -72,6 +72,39 @@ def _(flag: bool):
else:
f = "foo"
x = f() # error: "Object of type `Literal[1, "foo"]` is not callable"
x = f() # error: [call-non-callable] "Object of type `Literal[1, "foo"]` is not callable"
reveal_type(x) # revealed: Unknown
```
## Mismatching signatures
Calling a union where the arguments don't match the signature of all variants.
```py
def f1(a: int) -> int: ...
def f2(a: str) -> str: ...
def _(flag: bool):
if flag:
f = f1
else:
f = f2
# error: [invalid-argument-type] "Object of type `Literal[3]` cannot be assigned to parameter 1 (`a`) of function `f2`; expected type `str`"
x = f(3)
reveal_type(x) # revealed: int | str
```
## Any non-callable variant
```py
def f1(a: int): ...
def _(flag: bool):
if flag:
f = f1
else:
f = "This is a string literal"
# error: [call-non-callable] "Object of type `Literal["This is a string literal"]` is not callable"
x = f(3)
reveal_type(x) # revealed: Unknown
```

View file

@ -21,8 +21,9 @@ class A:
reveal_type("hello" in A()) # revealed: bool
reveal_type("hello" not in A()) # revealed: bool
# TODO: should emit diagnostic, need to check arg type, will fail
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `not in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 not in A()) # revealed: bool
```
@ -126,9 +127,9 @@ class A:
reveal_type(CheckContains() in A()) # revealed: bool
# TODO: should emit diagnostic, need to check arg type,
# should not fall back to __iter__ or __getitem__
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckIter` and `A`"
reveal_type(CheckIter() in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `CheckGetItem` and `A`"
reveal_type(CheckGetItem() in A()) # revealed: bool
class B:
@ -154,7 +155,8 @@ class A:
def __getitem__(self, key: str) -> str:
return "foo"
# TODO should emit a diagnostic
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `A`, in comparing `Literal[42]` with `A`"
reveal_type(42 in A()) # revealed: bool
# error: [unsupported-operator] "Operator `in` is not supported for types `str` and `A`, in comparing `Literal["hello"]` with `A`"
reveal_type("hello" in A()) # revealed: bool
```

View file

@ -117,14 +117,11 @@ class B:
def __ne__(self, other: str) -> B:
return B()
# TODO: should be `int` and `bytearray`.
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
#
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
# this can only happen with an invalid override of these methods,
# but we still support it.
reveal_type(B() == A()) # revealed: B
reveal_type(B() != A()) # revealed: B
reveal_type(B() == A()) # revealed: int
reveal_type(B() != A()) # revealed: bytearray
reveal_type(B() < A()) # revealed: list
reveal_type(B() <= A()) # revealed: set
@ -222,9 +219,8 @@ class B(A):
def __gt__(self, other: int) -> B:
return B()
# TODO: should be `A`, need to check argument type and fall back to LHS method
reveal_type(A() < B()) # revealed: B
reveal_type(A() > B()) # revealed: B
reveal_type(A() < B()) # revealed: A
reveal_type(A() > B()) # revealed: A
```
## Operations involving instances of classes inheriting from `Any`
@ -272,9 +268,8 @@ class A:
def __ne__(self, other: int) -> A:
return A()
# TODO: it should be `bool`, need to check arg type and fall back to `is` and `is not`
reveal_type(A() == A()) # revealed: A
reveal_type(A() != A()) # revealed: A
reveal_type(A() == A()) # revealed: bool
reveal_type(A() != A()) # revealed: bool
```
## Object Comparisons with Typeshed
@ -305,12 +300,14 @@ reveal_type(1 >= 1.0) # revealed: bool
reveal_type(1 == 2j) # revealed: bool
reveal_type(1 != 2j) # revealed: bool
# TODO: should be Unknown and emit diagnostic,
# need to check arg type and should be failed
reveal_type(1 < 2j) # revealed: bool
reveal_type(1 <= 2j) # revealed: bool
reveal_type(1 > 2j) # revealed: bool
reveal_type(1 >= 2j) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 < 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 <= 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 > 2j) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `complex`, in comparing `Literal[1]` with `complex`"
reveal_type(1 >= 2j) # revealed: Unknown
def f(x: bool, y: int):
reveal_type(x < y) # revealed: bool

View file

@ -12,8 +12,8 @@ reveal_type(1 is 1) # revealed: bool
reveal_type(1 is not 1) # revealed: bool
reveal_type(1 is 2) # revealed: Literal[False]
reveal_type(1 is not 7) # revealed: Literal[True]
# TODO: should be Unknown, and emit diagnostic, once we check call argument types
reveal_type(1 <= "" and 0 < 1) # revealed: bool
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `Literal[1]` with `Literal[""]`"
reveal_type(1 <= "" and 0 < 1) # revealed: Unknown & ~AlwaysTruthy | Literal[True]
```
## Integer instance

View file

@ -8,7 +8,9 @@ types, we can infer that the result for the intersection type is also true/false
```py
from typing import Literal
class Base: ...
class Base:
def __gt__(self, other) -> bool:
return False
class Child1(Base):
def __eq__(self, other) -> Literal[True]:

View file

@ -23,6 +23,7 @@ from __future__ import annotations
class A:
def __lt__(self, other) -> A: ...
def __gt__(self, other) -> bool: ...
class B:
def __lt__(self, other) -> B: ...

View file

@ -92,11 +92,14 @@ reveal_type(a == b) # revealed: bool
# TODO: should be Literal[True], once we implement (in)equality for mismatched literals
reveal_type(a != b) # revealed: bool
# TODO: should be Unknown and add more informative diagnostics
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a < b) # revealed: Unknown
# error: [unsupported-operator] "Operator `<=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a <= b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a > b) # revealed: Unknown
# error: [unsupported-operator] "Operator `>=` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
reveal_type(a >= b) # revealed: Unknown
```
However, if the lexicographic comparison completes without reaching a point where str and int are

View file

@ -9,28 +9,22 @@ def _(flag: bool, flag1: bool, flag2: bool):
b = 0 not in 10 # error: "Operator `not in` is not supported for types `Literal[0]` and `Literal[10]`"
reveal_type(b) # revealed: bool
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `object` and `int`")
# error: [unsupported-operator] "Operator `<` is not supported for types `object` and `int`, in comparing `object` with `Literal[5]`"
c = object() < 5
# TODO: should be Unknown, once operand type check is implemented
reveal_type(c) # revealed: bool
reveal_type(c) # revealed: Unknown
# TODO: should error, once operand type check is implemented
# ("Operator `<` is not supported for types `int` and `object`")
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `object`, in comparing `Literal[5]` with `object`"
d = 5 < object()
# TODO: should be Unknown, once operand type check is implemented
reveal_type(d) # revealed: bool
reveal_type(d) # revealed: Unknown
int_literal_or_str_literal = 1 if flag else "foo"
# error: "Operator `in` is not supported for types `Literal[42]` and `Literal[1]`, in comparing `Literal[42]` with `Literal[1, "foo"]`"
e = 42 in int_literal_or_str_literal
reveal_type(e) # revealed: bool
# TODO: should error, need to check if __lt__ signature is valid for right operand
# error may be "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`
# error: [unsupported-operator] "Operator `<` is not supported for types `int` and `str`, in comparing `tuple[Literal[1], Literal[2]]` with `tuple[Literal[1], Literal["hello"]]`"
f = (1, 2) < (1, "hello")
# TODO: should be Unknown, once operand type check is implemented
reveal_type(f) # revealed: bool
reveal_type(f) # revealed: Unknown
# error: [unsupported-operator] "Operator `<` is not supported for types `A` and `A`, in comparing `tuple[bool, A]` with `tuple[bool, A]`"
g = (flag1, A()) < (flag2, A())

View file

@ -245,9 +245,10 @@ class Test2:
return 42
def _(flag: bool):
# TODO: Improve error message to state which union variant isn't iterable (https://github.com/astral-sh/ruff/issues/13989)
# error: "Object of type `Test | Test2` is not iterable"
for x in Test() if flag else Test2():
reveal_type(x) # revealed: Unknown
reveal_type(x) # revealed: int
```
## Union type as iterator where one union element has no `__next__` method
@ -263,5 +264,5 @@ class Test:
# error: [not-iterable] "Object of type `Test` is not iterable"
for x in Test():
reveal_type(x) # revealed: Unknown
reveal_type(x) # revealed: int
```

View file

@ -80,7 +80,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 `int` is not callable"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
with Manager():
...
```
@ -95,7 +95,7 @@ class Manager:
__exit__: int = 32
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because the method `__exit__` of type `int` is not callable"
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__exit__`"
with Manager():
...
```
@ -134,3 +134,19 @@ def _(flag: bool):
with Manager() as f:
reveal_type(f) # revealed: str
```
## Invalid `__enter__` signature
```py
class Manager:
def __enter__() -> str:
return "foo"
def __exit__(self, exc_type, exc_value, traceback): ...
context_expr = Manager()
# error: [invalid-context-manager] "Object of type `Manager` cannot be used with `with` because it does not correctly implement `__enter__`"
with context_expr as f:
reveal_type(f) # revealed: str
```

View file

@ -1,11 +1,11 @@
use std::hash::Hash;
use bitflags::bitflags;
use call::{CallDunderError, CallError};
use context::InferContext;
use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound};
use indexmap::IndexSet;
use itertools::Itertools;
use ruff_db::diagnostic::Severity;
use ruff_db::files::File;
use ruff_python_ast as ast;
use ruff_python_ast::python_version::PythonVersion;
@ -36,9 +36,7 @@ use crate::symbol::{
global_symbol, imported_symbol, known_module_symbol, symbol, symbol_from_bindings,
symbol_from_declarations, Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
};
use crate::types::call::{
bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind,
};
use crate::types::call::{bind_call, CallArguments, CallBinding, CallOutcome};
use crate::types::class_base::ClassBase;
use crate::types::diagnostic::INVALID_TYPE_FORM;
use crate::types::infer::infer_unpack_types;
@ -1469,9 +1467,9 @@ impl<'db> Type<'db> {
return Truthiness::Ambiguous;
};
if let Some(Type::BooleanLiteral(bool_val)) = bool_method
if let Ok(Type::BooleanLiteral(bool_val)) = bool_method
.call_bound(db, instance_ty, &CallArguments::positional([]))
.return_type(db)
.map(|outcome| outcome.return_type(db))
{
bool_val.into()
} else {
@ -1544,72 +1542,39 @@ impl<'db> Type<'db> {
}
let return_ty = match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) {
// TODO: emit a diagnostic
CallDunderResult::MethodNotAvailable => return None,
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => outcome.return_type(db),
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
outcome.return_type(db)?
}
// TODO: emit a diagnostic
Err(err) => err.return_type(db)?,
};
non_negative_int_literal(db, return_ty)
}
/// Return the outcome of calling an object of this type.
#[must_use]
fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> {
/// Calls `self`
///
/// Returns `Ok` if the call with the given arguments is successful and `Err` otherwise.
fn call(
self,
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
) -> Result<CallOutcome<'db>, CallError<'db>> {
match self {
Type::FunctionLiteral(function_type) => {
let mut binding = bind_call(db, arguments, function_type.signature(db), self);
match function_type.known(db) {
Some(KnownFunction::RevealType) => {
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
CallOutcome::revealed(binding, revealed_ty)
}
Some(KnownFunction::StaticAssert) => {
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
let truthiness = parameter_ty.bool(db);
if truthiness.is_always_true() {
CallOutcome::callable(binding)
} else {
let error_kind = if let Some(message) =
message.into_string_literal().map(|s| &**s.value(db))
{
StaticAssertionErrorKind::CustomError(message)
} else if parameter_ty == Type::BooleanLiteral(false) {
StaticAssertionErrorKind::ArgumentIsFalse
} else if truthiness.is_always_false() {
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty)
} else {
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(
parameter_ty,
)
};
CallOutcome::StaticAssertionError {
binding,
error_kind,
}
}
} else {
CallOutcome::callable(binding)
}
}
Some(KnownFunction::IsEquivalentTo) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_equivalent_to(db, ty_b)));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsSubtypeOf) => {
let (ty_a, ty_b) = binding
.two_parameter_types()
.unwrap_or((Type::unknown(), Type::unknown()));
binding.set_return_type(Type::BooleanLiteral(ty_a.is_subtype_of(db, ty_b)));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsAssignableTo) => {
let (ty_a, ty_b) = binding
@ -1617,7 +1582,6 @@ impl<'db> Type<'db> {
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_assignable_to(db, ty_b)));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsDisjointFrom) => {
let (ty_a, ty_b) = binding
@ -1625,7 +1589,6 @@ impl<'db> Type<'db> {
.unwrap_or((Type::unknown(), Type::unknown()));
binding
.set_return_type(Type::BooleanLiteral(ty_a.is_disjoint_from(db, ty_b)));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsGradualEquivalentTo) => {
let (ty_a, ty_b) = binding
@ -1634,22 +1597,18 @@ impl<'db> Type<'db> {
binding.set_return_type(Type::BooleanLiteral(
ty_a.is_gradual_equivalent_to(db, ty_b),
));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsFullyStatic) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_fully_static(db)));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsSingleton) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_singleton(db)));
CallOutcome::callable(binding)
}
Some(KnownFunction::IsSingleValued) => {
let ty = binding.one_parameter_type().unwrap_or(Type::unknown());
binding.set_return_type(Type::BooleanLiteral(ty.is_single_valued(db)));
CallOutcome::callable(binding)
}
Some(KnownFunction::Len) => {
@ -1658,108 +1617,111 @@ impl<'db> Type<'db> {
binding.set_return_type(len_ty);
}
};
CallOutcome::callable(binding)
}
Some(KnownFunction::Repr) => {
if let Some(first_arg) = binding.one_parameter_type() {
binding.set_return_type(first_arg.repr(db));
};
CallOutcome::callable(binding)
}
Some(KnownFunction::AssertType) => {
let Some((_, asserted_ty)) = binding.two_parameter_types() else {
return CallOutcome::callable(binding);
};
CallOutcome::asserted(binding, asserted_ty)
}
Some(KnownFunction::Cast) => {
// TODO: Use `.two_parameter_tys()` exclusively
// when overloads are supported.
if binding.two_parameter_types().is_none() {
return CallOutcome::callable(binding);
};
if let Some(casted_ty) = arguments.first_argument() {
binding.set_return_type(casted_ty);
if binding.two_parameter_types().is_some() {
binding.set_return_type(casted_ty);
}
};
CallOutcome::callable(binding)
}
_ => CallOutcome::callable(binding),
_ => {}
};
if binding.has_binding_errors() {
Err(CallError::BindingError { binding })
} else {
Ok(CallOutcome::Single(binding))
}
}
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Type::ClassLiteral(ClassLiteralType { class }) => {
CallOutcome::callable(CallBinding::from_return_type(match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument()
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
Ok(CallOutcome::Single(CallBinding::from_return_type(
match class.known(db) {
// If the class is the builtin-bool class (for example `bool(1)`), we try to
// return the specific truthiness value of the input arg, `Literal[True]` for
// the example above.
Some(KnownClass::Bool) => arguments
.first_argument()
.map(|arg| arg.bool(db).into_type(db))
.unwrap_or(Type::BooleanLiteral(false)),
Some(KnownClass::Str) => arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
// TODO: Don't ignore the second and third arguments to `str`
// https://github.com/astral-sh/ruff/pull/16161#discussion_r1958425568
Some(KnownClass::Str) => arguments
.first_argument()
.map(|arg| arg.str(db))
.unwrap_or(Type::string_literal(db, "")),
_ => Type::Instance(InstanceType { class }),
}))
_ => Type::Instance(InstanceType { class }),
},
)))
}
instance_ty @ Type::Instance(_) => {
match instance_ty.call_dunder(db, "__call__", &arguments.with_self(instance_ty)) {
CallDunderResult::CallOutcome(CallOutcome::NotCallable { .. }) => {
// Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable"
CallOutcome::NotCallable {
not_callable_ty: self,
instance_ty
.call_dunder(db, "__call__", &arguments.with_self(instance_ty))
.map_err(|err| match err {
CallDunderError::Call(CallError::NotCallable { .. }) => {
// Turn "`<type of illegal '__call__'>` not callable" into
// "`X` not callable"
CallError::NotCallable {
not_callable_ty: self,
}
}
}
CallDunderResult::CallOutcome(outcome) => outcome,
CallDunderResult::PossiblyUnbound(call_outcome) => {
CallDunderError::Call(CallError::Union {
called_ty: _,
bindings,
errors,
}) => CallError::Union {
called_ty: self,
bindings,
errors,
},
CallDunderError::Call(error) => error,
// Turn "possibly unbound object of type `Literal['__call__']`"
// into "`X` not callable (possibly unbound `__call__` method)"
CallOutcome::PossiblyUnboundDunderCall {
called_ty: self,
call_outcome: Box::new(call_outcome),
CallDunderError::PossiblyUnbound(outcome) => {
CallError::PossiblyUnboundDunderCall {
called_type: self,
outcome: Box::new(outcome),
}
}
}
CallDunderResult::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable"
CallOutcome::NotCallable {
not_callable_ty: self,
CallDunderError::MethodNotAvailable => {
// Turn "`X.__call__` unbound" into "`X` not callable"
CallError::NotCallable {
not_callable_ty: self,
}
}
}
}
})
}
// Dynamic types are callable, and the return type is the same dynamic type
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
Type::Union(union) => CallOutcome::union(
self,
union
.elements(db)
.iter()
.map(|elem| elem.call(db, arguments)),
),
Type::Union(union) => {
CallOutcome::try_call_union(db, union, |element| element.call(db, arguments))
}
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call()"),
)),
))),
_ => CallOutcome::not_callable(self),
_ => Err(CallError::NotCallable {
not_callable_ty: self,
}),
}
}
@ -1769,13 +1731,12 @@ impl<'db> Type<'db> {
/// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`.
///
/// TODO: handle `super()` objects properly
#[must_use]
fn call_bound(
self,
db: &'db dyn Db,
receiver_ty: &Type<'db>,
arguments: &CallArguments<'_, 'db>,
) -> CallOutcome<'db> {
) -> Result<CallOutcome<'db>, CallError<'db>> {
debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal());
match self {
@ -1790,22 +1751,20 @@ impl<'db> Type<'db> {
self.call(db, arguments)
}
Type::Union(union) => CallOutcome::union(
self,
union
.elements(db)
.iter()
.map(|elem| elem.call_bound(db, receiver_ty, arguments)),
),
Type::Union(union) => CallOutcome::try_call_union(db, union, |element| {
element.call_bound(db, receiver_ty, arguments)
}),
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
Type::Intersection(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(
todo_type!("Type::Intersection.call_bound()"),
)),
))),
// Cases that duplicate, and thus must be kept in sync with, `Type::call()`
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
Type::Dynamic(_) => Ok(CallOutcome::Single(CallBinding::from_return_type(self))),
_ => CallOutcome::not_callable(self),
_ => Err(CallError::NotCallable {
not_callable_ty: self,
}),
}
}
@ -1815,15 +1774,14 @@ impl<'db> Type<'db> {
db: &'db dyn Db,
name: &str,
arguments: &CallArguments<'_, 'db>,
) -> CallDunderResult<'db> {
) -> Result<CallOutcome<'db>, CallDunderError<'db>> {
match self.to_meta_type(db).member(db, name) {
Symbol::Type(callable_ty, Boundness::Bound) => {
CallDunderResult::CallOutcome(callable_ty.call(db, arguments))
}
Symbol::Type(callable_ty, Boundness::Bound) => Ok(callable_ty.call(db, arguments)?),
Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => {
CallDunderResult::PossiblyUnbound(callable_ty.call(db, arguments))
let call = callable_ty.call(db, arguments)?;
Err(CallDunderError::PossiblyUnbound(call))
}
Symbol::Unbound => CallDunderResult::MethodNotAvailable,
Symbol::Unbound => Err(CallDunderError::MethodNotAvailable),
}
}
@ -1844,34 +1802,51 @@ impl<'db> Type<'db> {
let dunder_iter_result =
self.call_dunder(db, "__iter__", &CallArguments::positional([self]));
match dunder_iter_result {
CallDunderResult::CallOutcome(ref call_outcome)
| CallDunderResult::PossiblyUnbound(ref call_outcome) => {
let Some(iterator_ty) = call_outcome.return_type(db) else {
return IterationOutcome::NotIterable {
not_iterable_ty: self,
};
};
match &dunder_iter_result {
Ok(outcome) | Err(CallDunderError::PossiblyUnbound(outcome)) => {
let iterator_ty = outcome.return_type(db);
return if let Some(element_ty) = iterator_ty
.call_dunder(db, "__next__", &CallArguments::positional([iterator_ty]))
.return_type(db)
{
if matches!(dunder_iter_result, CallDunderResult::PossiblyUnbound(..)) {
return match iterator_ty.call_dunder(
db,
"__next__",
&CallArguments::positional([iterator_ty]),
) {
Ok(outcome) => {
if matches!(
dunder_iter_result,
Err(CallDunderError::PossiblyUnbound { .. })
) {
IterationOutcome::PossiblyUnboundDunderIter {
iterable_ty: self,
element_ty: outcome.return_type(db),
}
} else {
IterationOutcome::Iterable {
element_ty: outcome.return_type(db),
}
}
}
Err(CallDunderError::PossiblyUnbound(outcome)) => {
IterationOutcome::PossiblyUnboundDunderIter {
iterable_ty: self,
element_ty,
element_ty: outcome.return_type(db),
}
} else {
IterationOutcome::Iterable { element_ty }
}
} else {
IterationOutcome::NotIterable {
Err(_) => IterationOutcome::NotIterable {
not_iterable_ty: self,
}
},
};
}
CallDunderResult::MethodNotAvailable => {}
// If `__iter__` exists but can't be called or doesn't have the expected signature,
// return not iterable over falling back to `__getitem__`.
Err(CallDunderError::Call(_)) => {
return IterationOutcome::NotIterable {
not_iterable_ty: self,
}
}
Err(CallDunderError::MethodNotAvailable) => {
// No `__iter__` attribute, try `__getitem__` next.
}
}
// Although it's not considered great practice,
@ -1880,19 +1855,23 @@ impl<'db> Type<'db> {
//
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
// accepting `int` or `SupportsIndex`
if let Some(element_ty) = self
.call_dunder(
db,
"__getitem__",
&CallArguments::positional([self, KnownClass::Int.to_instance(db)]),
)
.return_type(db)
{
IterationOutcome::Iterable { element_ty }
} else {
IterationOutcome::NotIterable {
not_iterable_ty: self,
match self.call_dunder(
db,
"__getitem__",
&CallArguments::positional([self, KnownClass::Int.to_instance(db)]),
) {
Ok(outcome) => IterationOutcome::Iterable {
element_ty: outcome.return_type(db),
},
Err(CallDunderError::PossiblyUnbound(outcome)) => {
IterationOutcome::PossiblyUnboundDunderIter {
iterable_ty: self,
element_ty: outcome.return_type(db),
}
}
Err(_) => IterationOutcome::NotIterable {
not_iterable_ty: self,
},
}
}
@ -3694,20 +3673,23 @@ impl<'db> Class<'db> {
let arguments = CallArguments::positional([name, bases, namespace]);
let return_ty_result = match metaclass.call(db, &arguments) {
CallOutcome::NotCallable { not_callable_ty } => Err(MetaclassError {
Ok(outcome) => Ok(outcome.return_type(db)),
Err(CallError::NotCallable { not_callable_ty }) => Err(MetaclassError {
kind: MetaclassErrorKind::NotCallable(not_callable_ty),
}),
CallOutcome::Union {
outcomes,
Err(CallError::Union {
called_ty,
} => {
errors,
bindings,
}) => {
let mut partly_not_callable = false;
let return_ty = outcomes
let return_ty = errors
.iter()
.fold(None, |acc, outcome| {
let ty = outcome.return_type(db);
.fold(None, |acc, error| {
let ty = error.return_type(db);
match (acc, ty) {
(acc, None) => {
@ -3718,7 +3700,13 @@ impl<'db> Class<'db> {
(Some(builder), Some(ty)) => Some(builder.add(ty)),
}
})
.map(UnionBuilder::build);
.map(|mut builder| {
for binding in bindings {
builder = builder.add(binding.return_type());
}
builder.build()
});
if partly_not_callable {
Err(MetaclassError {
@ -3729,16 +3717,13 @@ impl<'db> Class<'db> {
}
}
CallOutcome::PossiblyUnboundDunderCall { called_ty, .. } => Err(MetaclassError {
kind: MetaclassErrorKind::PartlyNotCallable(called_ty),
Err(CallError::PossiblyUnboundDunderCall { .. }) => Err(MetaclassError {
kind: MetaclassErrorKind::PartlyNotCallable(metaclass),
}),
// TODO we should also check for binding errors that would indicate the metaclass
// does not accept the right arguments
CallOutcome::Callable { binding }
| CallOutcome::RevealType { binding, .. }
| CallOutcome::StaticAssertionError { binding, .. }
| CallOutcome::AssertType { binding, .. } => Ok(binding.return_type()),
Err(CallError::BindingError { binding }) => Ok(binding.return_type()),
};
return return_ty_result.map(|ty| ty.to_meta_type(db));

View file

@ -1,423 +1,206 @@
use super::context::InferContext;
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
use super::{Signature, Type};
use crate::types::UnionType;
use crate::Db;
use ruff_db::diagnostic::DiagnosticId;
use ruff_python_ast as ast;
mod arguments;
mod bind;
pub(super) use arguments::{Argument, CallArguments};
pub(super) use bind::{bind_call, CallBinding};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum StaticAssertionErrorKind<'db> {
ArgumentIsFalse,
ArgumentIsFalsy(Type<'db>),
ArgumentTruthinessIsAmbiguous(Type<'db>),
CustomError(&'db str),
}
/// A successfully bound call where all arguments are valid.
///
/// It's guaranteed that the wrapped bindings have no errors.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallOutcome<'db> {
Callable {
binding: CallBinding<'db>,
},
RevealType {
binding: CallBinding<'db>,
revealed_ty: Type<'db>,
},
NotCallable {
not_callable_ty: Type<'db>,
},
Union {
called_ty: Type<'db>,
outcomes: Box<[CallOutcome<'db>]>,
},
PossiblyUnboundDunderCall {
called_ty: Type<'db>,
call_outcome: Box<CallOutcome<'db>>,
},
StaticAssertionError {
binding: CallBinding<'db>,
error_kind: StaticAssertionErrorKind<'db>,
},
AssertType {
binding: CallBinding<'db>,
asserted_ty: Type<'db>,
},
/// The call resolves to exactly one binding.
Single(CallBinding<'db>),
/// The call resolves to multiple bindings.
Union(Box<[CallBinding<'db>]>),
}
impl<'db> CallOutcome<'db> {
/// Create a new `CallOutcome::Callable` with given binding.
pub(super) fn callable(binding: CallBinding<'db>) -> CallOutcome<'db> {
CallOutcome::Callable { binding }
}
/// Calls each union element using the provided `call` function.
///
/// Returns `Ok` if all variants can be called without error according to the callback and `Err` otherwise.
pub(super) fn try_call_union<F>(
db: &'db dyn Db,
union: UnionType<'db>,
call: F,
) -> Result<Self, CallError<'db>>
where
F: Fn(Type<'db>) -> Result<Self, CallError<'db>>,
{
let elements = union.elements(db);
let mut bindings = Vec::with_capacity(elements.len());
let mut errors = Vec::new();
let mut not_callable = true;
/// Create a new `CallOutcome::NotCallable` with given not-callable type.
pub(super) fn not_callable(not_callable_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::NotCallable { not_callable_ty }
}
for element in elements {
match call(*element) {
Ok(CallOutcome::Single(binding)) => bindings.push(binding),
Ok(CallOutcome::Union(inner_bindings)) => {
bindings.extend(inner_bindings);
}
Err(error) => {
not_callable |= error.is_not_callable();
errors.push(error);
}
}
}
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::RevealType {
binding,
revealed_ty,
if errors.is_empty() {
Ok(CallOutcome::Union(bindings.into()))
} else if bindings.is_empty() && not_callable {
Err(CallError::NotCallable {
not_callable_ty: Type::Union(union),
})
} else {
Err(CallError::Union {
errors: errors.into(),
bindings: bindings.into(),
called_ty: Type::Union(union),
})
}
}
/// Create a new `CallOutcome::Union` with given wrapped outcomes.
pub(super) fn union(
called_ty: Type<'db>,
outcomes: impl IntoIterator<Item = CallOutcome<'db>>,
) -> CallOutcome<'db> {
CallOutcome::Union {
called_ty,
outcomes: outcomes.into_iter().collect(),
}
}
/// Create a new `CallOutcome::AssertType` with given asserted and return types.
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::AssertType {
binding,
asserted_ty,
}
}
/// Get the return type of the call, or `None` if not callable.
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
/// The type returned by this call.
pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> {
match self {
Self::Callable { binding } => Some(binding.return_type()),
Self::RevealType {
binding,
revealed_ty: _,
} => Some(binding.return_type()),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
called_ty: _,
} => outcomes
.iter()
// If all outcomes are NotCallable, we return None; if some outcomes are callable
// and some are not, we return a union including Unknown.
.fold(None, |acc, outcome| {
let ty = outcome.return_type(db);
match (acc, ty) {
(None, None) => None,
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
(Some(builder), ty) => Some(builder.add(ty.unwrap_or(Type::unknown()))),
}
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_type(db),
Self::StaticAssertionError { .. } => Some(Type::none(db)),
Self::AssertType {
binding,
asserted_ty: _,
} => Some(binding.return_type()),
}
}
/// Get the return type of the call, emitting default diagnostics if needed.
pub(super) fn unwrap_with_diagnostic(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
) -> Type<'db> {
match self.return_type_result(context, node) {
Ok(return_ty) => return_ty,
Err(NotCallableError::Type {
not_callable_ty,
return_ty,
}) => {
context.report_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable",
not_callable_ty.display(context.db())
),
);
return_ty
}
Err(NotCallableError::UnionElement {
not_callable_ty,
called_ty,
return_ty,
}) => {
context.report_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (due to union element `{}`)",
called_ty.display(context.db()),
not_callable_ty.display(context.db()),
),
);
return_ty
}
Err(NotCallableError::UnionElements {
not_callable_tys,
called_ty,
return_ty,
}) => {
context.report_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (due to union elements {})",
called_ty.display(context.db()),
not_callable_tys.display(context.db()),
),
);
return_ty
}
Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: called_ty,
return_ty,
}) => {
context.report_lint(
&CALL_NON_CALLABLE,
node,
format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_ty.display(context.db())
),
);
return_ty
Self::Single(binding) => binding.return_type(),
Self::Union(bindings) => {
UnionType::from_elements(db, bindings.iter().map(bind::CallBinding::return_type))
}
}
}
/// Get the return type of the call as a result.
pub(super) fn return_type_result(
&self,
context: &InferContext<'db>,
node: ast::AnyNodeRef,
) -> Result<Type<'db>, NotCallableError<'db>> {
// TODO should this method emit diagnostics directly, or just return results that allow the
// caller to decide about emitting diagnostics? Currently it emits binding diagnostics, but
// only non-callable diagnostics in the union case, which is inconsistent.
pub(super) fn bindings(&self) -> &[CallBinding<'db>] {
match self {
Self::Callable { binding } => {
binding.report_diagnostics(context, node);
Ok(binding.return_type())
}
Self::RevealType {
binding,
revealed_ty,
} => {
binding.report_diagnostics(context, node);
context.report_diagnostic(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
);
Ok(binding.return_type())
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
return_ty: Type::unknown(),
}),
Self::PossiblyUnboundDunderCall {
called_ty,
call_outcome,
} => Err(NotCallableError::PossiblyUnboundDunderCall {
callable_ty: *called_ty,
return_ty: call_outcome
.return_type(context.db())
.unwrap_or(Type::unknown()),
}),
Self::Union {
outcomes,
called_ty,
} => {
let mut not_callable = vec![];
let mut union_builder = UnionBuilder::new(context.db());
let mut revealed = false;
for outcome in outcomes {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::unknown()
}
Self::RevealType {
binding,
revealed_ty: _,
} => {
if revealed {
binding.return_type()
} else {
revealed = true;
outcome.unwrap_with_diagnostic(context, node)
}
}
_ => outcome.unwrap_with_diagnostic(context, node),
};
union_builder = union_builder.add(return_ty);
}
let return_ty = union_builder.build();
match not_callable[..] {
[] => Ok(return_ty),
[elem] => Err(NotCallableError::UnionElement {
not_callable_ty: elem,
called_ty: *called_ty,
return_ty,
}),
_ if not_callable.len() == outcomes.len() => Err(NotCallableError::Type {
not_callable_ty: *called_ty,
return_ty,
}),
_ => Err(NotCallableError::UnionElements {
not_callable_tys: not_callable.into_boxed_slice(),
called_ty: *called_ty,
return_ty,
}),
}
}
Self::StaticAssertionError {
binding,
error_kind,
} => {
binding.report_diagnostics(context, node);
match error_kind {
StaticAssertionErrorKind::ArgumentIsFalse => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: argument evaluates to `False`"),
);
}
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::CustomError(message) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: {message}"),
);
}
}
Ok(Type::unknown())
}
Self::AssertType {
binding,
asserted_ty,
} => {
let [actual_ty, _asserted] = binding.parameter_types() else {
return Ok(binding.return_type());
};
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
context.report_lint(
&TYPE_ASSERTION_FAILURE,
node,
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(context.db()),
asserted_ty.display(context.db()),
),
);
}
Ok(binding.return_type())
}
}
}
}
pub(super) enum CallDunderResult<'db> {
CallOutcome(CallOutcome<'db>),
PossiblyUnbound(CallOutcome<'db>),
MethodNotAvailable,
}
impl<'db> CallDunderResult<'db> {
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::CallOutcome(outcome) => outcome.return_type(db),
Self::PossiblyUnbound { .. } => None,
Self::MethodNotAvailable => None,
Self::Single(binding) => std::slice::from_ref(binding),
Self::Union(bindings) => bindings,
}
}
}
/// The reason why calling a type failed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum NotCallableError<'db> {
pub(super) enum CallError<'db> {
/// The type is not callable.
Type {
NotCallable {
/// The type that can't be called.
not_callable_ty: Type<'db>,
return_ty: Type<'db>,
},
/// A single union element is not callable.
UnionElement {
not_callable_ty: Type<'db>,
/// A call to a union failed because at least one variant
/// can't be called with the given arguments.
///
/// A union where all variants are not callable is represented as a `NotCallable` error.
Union {
/// The variants that can't be called with the given arguments.
errors: Box<[CallError<'db>]>,
/// The bindings for the callable variants (that have no binding errors).
bindings: Box<[CallBinding<'db>]>,
/// The union type that we tried calling.
called_ty: Type<'db>,
return_ty: Type<'db>,
},
/// Multiple (but not all) union elements are not callable.
UnionElements {
not_callable_tys: Box<[Type<'db>]>,
called_ty: Type<'db>,
return_ty: Type<'db>,
},
/// The type has a `__call__` method but it isn't always bound.
PossiblyUnboundDunderCall {
callable_ty: Type<'db>,
return_ty: Type<'db>,
called_type: Type<'db>,
outcome: Box<CallOutcome<'db>>,
},
/// The type is callable but not with the given arguments.
BindingError { binding: CallBinding<'db> },
}
impl<'db> NotCallableError<'db> {
/// The return type that should be used when a call is not callable.
pub(super) fn return_type(&self) -> Type<'db> {
impl<'db> CallError<'db> {
/// Returns a fallback return type to use that best approximates the return type of the call.
///
/// Returns `None` if the type isn't callable.
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Type { return_ty, .. } => *return_ty,
Self::UnionElement { return_ty, .. } => *return_ty,
Self::UnionElements { return_ty, .. } => *return_ty,
Self::PossiblyUnboundDunderCall { return_ty, .. } => *return_ty,
CallError::NotCallable { .. } => None,
// If some variants are callable, and some are not, return the union of the return types of the callable variants
// combined with `Type::Unknown`
CallError::Union {
errors, bindings, ..
} => Some(UnionType::from_elements(
db,
bindings
.iter()
.map(CallBinding::return_type)
.chain(errors.iter().map(|err| err.fallback_return_type(db))),
)),
Self::PossiblyUnboundDunderCall { outcome, .. } => Some(outcome.return_type(db)),
Self::BindingError { binding } => Some(binding.return_type()),
}
}
/// Returns the return type of the call or a fallback that
/// represents the best guess of the return type (e.g. the actual return type even if the
/// dunder is possibly unbound).
///
/// If the type is not callable, returns `Type::Unknown`.
pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> {
self.return_type(db).unwrap_or(Type::unknown())
}
/// The resolved type that was not callable.
///
/// For unions, returns the union type itself, which may contain a mix of callable and
/// non-callable types.
pub(super) fn called_type(&self) -> Type<'db> {
match self {
Self::Type {
Self::NotCallable {
not_callable_ty, ..
} => *not_callable_ty,
Self::UnionElement { called_ty, .. } => *called_ty,
Self::UnionElements { called_ty, .. } => *called_ty,
Self::PossiblyUnboundDunderCall {
callable_ty: called_ty,
..
} => *called_ty,
Self::Union { called_ty, .. } => *called_ty,
Self::PossiblyUnboundDunderCall { called_type, .. } => *called_type,
Self::BindingError { binding } => binding.callable_type(),
}
}
pub(super) const fn is_not_callable(&self) -> bool {
matches!(self, Self::NotCallable { .. })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallDunderError<'db> {
/// The dunder attribute exists but it can't be called with the given arguments.
///
/// This includes non-callable dunder attributes that are possibly unbound.
Call(CallError<'db>),
/// The type has the specified dunder method and it is callable
/// with the specified arguments without any binding errors
/// but it is possibly unbound.
PossiblyUnbound(CallOutcome<'db>),
/// The dunder method with the specified name is missing.
MethodNotAvailable,
}
impl<'db> CallDunderError<'db> {
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Call(error) => error.return_type(db),
Self::PossiblyUnbound(_) => None,
Self::MethodNotAvailable => None,
}
}
pub(super) fn fallback_return_type(&self, db: &'db dyn Db) -> Type<'db> {
self.return_type(db).unwrap_or(Type::unknown())
}
}
impl<'db> From<CallError<'db>> for CallDunderError<'db> {
fn from(error: CallError<'db>) -> Self {
Self::Call(error)
}
}

View file

@ -161,6 +161,10 @@ impl<'db> CallBinding<'db> {
}
}
pub(crate) fn callable_type(&self) -> Type<'db> {
self.callable_ty
}
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
self.return_ty = return_ty;
}
@ -195,12 +199,16 @@ impl<'db> CallBinding<'db> {
}
}
pub(super) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
let callable_name = self.callable_name(context.db());
for error in &self.errors {
error.report_diagnostic(context, node, callable_name);
}
}
pub(crate) fn has_binding_errors(&self) -> bool {
!self.errors.is_empty()
}
}
/// Information needed to emit a diagnostic regarding a parameter.

View file

@ -29,6 +29,7 @@
use std::num::NonZeroU32;
use itertools::{Either, Itertools};
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
@ -66,29 +67,30 @@ use crate::types::diagnostic::{
use crate::types::mro::MroErrorKind;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
todo_type, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType, FunctionType,
InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass,
KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType,
SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType,
TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints,
TypeVarInstance, UnionBuilder, UnionType,
todo_type, Boundness, Class, ClassLiteralType, DynamicType, FunctionType, InstanceType,
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, SubclassOfType,
Symbol, SymbolAndQualifiers, Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers,
TypeArrayDisplay, TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
UnionType,
};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use super::call::CallError;
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_non_subscriptable,
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
INVALID_METACLASS, SUBCLASS_OF_FINAL_CLASS,
INVALID_METACLASS, STATIC_ASSERT_ERROR, SUBCLASS_OF_FINAL_CLASS, TYPE_ASSERTION_FAILURE,
};
use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
use super::{global_symbol, ParameterExpectation, ParameterExpectations};
use super::{global_symbol, CallDunderError, ParameterExpectation, ParameterExpectations};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@ -1616,16 +1618,20 @@ impl<'db> TypeInferenceBuilder<'db> {
let target_ty = enter_ty
.call(self.db(), &CallArguments::positional([context_expression_ty]))
.return_type_result(&self.context, context_expression.into())
.unwrap_or_else(|err| {
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
// TODO: Use more specific error messages for the different error cases.
// E.g. hint toward the union variant that doesn't correctly implement enter,
// distinguish between a not callable `__enter__` attribute and a wrong signature.
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(),
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())
Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__enter__`",
context_expression = context_expression_ty.display(self.db()),
),
);
err.return_type()
err.fallback_return_type(self.db())
});
match exit {
@ -1663,16 +1669,17 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::none(self.db()),
]),
)
.return_type_result(&self.context, context_expression.into())
.is_err()
{
// TODO: Use more specific error messages for the different error cases.
// E.g. hint toward the union variant that doesn't correctly implement enter,
// distinguish between a not callable `__exit__` attribute and a wrong signature.
self.context.report_lint(
&INVALID_CONTEXT_MANAGER,
context_expression.into(),
format_args!(
"Object of type `{context_expression}` cannot be used with `with` because the method `__exit__` of type `{exit_ty}` is not callable",
"Object of type `{context_expression}` cannot be used with `with` because it does not correctly implement `__exit__`",
context_expression = context_expression_ty.display(self.db()),
exit_ty = exit_ty.display(self.db()),
),
);
}
@ -2207,10 +2214,8 @@ impl<'db> TypeInferenceBuilder<'db> {
self.db(),
&CallArguments::positional([target_type, value_type]),
);
let augmented_return_ty = match call
.return_type_result(&self.context, AnyNodeRef::StmtAugAssign(assignment))
{
Ok(t) => t,
let augmented_return_ty = match call {
Ok(t) => t.return_type(self.db()),
Err(e) => {
self.context.report_lint(
&UNSUPPORTED_OPERATOR,
@ -2221,7 +2226,7 @@ impl<'db> TypeInferenceBuilder<'db> {
value_type.display(self.db())
),
);
e.return_type()
e.fallback_return_type(self.db())
}
};
@ -3243,9 +3248,155 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_or_default();
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
function_type
.call(self.db(), &call_arguments)
.unwrap_with_diagnostic(&self.context, call_expression.into())
let call = function_type.call(self.db(), &call_arguments);
match call {
Ok(outcome) => {
for binding in outcome.bindings() {
let Some(known_function) = binding
.callable_type()
.into_function_literal()
.and_then(|function_type| function_type.known(self.db()))
else {
continue;
};
match known_function {
KnownFunction::RevealType => {
if let Some(revealed_type) = binding.one_parameter_type() {
self.context.report_diagnostic(
call_expression.into(),
DiagnosticId::RevealedType,
Severity::Info,
format_args!(
"Revealed type is `{}`",
revealed_type.display(self.db())
),
);
}
}
KnownFunction::AssertType => {
if let [actual_ty, asserted_ty] = binding.parameter_types() {
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
call_expression.into(),
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(self.db()),
asserted_ty.display(self.db()),
),
);
}
}
}
KnownFunction::StaticAssert => {
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
let truthiness = parameter_ty.bool(self.db());
if !truthiness.is_always_true() {
if let Some(message) =
message.into_string_literal().map(|s| &**s.value(self.db()))
{
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!("Static assertion error: {message}"),
);
} else if parameter_ty == Type::BooleanLiteral(false) {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!("Static assertion error: argument evaluates to `False`"),
);
} else if truthiness.is_always_false() {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(self.db())
),
);
} else {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(self.db())
),
);
};
}
}
}
_ => {}
}
}
outcome.return_type(self.db())
}
Err(err) => {
// TODO: We currently only report the first error. Ideally, we'd report
// an error saying that the union type can't be called, followed by a sub
// diagnostic explaining why.
fn report_call_error(
context: &InferContext,
err: CallError,
call_expression: &ast::ExprCall,
) {
match err {
CallError::NotCallable { not_callable_ty } => {
context.report_lint(
&CALL_NON_CALLABLE,
call_expression.into(),
format_args!(
"Object of type `{}` is not callable",
not_callable_ty.display(context.db())
),
);
}
CallError::Union {
called_ty: _,
bindings: _,
errors,
} => {
// TODO: Remove the `Vec::from` call once we use the Rust 2024 edition
// which adds `Box<[T]>::into_iter`
if let Some(first) = Vec::from(errors).into_iter().next() {
report_call_error(context, first, call_expression);
} else {
debug_assert!(
false,
"Expected `CalLError::Union` to at least have one error"
);
}
}
CallError::PossiblyUnboundDunderCall { called_type, .. } => {
context.report_lint(
&CALL_NON_CALLABLE,
call_expression.into(),
format_args!(
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
called_type.display(context.db())
),
);
}
CallError::BindingError { binding, .. } => {
binding.report_diagnostics(context, call_expression.into());
}
}
}
let return_type = err.fallback_return_type(self.db());
report_call_error(&self.context, err, call_expression);
return_type
}
}
}
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
@ -3567,37 +3718,23 @@ impl<'db> TypeInferenceBuilder<'db> {
}
};
if let CallDunderResult::CallOutcome(call)
| CallDunderResult::PossiblyUnbound(call) = operand_type.call_dunder(
match operand_type.call_dunder(
self.db(),
unary_dunder_method,
&CallArguments::positional([operand_type]),
) {
match call.return_type_result(&self.context, AnyNodeRef::ExprUnaryOp(unary)) {
Ok(t) => t,
Err(e) => {
self.context.report_lint(
&UNSUPPORTED_OPERATOR,
unary.into(),
format_args!(
"Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db()),
),
);
e.return_type()
}
Ok(outcome) => outcome.return_type(self.db()),
Err(e) => {
self.context.report_lint(
&UNSUPPORTED_OPERATOR,
unary.into(),
format_args!(
"Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db()),
),
);
e.fallback_return_type(self.db())
}
} else {
self.context.report_lint(
&UNSUPPORTED_OPERATOR,
unary.into(),
format_args!(
"Unary operator `{op}` is unsupported for type `{}`",
operand_type.display(self.db()),
),
);
Type::unknown()
}
}
}
@ -3835,25 +3972,28 @@ impl<'db> TypeInferenceBuilder<'db> {
reflected_dunder,
&CallArguments::positional([right_ty, left_ty]),
)
.return_type(self.db())
.or_else(|| {
.map(|outcome| outcome.return_type(self.db()))
.or_else(|_| {
left_ty
.call_dunder(
self.db(),
op.dunder(),
&CallArguments::positional([left_ty, right_ty]),
)
.return_type(self.db())
});
.map(|outcome| outcome.return_type(self.db()))
})
.ok();
}
}
// TODO: Use `call_dunder`?
let call_on_left_instance = if let Symbol::Type(class_member, _) =
left_class.member(self.db(), op.dunder())
{
class_member
.call(self.db(), &CallArguments::positional([left_ty, right_ty]))
.return_type(self.db())
.map(|outcome| outcome.return_type(self.db()))
.ok()
} else {
None
};
@ -3865,9 +4005,11 @@ impl<'db> TypeInferenceBuilder<'db> {
if let Symbol::Type(class_member, _) =
right_class.member(self.db(), op.reflected_dunder())
{
// TODO: Use `call_dunder`
class_member
.call(self.db(), &CallArguments::positional([right_ty, left_ty]))
.return_type(self.db())
.map(|outcome| outcome.return_type(self.db()))
.ok()
} else {
None
}
@ -4626,43 +4768,44 @@ impl<'db> TypeInferenceBuilder<'db> {
Type::IntLiteral(i64::from(bool)),
),
(value_ty, slice_ty) => {
// Resolve the value to its class.
let value_meta_ty = value_ty.to_meta_type(self.db());
// If the class defines `__getitem__`, return its return type.
//
// See: https://docs.python.org/3/reference/datamodel.html#class-getitem-versus-getitem
match value_meta_ty.member(self.db(), "__getitem__") {
Symbol::Unbound => {}
Symbol::Type(dunder_getitem_method, boundness) => {
if boundness == Boundness::PossiblyUnbound {
self.context.report_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
match value_ty.call_dunder(
self.db(),
"__getitem__",
&CallArguments::positional([value_ty, slice_ty]),
) {
Ok(outcome) => return outcome.return_type(self.db()),
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
self.context.report_lint(
&CALL_POSSIBLY_UNBOUND_METHOD,
value_node.into(),
format_args!(
"Method `__getitem__` of type `{}` is possibly unbound",
value_ty.display(self.db()),
),
);
return err.fallback_return_type(self.db());
}
Err(CallDunderError::Call(err)) => {
self.context.report_lint(
&CALL_NON_CALLABLE,
value_node.into(),
format_args!(
"Method `__getitem__` of type `{}` is possibly unbound",
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
err.called_type().display(self.db()),
value_ty.display(self.db()),
),
);
}
return dunder_getitem_method
.call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
.return_type_result(&self.context, value_node.into())
.unwrap_or_else(|err| {
self.context.report_lint(
&CALL_NON_CALLABLE,
value_node.into(),
format_args!(
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
err.called_type().display(self.db()),
value_ty.display(self.db()),
),
);
err.return_type()
});
return err.fallback_return_type(self.db());
}
}
Err(CallDunderError::MethodNotAvailable) => {
// try `__class_getitem__`
}
};
// Otherwise, if the value is itself a class and defines `__class_getitem__`,
// return its return type.
@ -4693,7 +4836,7 @@ impl<'db> TypeInferenceBuilder<'db> {
return ty
.call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
.return_type_result(&self.context, value_node.into())
.map(|outcome| outcome.return_type(self.db()))
.unwrap_or_else(|err| {
self.context.report_lint(
&CALL_NON_CALLABLE,
@ -4704,7 +4847,7 @@ impl<'db> TypeInferenceBuilder<'db> {
value_ty.display(self.db()),
),
);
err.return_type()
err.fallback_return_type(self.db())
});
}
}
@ -5929,23 +6072,20 @@ fn perform_rich_comparison<'db>(
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
// The following resource has details about the rich comparison algorithm:
// https://snarky.ca/unravelling-rich-comparison-operators/
//
// TODO: this currently gives the return type even if the arg types are invalid
// (e.g. int.__lt__ with string instance should be errored, currently bool)
let call_dunder = |op: RichCompareOperator,
left: InstanceType<'db>,
right: InstanceType<'db>| {
match left.class.class_member(db, op.dunder()) {
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
.call(
db,
&CallArguments::positional([Type::Instance(left), Type::Instance(right)]),
)
.return_type(db),
_ => None,
}
};
let call_dunder =
|op: RichCompareOperator, left: InstanceType<'db>, right: InstanceType<'db>| {
// TODO: How do we want to handle possibly unbound dunder methods?
match left.class.class_member(db, op.dunder()) {
Symbol::Type(class_member_dunder, Boundness::Bound) => class_member_dunder
.call(
db,
&CallArguments::positional([Type::Instance(left), Type::Instance(right)]),
)
.map(|outcome| outcome.return_type(db))
.ok(),
_ => None,
}
};
// The reflected dunder has priority if the right-hand side is a strict subclass of the left-hand side.
if left != right && right.is_subtype_of(db, left) {
@ -5989,7 +6129,8 @@ fn perform_membership_test_comparison<'db>(
db,
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
)
.return_type(db)
.map(|outcome| outcome.return_type(db))
.ok()
}
_ => {
// iteration-based membership test