mirror of https://github.com/astral-sh/ruff
[red-knot] Handle unions of callables better (#16716)
This cleans up how we handle calling unions of types. #16568 adding a three-level structure for callable signatures (`Signatures`, `CallableSignature`, and `Signature`) to handle unions and overloads. This PR updates the bindings side to mimic that structure. What used to be called `CallOutcome` is now `Bindings`, and represents the result of binding actual arguments against a possible union of callables. `CallableBinding` is the result of binding a single, possibly overloaded, callable type. `Binding` is the result of binding a single overload. While we're here, this also cleans up `CallError` greatly. It was previously extracting error information from the bindings and storing it in the error result. It is now a simple enum, carrying no data, that's used as a status code to talk about whether the overall binding was successful or not. We are now more consistent about walking the binding itself to get detailed information about _how_ the binding was unsucessful. --------- Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
3ccc8dbbf9
commit
23ccb52fa6
|
|
@ -363,7 +363,7 @@ reveal_type(X() + Y()) # revealed: int
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
a = NotBoolable()
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ def _(flag: bool):
|
|||
|
||||
a = NonCallable()
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(a()) # revealed: int | Unknown
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Call binding errors
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ def _(flag: bool):
|
|||
def f() -> int:
|
||||
return 1
|
||||
x = f() # error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
reveal_type(x) # revealed: int | Unknown
|
||||
reveal_type(x) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
## Multiple non-callable elements in a union
|
||||
|
|
@ -58,7 +58,7 @@ def _(flag: bool, flag2: bool):
|
|||
return 1
|
||||
# TODO we should mention all non-callable elements of the union
|
||||
# error: [call-non-callable] "Object of type `Literal[1]` is not callable"
|
||||
# revealed: int | Unknown
|
||||
# revealed: Unknown | int
|
||||
reveal_type(f())
|
||||
```
|
||||
|
||||
|
|
@ -148,3 +148,16 @@ def _(flag: bool):
|
|||
x = f(3)
|
||||
reveal_type(x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Union including a special-cased function
|
||||
|
||||
```py
|
||||
def _(flag: bool):
|
||||
if flag:
|
||||
f = str
|
||||
else:
|
||||
f = repr
|
||||
reveal_type(str("string")) # revealed: Literal["string"]
|
||||
reveal_type(repr("string")) # revealed: Literal["'string'"]
|
||||
reveal_type(f("string")) # revealed: Literal["string", "'string'"]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ It may also be more appropriate to use `unsupported-operator` as the error code.
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
class WithContains:
|
||||
def __contains__(self, item) -> NotBoolable:
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ element) of a chained comparison.
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, item) -> NotBoolable:
|
||||
|
|
|
|||
|
|
@ -355,7 +355,7 @@ def compute_chained_comparison():
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 5
|
||||
__bool__: int = 5
|
||||
|
||||
class Comparable:
|
||||
def __lt__(self, other) -> NotBoolable:
|
||||
|
|
@ -387,7 +387,7 @@ class A:
|
|||
return NotBoolable()
|
||||
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
__bool__: None = None
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
(A(),) == (A(),)
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ def _(flag: bool):
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
3 if NotBoolable() else 4
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ def _(flag: bool):
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ def _(target: int):
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
def _(target: int, flag: NotBoolable):
|
||||
y = 1
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
assert NotBoolable()
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ if NotBoolable():
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
__bool__: None = None
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
|
|
@ -133,9 +133,9 @@ if NotBoolable():
|
|||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__ = None if cond else 3
|
||||
__bool__: int | None = None if cond else 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; it incorrectly implements `__bool__`"
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
if NotBoolable():
|
||||
...
|
||||
```
|
||||
|
|
@ -145,7 +145,7 @@ def test(cond: bool):
|
|||
```py
|
||||
def test(cond: bool):
|
||||
class NotBoolable:
|
||||
__bool__ = None
|
||||
__bool__: None = None
|
||||
|
||||
a = 10 if cond else NotBoolable()
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ def _(flag: bool, flag2: bool):
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion] "Boolean conversion is unsupported for type `NotBoolable`; its `__bool__` method isn't callable"
|
||||
while NotBoolable():
|
||||
|
|
|
|||
|
|
@ -86,8 +86,7 @@ error: lint:not-iterable
|
|||
|
|
||||
35 | # error: [not-iterable]
|
||||
36 | for y in Iterable2():
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`)
|
||||
may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
|
||||
| ^^^^^^^^^^^ Object of type `Iterable2` may not be iterable because it may not have an `__iter__` method and its `__getitem__` method (with type `<bound method `__getitem__` of `Iterable2`> | <bound method `__getitem__` of `Iterable2`>`) may have an incorrect signature for the old-style iteration protocol (expected a signature at least as permissive as `def __getitem__(self, key: int): ...`)
|
||||
37 | reveal_type(y) # revealed: bytes | str | int
|
||||
|
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/binary/instances.m
|
|||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
2 | __bool__: int = 3
|
||||
3 |
|
||||
4 | a = NotBoolable()
|
||||
5 |
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
|
|||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
2 | __bool__: int = 3
|
||||
3 |
|
||||
4 | class WithContains:
|
||||
5 | def __contains__(self, item) -> NotBoolable:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/unary/not.md
|
|||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
2 | __bool__: int = 3
|
||||
3 |
|
||||
4 | # error: [unsupported-bool-conversion]
|
||||
5 | not NotBoolable()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/instanc
|
|||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 3
|
||||
2 | __bool__: int = 3
|
||||
3 |
|
||||
4 | class Comparable:
|
||||
5 | def __lt__(self, item) -> NotBoolable:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
|
|||
|
||||
```
|
||||
1 | class NotBoolable:
|
||||
2 | __bool__ = 5
|
||||
2 | __bool__: int = 5
|
||||
3 |
|
||||
4 | class Comparable:
|
||||
5 | def __lt__(self, other) -> NotBoolable:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ mdtest path: crates/red_knot_python_semantic/resources/mdtest/comparison/tuples.
|
|||
3 | return NotBoolable()
|
||||
4 |
|
||||
5 | class NotBoolable:
|
||||
6 | __bool__ = None
|
||||
6 | __bool__: None = None
|
||||
7 |
|
||||
8 | # error: [unsupported-bool-conversion]
|
||||
9 | (A(),) == (A(),)
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ reveal_type(not PossiblyUnboundBool())
|
|||
|
||||
```py
|
||||
class NotBoolable:
|
||||
__bool__ = 3
|
||||
__bool__: int = 3
|
||||
|
||||
# error: [unsupported-bool-conversion]
|
||||
not NotBoolable()
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use crate::{resolve_module, Db, KnownModule, Module, Program};
|
|||
|
||||
pub(crate) use implicit_globals::module_type_implicit_global_symbol;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum Boundness {
|
||||
Bound,
|
||||
PossiblyUnbound,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,197 +1,35 @@
|
|||
use super::context::InferContext;
|
||||
use super::{CallableSignature, Signature, Type};
|
||||
use crate::types::UnionType;
|
||||
use super::{CallableSignature, Signature, Signatures, Type};
|
||||
use crate::Db;
|
||||
|
||||
mod arguments;
|
||||
mod bind;
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{bind_call, CallBinding};
|
||||
pub(super) use bind::Bindings;
|
||||
|
||||
/// A successfully bound call where all arguments are valid.
|
||||
/// Wraps a [`Bindings`] for an unsuccessful call with information about why the call was
|
||||
/// unsuccessful.
|
||||
///
|
||||
/// It's guaranteed that the wrapped bindings have no errors.
|
||||
/// The bindings are boxed so that we do not pass around large `Err` variants on the stack.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallOutcome<'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> {
|
||||
/// 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 all_errors_not_callable = true;
|
||||
|
||||
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) => {
|
||||
all_errors_not_callable &= error.is_not_callable();
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(CallOutcome::Union(bindings.into()))
|
||||
} else if bindings.is_empty() && all_errors_not_callable {
|
||||
Err(CallError::NotCallable {
|
||||
not_callable_type: Type::Union(union),
|
||||
})
|
||||
} else {
|
||||
Err(CallError::Union(UnionCallError {
|
||||
errors: errors.into(),
|
||||
bindings: bindings.into(),
|
||||
called_type: Type::Union(union),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// The type returned by this call.
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Self::Single(binding) => binding.return_type(),
|
||||
Self::Union(bindings) => {
|
||||
UnionType::from_elements(db, bindings.iter().map(CallBinding::return_type))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn bindings(&self) -> &[CallBinding<'db>] {
|
||||
match self {
|
||||
Self::Single(binding) => std::slice::from_ref(binding),
|
||||
Self::Union(bindings) => bindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) struct CallError<'db>(pub(crate) CallErrorKind, pub(crate) Box<Bindings<'db>>);
|
||||
|
||||
/// The reason why calling a type failed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallError<'db> {
|
||||
/// The type is not callable.
|
||||
NotCallable {
|
||||
/// The type that can't be called.
|
||||
not_callable_type: Type<'db>,
|
||||
},
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum CallErrorKind {
|
||||
/// The type is not callable. For a union type, _none_ of the union elements are callable.
|
||||
NotCallable,
|
||||
|
||||
/// A call to a union failed because at least one variant
|
||||
/// can't be called with the given arguments.
|
||||
/// The type is not callable with the given arguments.
|
||||
///
|
||||
/// A union where all variants are not callable is represented as a `NotCallable` error.
|
||||
Union(UnionCallError<'db>),
|
||||
/// `BindingError` takes precedence over `PossiblyNotCallable`: for a union type, there might
|
||||
/// be some union elements that are not callable at all, but the call arguments are not
|
||||
/// compatible with at least one of the callable elements.
|
||||
BindingError,
|
||||
|
||||
/// The type has a `__call__` method but it isn't always bound.
|
||||
PossiblyUnboundDunderCall {
|
||||
called_type: Type<'db>,
|
||||
outcome: Box<CallOutcome<'db>>,
|
||||
},
|
||||
|
||||
/// The type is callable but not with the given arguments.
|
||||
BindingError { binding: CallBinding<'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 {
|
||||
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(UnionCallError {
|
||||
bindings, errors, ..
|
||||
}) => 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::NotCallable {
|
||||
not_callable_type, ..
|
||||
} => *not_callable_type,
|
||||
Self::Union(UnionCallError { called_type, .. })
|
||||
| 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) struct UnionCallError<'db> {
|
||||
/// The variants that can't be called with the given arguments.
|
||||
pub(super) errors: Box<[CallError<'db>]>,
|
||||
|
||||
/// The bindings for the callable variants (that have no binding errors).
|
||||
pub(super) bindings: Box<[CallBinding<'db>]>,
|
||||
|
||||
/// The union type that we tried calling.
|
||||
pub(super) called_type: Type<'db>,
|
||||
}
|
||||
|
||||
impl UnionCallError<'_> {
|
||||
/// Return `true` if this `UnionCallError` indicates that the union might not be callable at all.
|
||||
/// Otherwise, return `false`.
|
||||
///
|
||||
/// For example, the union type `Callable[[int], int] | None` may not be callable at all,
|
||||
/// because the `None` element in this union has no `__call__` method. Calling an object that
|
||||
/// inhabited this union type would lead to a `UnionCallError` that would indicate that the
|
||||
/// union might not be callable at all.
|
||||
///
|
||||
/// On the other hand, the union type `Callable[[int], int] | Callable[[str], str]` is always
|
||||
/// *callable*, but it would still lead to a `UnionCallError` if an inhabitant of this type was
|
||||
/// called with a single `int` argument passed in. That's because the second element in the
|
||||
/// union doesn't accept an `int` when it's called: it only accepts a `str`.
|
||||
pub(crate) fn indicates_type_possibly_not_callable(&self) -> bool {
|
||||
self.errors.iter().any(|error| match error {
|
||||
CallError::BindingError { .. } => false,
|
||||
CallError::NotCallable { .. } | CallError::PossiblyUnboundDunderCall { .. } => true,
|
||||
CallError::Union(union_error) => union_error.indicates_type_possibly_not_callable(),
|
||||
})
|
||||
}
|
||||
/// Not all of the elements of a union type are callable, but the call arguments are compatible
|
||||
/// with all of the callable elements.
|
||||
PossiblyNotCallable,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
|
@ -199,12 +37,12 @@ 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>),
|
||||
CallError(CallErrorKind, Box<Bindings<'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>),
|
||||
PossiblyUnbound(Box<Bindings<'db>>),
|
||||
|
||||
/// The dunder method with the specified name is missing.
|
||||
MethodNotAvailable,
|
||||
|
|
@ -213,9 +51,9 @@ pub(super) enum CallDunderError<'db> {
|
|||
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(call_outcome) => Some(call_outcome.return_type(db)),
|
||||
Self::MethodNotAvailable => None,
|
||||
Self::MethodNotAvailable | Self::CallError(CallErrorKind::NotCallable, _) => None,
|
||||
Self::CallError(_, bindings) => Some(bindings.return_type(db)),
|
||||
Self::PossiblyUnbound(bindings) => Some(bindings.return_type(db)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +63,7 @@ impl<'db> CallDunderError<'db> {
|
|||
}
|
||||
|
||||
impl<'db> From<CallError<'db>> for CallDunderError<'db> {
|
||||
fn from(error: CallError<'db>) -> Self {
|
||||
Self::Call(error)
|
||||
fn from(CallError(kind, bindings): CallError<'db>) -> Self {
|
||||
Self::CallError(kind, bindings)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,20 @@
|
|||
//! When analyzing a call site, we create _bindings_, which match and type-check the actual
|
||||
//! arguments against the parameters of the callable. Like with
|
||||
//! [signatures][crate::types::signatures], we have to handle the fact that the callable might be a
|
||||
//! union of types, each of which might contain multiple overloads.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::{
|
||||
Argument, CallArguments, CallError, CallOutcome, CallableSignature, InferContext, Signature,
|
||||
Type,
|
||||
Argument, CallArguments, CallError, CallErrorKind, CallableSignature, InferContext, Signature,
|
||||
Signatures, Type,
|
||||
};
|
||||
use crate::db::Db;
|
||||
use crate::types::diagnostic::{
|
||||
INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED,
|
||||
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
|
||||
CALL_NON_CALLABLE, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD,
|
||||
PARAMETER_ALREADY_ASSIGNED, TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
|
||||
};
|
||||
use crate::types::signatures::Parameter;
|
||||
use crate::types::{CallableType, UnionType};
|
||||
|
|
@ -13,162 +22,141 @@ use ruff_db::diagnostic::{OldSecondaryDiagnosticMessage, Span};
|
|||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// Bind a [`CallArguments`] against a [`CallableSignature`].
|
||||
/// Binding information for a possible union of callables. At a call site, the arguments must be
|
||||
/// compatible with _all_ of the types in the union for the call to be valid.
|
||||
///
|
||||
/// The returned [`CallBinding`] provides the return type of the call, the bound types for all
|
||||
/// parameters, and any errors resulting from binding the call.
|
||||
pub(crate) fn bind_call<'db>(
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
overloads: &CallableSignature<'db>,
|
||||
callable_ty: Type<'db>,
|
||||
) -> CallBinding<'db> {
|
||||
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
|
||||
// arguments are checked for arity first, and are only checked for type assignability against
|
||||
// the matching overloads. Make sure to implement that as part of separating call binding into
|
||||
// two phases.
|
||||
//
|
||||
// [1] https://github.com/python/typing/pull/1839
|
||||
let overloads = overloads
|
||||
.iter()
|
||||
.map(|signature| bind_overload(db, arguments, signature))
|
||||
.collect::<Vec<_>>()
|
||||
.into_boxed_slice();
|
||||
CallBinding {
|
||||
callable_ty,
|
||||
overloads,
|
||||
}
|
||||
/// It's guaranteed that the wrapped bindings have no errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Bindings<'db> {
|
||||
pub(crate) callable_type: Type<'db>,
|
||||
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a non-union
|
||||
/// type.
|
||||
elements: SmallVec<[CallableBinding<'db>; 1]>,
|
||||
}
|
||||
|
||||
fn bind_overload<'db>(
|
||||
db: &'db dyn Db,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
signature: &Signature<'db>,
|
||||
) -> OverloadBinding<'db> {
|
||||
let parameters = signature.parameters();
|
||||
// The type assigned to each parameter at this call site.
|
||||
let mut parameter_tys = vec![None; parameters.len()];
|
||||
let mut errors = vec![];
|
||||
let mut next_positional = 0;
|
||||
let mut first_excess_positional = None;
|
||||
let mut num_synthetic_args = 0;
|
||||
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
|
||||
if argument_index >= num_synthetic_args {
|
||||
// Adjust the argument index to skip synthetic args, which don't appear at the call
|
||||
// site and thus won't be in the Call node arguments list.
|
||||
Some(argument_index - num_synthetic_args)
|
||||
} else {
|
||||
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
|
||||
// entire Call node, since there's no argument node for this argument at the call site
|
||||
None
|
||||
}
|
||||
};
|
||||
for (argument_index, argument) in arguments.iter().enumerate() {
|
||||
let (index, parameter, argument_ty, positional) = match argument {
|
||||
Argument::Positional(ty) | Argument::Synthetic(ty) => {
|
||||
if matches!(argument, Argument::Synthetic(_)) {
|
||||
num_synthetic_args += 1;
|
||||
}
|
||||
let Some((index, parameter)) = parameters
|
||||
.get_positional(next_positional)
|
||||
.map(|param| (next_positional, param))
|
||||
.or_else(|| parameters.variadic())
|
||||
else {
|
||||
first_excess_positional.get_or_insert(argument_index);
|
||||
next_positional += 1;
|
||||
continue;
|
||||
};
|
||||
next_positional += 1;
|
||||
(index, parameter, ty, !parameter.is_variadic())
|
||||
}
|
||||
Argument::Keyword { name, ty } => {
|
||||
let Some((index, parameter)) = parameters
|
||||
.keyword_by_name(name)
|
||||
.or_else(|| parameters.keyword_variadic())
|
||||
else {
|
||||
errors.push(CallBindingError::UnknownArgument {
|
||||
argument_name: ast::name::Name::new(name),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
(index, parameter, ty, false)
|
||||
}
|
||||
|
||||
Argument::Variadic(_) | Argument::Keywords(_) => {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(expected_ty) = parameter.annotated_type() {
|
||||
if !argument_ty.is_assignable_to(db, expected_ty) {
|
||||
errors.push(CallBindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
expected_ty,
|
||||
provided_ty: *argument_ty,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
|
||||
if parameter.is_variadic() || parameter.is_keyword_variadic() {
|
||||
let union = UnionType::from_elements(db, [existing, *argument_ty]);
|
||||
parameter_tys[index].replace(union);
|
||||
} else {
|
||||
errors.push(CallBindingError::ParameterAlreadyAssigned {
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first_excess_argument_index) = first_excess_positional {
|
||||
errors.push(CallBindingError::TooManyPositionalArguments {
|
||||
first_excess_argument_index: get_argument_index(
|
||||
first_excess_argument_index,
|
||||
num_synthetic_args,
|
||||
),
|
||||
expected_positional_count: parameters.positional().count(),
|
||||
provided_positional_count: next_positional,
|
||||
});
|
||||
}
|
||||
let mut missing = vec![];
|
||||
for (index, bound_ty) in parameter_tys.iter().enumerate() {
|
||||
if bound_ty.is_none() {
|
||||
let param = ¶meters[index];
|
||||
if param.is_variadic() || param.is_keyword_variadic() || param.default_type().is_some()
|
||||
{
|
||||
// variadic/keywords and defaulted arguments are not required
|
||||
continue;
|
||||
}
|
||||
missing.push(ParameterContext::new(param, index, false));
|
||||
}
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
errors.push(CallBindingError::MissingArguments {
|
||||
parameters: ParameterContexts(missing),
|
||||
});
|
||||
}
|
||||
|
||||
OverloadBinding {
|
||||
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
|
||||
parameter_tys: parameter_tys
|
||||
impl<'db> Bindings<'db> {
|
||||
/// Binds the arguments of a call site against a signature.
|
||||
///
|
||||
/// The returned bindings provide the return type of the call, the bound types for all
|
||||
/// parameters, and any errors resulting from binding the call, all for each union element and
|
||||
/// overload (if any).
|
||||
pub(crate) fn bind(
|
||||
db: &'db dyn Db,
|
||||
signatures: &Signatures<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> Result<Self, CallError<'db>> {
|
||||
let elements: SmallVec<[CallableBinding<'db>; 1]> = signatures
|
||||
.into_iter()
|
||||
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
|
||||
.collect(),
|
||||
errors,
|
||||
.map(|signature| CallableBinding::bind(db, signature, arguments))
|
||||
.collect();
|
||||
|
||||
// In order of precedence:
|
||||
//
|
||||
// - If every union element is Ok, then the union is too.
|
||||
// - If any element has a BindingError, the union has a BindingError.
|
||||
// - If every element is NotCallable, then the union is also NotCallable.
|
||||
// - Otherwise, the elements are some mixture of Ok, NotCallable, and PossiblyNotCallable.
|
||||
// The union as a whole is PossiblyNotCallable.
|
||||
//
|
||||
// For example, the union type `Callable[[int], int] | None` may not be callable at all,
|
||||
// because the `None` element in this union has no `__call__` method.
|
||||
//
|
||||
// On the other hand, the union type `Callable[[int], int] | Callable[[str], str]` is
|
||||
// always *callable*, but it would produce a `BindingError` if an inhabitant of this type
|
||||
// was called with a single `int` argument passed in. That's because the second element in
|
||||
// the union doesn't accept an `int` when it's called: it only accepts a `str`.
|
||||
let mut all_ok = true;
|
||||
let mut any_binding_error = false;
|
||||
let mut all_not_callable = true;
|
||||
for binding in &elements {
|
||||
let result = binding.as_result();
|
||||
all_ok &= result.is_ok();
|
||||
any_binding_error |= matches!(result, Err(CallErrorKind::BindingError));
|
||||
all_not_callable &= matches!(result, Err(CallErrorKind::NotCallable));
|
||||
}
|
||||
|
||||
let bindings = Bindings {
|
||||
callable_type: signatures.callable_type,
|
||||
elements,
|
||||
};
|
||||
|
||||
if all_ok {
|
||||
Ok(bindings)
|
||||
} else if any_binding_error {
|
||||
Err(CallError(CallErrorKind::BindingError, Box::new(bindings)))
|
||||
} else if all_not_callable {
|
||||
Err(CallError(CallErrorKind::NotCallable, Box::new(bindings)))
|
||||
} else {
|
||||
Err(CallError(
|
||||
CallErrorKind::PossiblyNotCallable,
|
||||
Box::new(bindings),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_single(&self) -> bool {
|
||||
self.elements.len() == 1
|
||||
}
|
||||
|
||||
/// Returns the return type of the call. For successful calls, this is the actual return type.
|
||||
/// For calls with binding errors, this is a type that best approximates the return type. For
|
||||
/// types that are not callable, returns `Type::Unknown`.
|
||||
pub(crate) fn return_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
if let [binding] = self.elements.as_slice() {
|
||||
return binding.return_type();
|
||||
}
|
||||
UnionType::from_elements(db, self.into_iter().map(CallableBinding::return_type))
|
||||
}
|
||||
|
||||
/// Report diagnostics for all of the errors that occurred when trying to match actual
|
||||
/// arguments to formal parameters. If the callable is a union, or has multiple overloads, we
|
||||
/// report a single diagnostic if we couldn't match any union element or overload.
|
||||
/// TODO: Update this to add subdiagnostics about how we failed to match each union element and
|
||||
/// overload.
|
||||
pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
// If all union elements are not callable, report that the union as a whole is not
|
||||
// callable.
|
||||
if self.into_iter().all(|b| !b.is_callable()) {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
self.callable_type.display(context.db())
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: We currently only report errors for the first union element. Ideally, we'd report
|
||||
// an error saying that the union type can't be called, followed by subdiagnostics
|
||||
// explaining why.
|
||||
if let Some(first) = self.into_iter().find(|b| b.as_result().is_err()) {
|
||||
first.report_diagnostics(context, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a callable for the purposes of diagnostics.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CallableDescriptor<'a> {
|
||||
name: &'a str,
|
||||
kind: &'a str,
|
||||
impl<'a, 'db> IntoIterator for &'a Bindings<'db> {
|
||||
type Item = &'a CallableBinding<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, CallableBinding<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.elements.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Binding information for a call site.
|
||||
impl<'a, 'db> IntoIterator for &'a mut Bindings<'db> {
|
||||
type Item = &'a mut CallableBinding<'db>;
|
||||
type IntoIter = std::slice::IterMut<'a, CallableBinding<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.elements.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
/// Binding information for a single callable. If the callable is overloaded, there is a separate
|
||||
/// [`Binding`] for each overload.
|
||||
///
|
||||
/// For a successful binding, each argument is mapped to one of the callable's formal parameters.
|
||||
/// If the callable has multiple overloads, the first one that matches is used as the overall
|
||||
|
|
@ -183,23 +171,72 @@ pub(crate) struct CallableDescriptor<'a> {
|
|||
///
|
||||
/// [overloads]: https://github.com/python/typing/pull/1839
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct CallBinding<'db> {
|
||||
/// Type of the callable object (function, class...)
|
||||
callable_ty: Type<'db>,
|
||||
pub(crate) struct CallableBinding<'db> {
|
||||
pub(crate) callable_type: Type<'db>,
|
||||
pub(crate) signature_type: Type<'db>,
|
||||
pub(crate) dunder_call_is_possibly_unbound: bool,
|
||||
|
||||
overloads: Box<[OverloadBinding<'db>]>,
|
||||
/// The bindings of each overload of this callable. Will be empty if the type is not callable.
|
||||
///
|
||||
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a
|
||||
/// non-overloaded callable.
|
||||
overloads: SmallVec<[Binding<'db>; 1]>,
|
||||
}
|
||||
|
||||
impl<'db> CallBinding<'db> {
|
||||
pub(crate) fn into_outcome(self) -> Result<CallOutcome<'db>, CallError<'db>> {
|
||||
if self.has_binding_errors() {
|
||||
return Err(CallError::BindingError { binding: self });
|
||||
impl<'db> CallableBinding<'db> {
|
||||
/// Bind a [`CallArguments`] against a [`CallableSignature`].
|
||||
///
|
||||
/// The returned [`CallableBinding`] provides the return type of the call, the bound types for
|
||||
/// all parameters, and any errors resulting from binding the call.
|
||||
fn bind(
|
||||
db: &'db dyn Db,
|
||||
signature: &CallableSignature<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> Self {
|
||||
// If this callable is a bound method, prepend the self instance onto the arguments list
|
||||
// before checking.
|
||||
let arguments = if let Some(bound_type) = signature.bound_type {
|
||||
Cow::Owned(arguments.with_self(bound_type))
|
||||
} else {
|
||||
Cow::Borrowed(arguments)
|
||||
};
|
||||
|
||||
// TODO: This checks every overload. In the proposed more detailed call checking spec [1],
|
||||
// arguments are checked for arity first, and are only checked for type assignability against
|
||||
// the matching overloads. Make sure to implement that as part of separating call binding into
|
||||
// two phases.
|
||||
//
|
||||
// [1] https://github.com/python/typing/pull/1839
|
||||
let overloads = signature
|
||||
.into_iter()
|
||||
.map(|signature| Binding::bind(db, signature, arguments.as_ref()))
|
||||
.collect();
|
||||
CallableBinding {
|
||||
callable_type: signature.callable_type,
|
||||
signature_type: signature.signature_type,
|
||||
dunder_call_is_possibly_unbound: signature.dunder_call_is_possibly_unbound,
|
||||
overloads,
|
||||
}
|
||||
Ok(CallOutcome::Single(self))
|
||||
}
|
||||
|
||||
pub(crate) fn callable_type(&self) -> Type<'db> {
|
||||
self.callable_ty
|
||||
fn as_result(&self) -> Result<(), CallErrorKind> {
|
||||
if !self.is_callable() {
|
||||
return Err(CallErrorKind::NotCallable);
|
||||
}
|
||||
|
||||
if self.has_binding_errors() {
|
||||
return Err(CallErrorKind::BindingError);
|
||||
}
|
||||
|
||||
if self.dunder_call_is_possibly_unbound {
|
||||
return Err(CallErrorKind::PossiblyNotCallable);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_callable(&self) -> bool {
|
||||
!self.overloads.is_empty()
|
||||
}
|
||||
|
||||
/// Returns whether there were any errors binding this call site. If the callable has multiple
|
||||
|
|
@ -210,20 +247,20 @@ impl<'db> CallBinding<'db> {
|
|||
|
||||
/// Returns the overload that matched for this call binding. Returns `None` if none of the
|
||||
/// overloads matched.
|
||||
pub(crate) fn matching_overload(&self) -> Option<(usize, &OverloadBinding<'db>)> {
|
||||
pub(crate) fn matching_overload(&self) -> Option<(usize, &Binding<'db>)> {
|
||||
self.overloads
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, overload)| !overload.has_binding_errors())
|
||||
.find(|(_, overload)| overload.as_result().is_ok())
|
||||
}
|
||||
|
||||
/// Returns the overload that matched for this call binding. Returns `None` if none of the
|
||||
/// overloads matched.
|
||||
pub(crate) fn matching_overload_mut(&mut self) -> Option<(usize, &mut OverloadBinding<'db>)> {
|
||||
pub(crate) fn matching_overload_mut(&mut self) -> Option<(usize, &mut Binding<'db>)> {
|
||||
self.overloads
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.find(|(_, overload)| !overload.has_binding_errors())
|
||||
.find(|(_, overload)| overload.as_result().is_ok())
|
||||
}
|
||||
|
||||
/// Returns the return type of this call. For a valid call, this is the return type of the
|
||||
|
|
@ -235,53 +272,45 @@ impl<'db> CallBinding<'db> {
|
|||
if let Some((_, overload)) = self.matching_overload() {
|
||||
return overload.return_type();
|
||||
}
|
||||
if let [overload] = self.overloads.as_ref() {
|
||||
if let [overload] = self.overloads.as_slice() {
|
||||
return overload.return_type();
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
|
||||
fn callable_descriptor(&self, db: &'db dyn Db) -> Option<CallableDescriptor> {
|
||||
match self.callable_ty {
|
||||
Type::FunctionLiteral(function) => Some(CallableDescriptor {
|
||||
kind: "function",
|
||||
name: function.name(db),
|
||||
}),
|
||||
Type::ClassLiteral(class_type) => Some(CallableDescriptor {
|
||||
kind: "class",
|
||||
name: class_type.class().name(db),
|
||||
}),
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => Some(CallableDescriptor {
|
||||
kind: "bound method",
|
||||
name: bound_method.function(db).name(db),
|
||||
}),
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
Some(CallableDescriptor {
|
||||
kind: "method wrapper `__get__` of function",
|
||||
name: function.name(db),
|
||||
})
|
||||
}
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => Some(CallableDescriptor {
|
||||
kind: "wrapper descriptor",
|
||||
name: "FunctionType.__get__",
|
||||
}),
|
||||
_ => None,
|
||||
fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
if !self.is_callable() {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
self.callable_type.display(context.db()),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Report diagnostics for all of the errors that occurred when trying to match actual
|
||||
/// arguments to formal parameters. If the callable has multiple overloads, we report a single
|
||||
/// diagnostic that we couldn't match any overload.
|
||||
/// TODO: Update this to add subdiagnostics about how we failed to match each overload.
|
||||
pub(crate) fn report_diagnostics(&self, context: &InferContext<'db>, node: ast::AnyNodeRef) {
|
||||
let callable_descriptor = self.callable_descriptor(context.db());
|
||||
if self.dunder_call_is_possibly_unbound {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
node,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable (possibly unbound `__call__` method)",
|
||||
self.callable_type.display(context.db()),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let callable_description = CallableDescription::new(context.db(), self.callable_type);
|
||||
if self.overloads.len() > 1 {
|
||||
context.report_lint(
|
||||
&NO_MATCHING_OVERLOAD,
|
||||
node,
|
||||
format_args!(
|
||||
"No overload{} matches arguments",
|
||||
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -291,12 +320,13 @@ impl<'db> CallBinding<'db> {
|
|||
return;
|
||||
}
|
||||
|
||||
let callable_description = CallableDescription::new(context.db(), self.signature_type);
|
||||
for overload in &self.overloads {
|
||||
overload.report_diagnostics(
|
||||
context,
|
||||
node,
|
||||
self.callable_ty,
|
||||
callable_descriptor.as_ref(),
|
||||
self.signature_type,
|
||||
callable_description.as_ref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -304,7 +334,7 @@ impl<'db> CallBinding<'db> {
|
|||
|
||||
/// Binding information for one of the overloads of a callable.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct OverloadBinding<'db> {
|
||||
pub(crate) struct Binding<'db> {
|
||||
/// Return type of the call.
|
||||
return_ty: Type<'db>,
|
||||
|
||||
|
|
@ -312,10 +342,133 @@ pub(crate) struct OverloadBinding<'db> {
|
|||
parameter_tys: Box<[Type<'db>]>,
|
||||
|
||||
/// Call binding errors, if any.
|
||||
errors: Vec<CallBindingError<'db>>,
|
||||
errors: Vec<BindingError<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> OverloadBinding<'db> {
|
||||
impl<'db> Binding<'db> {
|
||||
fn bind(
|
||||
db: &'db dyn Db,
|
||||
signature: &Signature<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> Self {
|
||||
let parameters = signature.parameters();
|
||||
// The type assigned to each parameter at this call site.
|
||||
let mut parameter_tys = vec![None; parameters.len()];
|
||||
let mut errors = vec![];
|
||||
let mut next_positional = 0;
|
||||
let mut first_excess_positional = None;
|
||||
let mut num_synthetic_args = 0;
|
||||
let get_argument_index = |argument_index: usize, num_synthetic_args: usize| {
|
||||
if argument_index >= num_synthetic_args {
|
||||
// Adjust the argument index to skip synthetic args, which don't appear at the call
|
||||
// site and thus won't be in the Call node arguments list.
|
||||
Some(argument_index - num_synthetic_args)
|
||||
} else {
|
||||
// we are erroring on a synthetic argument, we'll just emit the diagnostic on the
|
||||
// entire Call node, since there's no argument node for this argument at the call site
|
||||
None
|
||||
}
|
||||
};
|
||||
for (argument_index, argument) in arguments.iter().enumerate() {
|
||||
let (index, parameter, argument_ty, positional) = match argument {
|
||||
Argument::Positional(ty) | Argument::Synthetic(ty) => {
|
||||
if matches!(argument, Argument::Synthetic(_)) {
|
||||
num_synthetic_args += 1;
|
||||
}
|
||||
let Some((index, parameter)) = parameters
|
||||
.get_positional(next_positional)
|
||||
.map(|param| (next_positional, param))
|
||||
.or_else(|| parameters.variadic())
|
||||
else {
|
||||
first_excess_positional.get_or_insert(argument_index);
|
||||
next_positional += 1;
|
||||
continue;
|
||||
};
|
||||
next_positional += 1;
|
||||
(index, parameter, ty, !parameter.is_variadic())
|
||||
}
|
||||
Argument::Keyword { name, ty } => {
|
||||
let Some((index, parameter)) = parameters
|
||||
.keyword_by_name(name)
|
||||
.or_else(|| parameters.keyword_variadic())
|
||||
else {
|
||||
errors.push(BindingError::UnknownArgument {
|
||||
argument_name: ast::name::Name::new(name),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
});
|
||||
continue;
|
||||
};
|
||||
(index, parameter, ty, false)
|
||||
}
|
||||
|
||||
Argument::Variadic(_) | Argument::Keywords(_) => {
|
||||
// TODO
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Some(expected_ty) = parameter.annotated_type() {
|
||||
if !argument_ty.is_assignable_to(db, expected_ty) {
|
||||
errors.push(BindingError::InvalidArgumentType {
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
expected_ty,
|
||||
provided_ty: *argument_ty,
|
||||
});
|
||||
}
|
||||
}
|
||||
if let Some(existing) = parameter_tys[index].replace(*argument_ty) {
|
||||
if parameter.is_variadic() || parameter.is_keyword_variadic() {
|
||||
let union = UnionType::from_elements(db, [existing, *argument_ty]);
|
||||
parameter_tys[index].replace(union);
|
||||
} else {
|
||||
errors.push(BindingError::ParameterAlreadyAssigned {
|
||||
argument_index: get_argument_index(argument_index, num_synthetic_args),
|
||||
parameter: ParameterContext::new(parameter, index, positional),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first_excess_argument_index) = first_excess_positional {
|
||||
errors.push(BindingError::TooManyPositionalArguments {
|
||||
first_excess_argument_index: get_argument_index(
|
||||
first_excess_argument_index,
|
||||
num_synthetic_args,
|
||||
),
|
||||
expected_positional_count: parameters.positional().count(),
|
||||
provided_positional_count: next_positional,
|
||||
});
|
||||
}
|
||||
let mut missing = vec![];
|
||||
for (index, bound_ty) in parameter_tys.iter().enumerate() {
|
||||
if bound_ty.is_none() {
|
||||
let param = ¶meters[index];
|
||||
if param.is_variadic()
|
||||
|| param.is_keyword_variadic()
|
||||
|| param.default_type().is_some()
|
||||
{
|
||||
// variadic/keywords and defaulted arguments are not required
|
||||
continue;
|
||||
}
|
||||
missing.push(ParameterContext::new(param, index, false));
|
||||
}
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
errors.push(BindingError::MissingArguments {
|
||||
parameters: ParameterContexts(missing),
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
return_ty: signature.return_ty.unwrap_or(Type::unknown()),
|
||||
parameter_tys: parameter_tys
|
||||
.into_iter()
|
||||
.map(|opt_ty| opt_ty.unwrap_or(Type::unknown()))
|
||||
.collect(),
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_return_type(&mut self, return_ty: Type<'db>) {
|
||||
self.return_ty = return_ty;
|
||||
}
|
||||
|
|
@ -333,15 +486,55 @@ impl<'db> OverloadBinding<'db> {
|
|||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
callable_ty: Type<'db>,
|
||||
callable_descriptor: Option<&CallableDescriptor>,
|
||||
callable_description: Option<&CallableDescription>,
|
||||
) {
|
||||
for error in &self.errors {
|
||||
error.report_diagnostic(context, node, callable_ty, callable_descriptor);
|
||||
error.report_diagnostic(context, node, callable_ty, callable_description);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_binding_errors(&self) -> bool {
|
||||
!self.errors.is_empty()
|
||||
fn as_result(&self) -> Result<(), CallErrorKind> {
|
||||
if !self.errors.is_empty() {
|
||||
return Err(CallErrorKind::BindingError);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a callable for the purposes of diagnostics.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct CallableDescription<'a> {
|
||||
name: &'a str,
|
||||
kind: &'a str,
|
||||
}
|
||||
|
||||
impl<'db> CallableDescription<'db> {
|
||||
fn new(db: &'db dyn Db, callable_type: Type<'db>) -> Option<CallableDescription<'db>> {
|
||||
match callable_type {
|
||||
Type::FunctionLiteral(function) => Some(CallableDescription {
|
||||
kind: "function",
|
||||
name: function.name(db),
|
||||
}),
|
||||
Type::ClassLiteral(class_type) => Some(CallableDescription {
|
||||
kind: "class",
|
||||
name: class_type.class().name(db),
|
||||
}),
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => Some(CallableDescription {
|
||||
kind: "bound method",
|
||||
name: bound_method.function(db).name(db),
|
||||
}),
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
Some(CallableDescription {
|
||||
kind: "method wrapper `__get__` of function",
|
||||
name: function.name(db),
|
||||
})
|
||||
}
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => Some(CallableDescription {
|
||||
kind: "wrapper descriptor",
|
||||
name: "FunctionType.__get__",
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -399,7 +592,7 @@ impl std::fmt::Display for ParameterContexts {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum CallBindingError<'db> {
|
||||
pub(crate) enum BindingError<'db> {
|
||||
/// The type of an argument is not assignable to the annotated type of its corresponding
|
||||
/// parameter.
|
||||
InvalidArgumentType {
|
||||
|
|
@ -428,7 +621,7 @@ pub(crate) enum CallBindingError<'db> {
|
|||
},
|
||||
}
|
||||
|
||||
impl<'db> CallBindingError<'db> {
|
||||
impl<'db> BindingError<'db> {
|
||||
fn parameter_span_from_index(
|
||||
db: &'db dyn Db,
|
||||
callable_ty: Type<'db>,
|
||||
|
|
@ -468,7 +661,7 @@ impl<'db> CallBindingError<'db> {
|
|||
context: &InferContext<'db>,
|
||||
node: ast::AnyNodeRef,
|
||||
callable_ty: Type<'db>,
|
||||
callable_descriptor: Option<&CallableDescriptor>,
|
||||
callable_description: Option<&CallableDescription>,
|
||||
) {
|
||||
match self {
|
||||
Self::InvalidArgumentType {
|
||||
|
|
@ -495,7 +688,7 @@ impl<'db> CallBindingError<'db> {
|
|||
format_args!(
|
||||
"Object of type `{provided_ty_display}` cannot be assigned to \
|
||||
parameter {parameter}{}; expected type `{expected_ty_display}`",
|
||||
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -516,7 +709,7 @@ impl<'db> CallBindingError<'db> {
|
|||
format_args!(
|
||||
"Too many positional arguments{}: expected \
|
||||
{expected_positional_count}, got {provided_positional_count}",
|
||||
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" to {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -532,7 +725,7 @@ impl<'db> CallBindingError<'db> {
|
|||
node,
|
||||
format_args!(
|
||||
"No argument{s} provided for required parameter{s} {parameters}{}",
|
||||
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -550,7 +743,7 @@ impl<'db> CallBindingError<'db> {
|
|||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Argument `{argument_name}` does not match any known parameter{}",
|
||||
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
|
|
@ -568,7 +761,7 @@ impl<'db> CallBindingError<'db> {
|
|||
Self::get_node(node, *argument_index),
|
||||
format_args!(
|
||||
"Multiple values provided for parameter {parameter}{}",
|
||||
if let Some(CallableDescriptor { kind, name }) = callable_descriptor {
|
||||
if let Some(CallableDescription { kind, name }) = callable_description {
|
||||
format!(" of {kind} `{name}`")
|
||||
} else {
|
||||
String::new()
|
||||
|
|
|
|||
|
|
@ -11,8 +11,8 @@ use crate::{
|
|||
Boundness, LookupError, LookupResult, Symbol, SymbolAndQualifiers,
|
||||
},
|
||||
types::{
|
||||
definition_expression_type, CallArguments, CallError, DynamicType, MetaclassCandidate,
|
||||
TupleType, UnionBuilder, UnionCallError, UnionType,
|
||||
definition_expression_type, CallArguments, CallError, CallErrorKind, DynamicType,
|
||||
MetaclassCandidate, TupleType, UnionBuilder, UnionType,
|
||||
},
|
||||
Db, KnownModule, Program,
|
||||
};
|
||||
|
|
@ -282,57 +282,21 @@ impl<'db> Class<'db> {
|
|||
let arguments = CallArguments::positional([name, bases, namespace]);
|
||||
|
||||
let return_ty_result = match metaclass.try_call(db, &arguments) {
|
||||
Ok(outcome) => Ok(outcome.return_type(db)),
|
||||
Ok(bindings) => Ok(bindings.return_type(db)),
|
||||
|
||||
Err(CallError::NotCallable { not_callable_type }) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::NotCallable(not_callable_type),
|
||||
}),
|
||||
|
||||
Err(CallError::Union(UnionCallError {
|
||||
called_type,
|
||||
errors,
|
||||
bindings,
|
||||
})) => {
|
||||
let mut partly_not_callable = false;
|
||||
|
||||
let return_ty = errors
|
||||
.iter()
|
||||
.fold(None, |acc, error| {
|
||||
let ty = error.return_type(db);
|
||||
|
||||
match (acc, ty) {
|
||||
(acc, None) => {
|
||||
partly_not_callable = true;
|
||||
acc
|
||||
}
|
||||
(None, Some(ty)) => Some(UnionBuilder::new(db).add(ty)),
|
||||
(Some(builder), Some(ty)) => Some(builder.add(ty)),
|
||||
}
|
||||
})
|
||||
.map(|mut builder| {
|
||||
for binding in bindings {
|
||||
builder = builder.add(binding.return_type());
|
||||
}
|
||||
|
||||
builder.build()
|
||||
});
|
||||
|
||||
if partly_not_callable {
|
||||
Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::PartlyNotCallable(called_type),
|
||||
})
|
||||
} else {
|
||||
Ok(return_ty.unwrap_or(Type::unknown()))
|
||||
}
|
||||
}
|
||||
|
||||
Err(CallError::PossiblyUnboundDunderCall { .. }) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::PartlyNotCallable(metaclass),
|
||||
Err(CallError(CallErrorKind::NotCallable, bindings)) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::NotCallable(bindings.callable_type),
|
||||
}),
|
||||
|
||||
// TODO we should also check for binding errors that would indicate the metaclass
|
||||
// does not accept the right arguments
|
||||
Err(CallError::BindingError { binding }) => Ok(binding.return_type()),
|
||||
Err(CallError(CallErrorKind::BindingError, bindings)) => {
|
||||
Ok(bindings.return_type(db))
|
||||
}
|
||||
|
||||
Err(CallError(CallErrorKind::PossiblyNotCallable, _)) => Err(MetaclassError {
|
||||
kind: MetaclassErrorKind::PartlyNotCallable(metaclass),
|
||||
}),
|
||||
};
|
||||
|
||||
return return_ty_result.map(|ty| ty.to_meta_type(db));
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ use crate::symbol::{
|
|||
module_type_implicit_global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
typing_extensions_symbol, Boundness, LookupError,
|
||||
};
|
||||
use crate::types::call::{Argument, CallArguments, UnionCallError};
|
||||
use crate::types::call::{Argument, CallArguments, CallError};
|
||||
use crate::types::diagnostic::{
|
||||
report_implicit_return_type, report_invalid_arguments_to_annotated,
|
||||
report_invalid_arguments_to_callable, report_invalid_assignment,
|
||||
|
|
@ -89,7 +89,6 @@ use crate::unpack::Unpack;
|
|||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::Db;
|
||||
|
||||
use super::call::CallError;
|
||||
use super::class_base::ClassBase;
|
||||
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
|
||||
use super::diagnostic::{
|
||||
|
|
@ -2789,9 +2788,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
Err(CallDunderError::PossiblyUnbound(outcome)) => {
|
||||
UnionType::from_elements(db, [outcome.return_type(db), binary_return_ty()])
|
||||
}
|
||||
Err(CallDunderError::Call(call_error)) => {
|
||||
Err(CallDunderError::CallError(_, bindings)) => {
|
||||
report_unsupported_augmented_op(&mut self.context);
|
||||
call_error.fallback_return_type(db)
|
||||
bindings.return_type(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3857,13 +3856,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
.unwrap_or_default();
|
||||
|
||||
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
|
||||
let call = function_type.try_call(self.db(), &call_arguments);
|
||||
|
||||
match call {
|
||||
Ok(outcome) => {
|
||||
for binding in outcome.bindings() {
|
||||
match function_type.try_call(self.db(), &call_arguments) {
|
||||
Ok(bindings) => {
|
||||
for binding in &bindings {
|
||||
let Some(known_function) = binding
|
||||
.callable_type()
|
||||
.callable_type
|
||||
.into_function_literal()
|
||||
.and_then(|function_type| function_type.known(self.db()))
|
||||
else {
|
||||
|
|
@ -3967,61 +3964,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
outcome.return_type(self.db())
|
||||
bindings.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_type } => {
|
||||
context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
call_expression,
|
||||
format_args!(
|
||||
"Object of type `{}` is not callable",
|
||||
not_callable_type.display(context.db())
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CallError::Union(UnionCallError { errors, .. }) => {
|
||||
if let Some(first) = IntoIterator::into_iter(errors).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,
|
||||
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
|
||||
Err(CallError(_, bindings)) => {
|
||||
bindings.report_diagnostics(&self.context, call_expression.into());
|
||||
bindings.return_type(self.db())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4669,7 +4617,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
let reflected_dunder = op.reflected_dunder();
|
||||
let rhs_reflected = right_class.member(self.db(), reflected_dunder).symbol;
|
||||
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible
|
||||
// CallOutcomes together
|
||||
// Bindings together
|
||||
if !rhs_reflected.is_unbound()
|
||||
&& rhs_reflected != left_class.member(self.db(), reflected_dunder).symbol
|
||||
{
|
||||
|
|
@ -5412,7 +5360,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
db,
|
||||
&CallArguments::positional([Type::Instance(right), Type::Instance(left)]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(db))
|
||||
.map(|bindings| bindings.return_type(db))
|
||||
.ok()
|
||||
}
|
||||
_ => {
|
||||
|
|
@ -5696,18 +5644,18 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
|
||||
return err.fallback_return_type(self.db());
|
||||
}
|
||||
Err(CallDunderError::Call(err)) => {
|
||||
Err(CallDunderError::CallError(_, bindings)) => {
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node,
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_type().display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node,
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
bindings.callable_type.display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
|
||||
return err.fallback_return_type(self.db());
|
||||
return bindings.return_type(self.db());
|
||||
}
|
||||
Err(CallDunderError::MethodNotAvailable) => {
|
||||
// try `__class_getitem__`
|
||||
|
|
@ -5741,21 +5689,24 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
);
|
||||
}
|
||||
|
||||
return ty
|
||||
.try_call(self.db(), &CallArguments::positional([value_ty, slice_ty]))
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.unwrap_or_else(|err| {
|
||||
match ty.try_call(
|
||||
self.db(),
|
||||
&CallArguments::positional([value_ty, slice_ty]),
|
||||
) {
|
||||
Ok(bindings) => return bindings.return_type(self.db()),
|
||||
Err(CallError(_, bindings)) => {
|
||||
self.context.report_lint(
|
||||
&CALL_NON_CALLABLE,
|
||||
value_node,
|
||||
format_args!(
|
||||
"Method `__class_getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_type().display(self.db()),
|
||||
bindings.callable_type.display(self.db()),
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
err.fallback_return_type(self.db())
|
||||
});
|
||||
return bindings.return_type(self.db());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6686,16 +6637,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
|||
);
|
||||
return Type::unknown();
|
||||
};
|
||||
function_type
|
||||
.into_callable_type(self.db())
|
||||
.unwrap_or_else(|| {
|
||||
self.context.report_lint(
|
||||
&INVALID_TYPE_FORM,
|
||||
arguments_slice,
|
||||
format_args!("Overloaded function literal is not yet supported"),
|
||||
);
|
||||
Type::unknown()
|
||||
})
|
||||
function_type.into_callable_type(self.db())
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -10,75 +10,188 @@
|
|||
//! argument types and return types. For each callable type in the union, the call expression's
|
||||
//! arguments must match _at least one_ overload.
|
||||
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use super::{definition_expression_type, DynamicType, Type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::types::todo_type;
|
||||
use crate::Db;
|
||||
use crate::{semantic_index::definition::Definition, types::todo_type};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
|
||||
/// The signature of a possible union of callables.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub(crate) struct Signatures<'db> {
|
||||
/// The type that is (hopefully) callable.
|
||||
pub(crate) callable_type: Type<'db>,
|
||||
/// The type we'll use for error messages referring to details of the called signature. For calls to functions this
|
||||
/// will be the same as `callable_type`; for other callable instances it may be a `__call__` method.
|
||||
pub(crate) signature_type: Type<'db>,
|
||||
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a non-union
|
||||
/// type.
|
||||
elements: SmallVec<[CallableSignature<'db>; 1]>,
|
||||
}
|
||||
|
||||
impl<'db> Signatures<'db> {
|
||||
pub(crate) fn not_callable(signature_type: Type<'db>) -> Self {
|
||||
Self {
|
||||
callable_type: signature_type,
|
||||
signature_type,
|
||||
elements: smallvec![CallableSignature::not_callable(signature_type)],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn single(signature: CallableSignature<'db>) -> Self {
|
||||
Self {
|
||||
callable_type: signature.callable_type,
|
||||
signature_type: signature.signature_type,
|
||||
elements: smallvec![signature],
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `Signatures` from an iterator of [`Signature`]s. Panics if the iterator is
|
||||
/// empty.
|
||||
pub(crate) fn from_union<I>(signature_type: Type<'db>, elements: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Signatures<'db>>,
|
||||
{
|
||||
let elements: SmallVec<_> = elements
|
||||
.into_iter()
|
||||
.flat_map(|s| s.elements.into_iter())
|
||||
.collect();
|
||||
assert!(!elements.is_empty());
|
||||
Self {
|
||||
callable_type: signature_type,
|
||||
signature_type,
|
||||
elements,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) {
|
||||
if self.callable_type == before {
|
||||
self.callable_type = after;
|
||||
}
|
||||
for signature in &mut self.elements {
|
||||
signature.replace_callable_type(before, after);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_dunder_call_is_possibly_unbound(&mut self) {
|
||||
for signature in &mut self.elements {
|
||||
signature.dunder_call_is_possibly_unbound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db> IntoIterator for &'a Signatures<'db> {
|
||||
type Item = &'a CallableSignature<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, CallableSignature<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.elements.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// The signature of a single callable. If the callable is overloaded, there is a separate
|
||||
/// [`Signature`] for each overload.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub enum CallableSignature<'db> {
|
||||
Single(Signature<'db>),
|
||||
Overloaded(Box<[Signature<'db>]>),
|
||||
pub(crate) struct CallableSignature<'db> {
|
||||
/// The type that is (hopefully) callable.
|
||||
pub(crate) callable_type: Type<'db>,
|
||||
|
||||
/// The type we'll use for error messages referring to details of the called signature. For
|
||||
/// calls to functions this will be the same as `callable_type`; for other callable instances
|
||||
/// it may be a `__call__` method.
|
||||
pub(crate) signature_type: Type<'db>,
|
||||
|
||||
/// If this is a callable object (i.e. called via a `__call__` method), the boundness of
|
||||
/// that call method.
|
||||
pub(crate) dunder_call_is_possibly_unbound: bool,
|
||||
|
||||
/// The type of the bound `self` or `cls` parameter if this signature is for a bound method.
|
||||
pub(crate) bound_type: Option<Type<'db>>,
|
||||
|
||||
/// The signatures of each overload of this callable. Will be empty if the type is not
|
||||
/// callable.
|
||||
///
|
||||
/// By using `SmallVec`, we avoid an extra heap allocation for the common case of a
|
||||
/// non-overloaded callable.
|
||||
overloads: SmallVec<[Signature<'db>; 1]>,
|
||||
}
|
||||
|
||||
impl<'db> CallableSignature<'db> {
|
||||
/// Creates a new `CallableSignature` from an non-empty iterator of [`Signature`]s.
|
||||
/// Panics if the iterator is empty.
|
||||
pub(crate) fn from_overloads<I>(overloads: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
I::IntoIter: Iterator<Item = Signature<'db>>,
|
||||
{
|
||||
let mut iter = overloads.into_iter();
|
||||
let first_overload = iter.next().expect("overloads should not be empty");
|
||||
let Some(second_overload) = iter.next() else {
|
||||
return CallableSignature::Single(first_overload);
|
||||
};
|
||||
let mut overloads = vec![first_overload, second_overload];
|
||||
overloads.extend(iter);
|
||||
CallableSignature::Overloaded(overloads.into())
|
||||
}
|
||||
|
||||
/// Returns the [`Signature`] if this is a non-overloaded callable, [None] otherwise.
|
||||
pub(crate) fn as_single(&self) -> Option<&Signature<'db>> {
|
||||
match self {
|
||||
CallableSignature::Single(signature) => Some(signature),
|
||||
CallableSignature::Overloaded(_) => None,
|
||||
pub(crate) fn not_callable(signature_type: Type<'db>) -> Self {
|
||||
Self {
|
||||
callable_type: signature_type,
|
||||
signature_type,
|
||||
dunder_call_is_possibly_unbound: false,
|
||||
bound_type: None,
|
||||
overloads: smallvec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> std::slice::Iter<Signature<'db>> {
|
||||
match self {
|
||||
CallableSignature::Single(signature) => std::slice::from_ref(signature).iter(),
|
||||
CallableSignature::Overloaded(signatures) => signatures.iter(),
|
||||
pub(crate) fn single(signature_type: Type<'db>, signature: Signature<'db>) -> Self {
|
||||
Self {
|
||||
callable_type: signature_type,
|
||||
signature_type,
|
||||
dunder_call_is_possibly_unbound: false,
|
||||
bound_type: None,
|
||||
overloads: smallvec![signature],
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `CallableSignature` from an iterator of [`Signature`]s. Returns a
|
||||
/// non-callable signature if the iterator is empty.
|
||||
pub(crate) fn from_overloads<I>(signature_type: Type<'db>, overloads: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = Signature<'db>>,
|
||||
{
|
||||
Self {
|
||||
callable_type: signature_type,
|
||||
signature_type,
|
||||
dunder_call_is_possibly_unbound: false,
|
||||
bound_type: None,
|
||||
overloads: overloads.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a signature for a dynamic callable
|
||||
pub(crate) fn dynamic(ty: Type<'db>) -> Self {
|
||||
pub(crate) fn dynamic(signature_type: Type<'db>) -> Self {
|
||||
let signature = Signature {
|
||||
parameters: Parameters::gradual_form(),
|
||||
return_ty: Some(ty),
|
||||
return_ty: Some(signature_type),
|
||||
};
|
||||
signature.into()
|
||||
Self::single(signature_type, signature)
|
||||
}
|
||||
|
||||
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
|
||||
#[allow(unused_variables)] // 'reason' only unused in debug builds
|
||||
pub(crate) fn todo(reason: &'static str) -> Self {
|
||||
let signature_type = todo_type!(reason);
|
||||
let signature = Signature {
|
||||
parameters: Parameters::todo(),
|
||||
return_ty: Some(todo_type!(reason)),
|
||||
return_ty: Some(signature_type),
|
||||
};
|
||||
signature.into()
|
||||
Self::single(signature_type, signature)
|
||||
}
|
||||
|
||||
pub(crate) fn with_bound_type(mut self, bound_type: Type<'db>) -> Self {
|
||||
self.bound_type = Some(bound_type);
|
||||
self
|
||||
}
|
||||
|
||||
fn replace_callable_type(&mut self, before: Type<'db>, after: Type<'db>) {
|
||||
if self.callable_type == before {
|
||||
self.callable_type = after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Signature<'db>> for CallableSignature<'db> {
|
||||
fn from(signature: Signature<'db>) -> Self {
|
||||
CallableSignature::Single(signature)
|
||||
impl<'a, 'db> IntoIterator for &'a CallableSignature<'db> {
|
||||
type Item = &'a Signature<'db>;
|
||||
type IntoIter = std::slice::Iter<'a, Signature<'db>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.overloads.iter()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,11 +220,20 @@ impl<'db> Signature<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
|
||||
#[allow(unused_variables)] // 'reason' only unused in debug builds
|
||||
pub(crate) fn todo(reason: &'static str) -> Self {
|
||||
Signature {
|
||||
parameters: Parameters::todo(),
|
||||
return_ty: Some(todo_type!(reason)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a typed signature from a function definition.
|
||||
pub(super) fn from_function(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
function_node: &'db ast::StmtFunctionDef,
|
||||
function_node: &ast::StmtFunctionDef,
|
||||
) -> Self {
|
||||
let return_ty = function_node.returns.as_ref().map(|returns| {
|
||||
if function_node.is_async {
|
||||
|
|
@ -249,7 +371,7 @@ impl<'db> Parameters<'db> {
|
|||
fn from_parameters(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
parameters: &'db ast::Parameters,
|
||||
parameters: &ast::Parameters,
|
||||
) -> Self {
|
||||
let ast::Parameters {
|
||||
posonlyargs,
|
||||
|
|
@ -413,7 +535,7 @@ impl<'db> Parameter<'db> {
|
|||
fn from_node_and_kind(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
parameter: &'db ast::Parameter,
|
||||
parameter: &ast::Parameter,
|
||||
kind: ParameterKind<'db>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
|
@ -792,7 +914,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
|
||||
let expected_sig = func.internal_signature(&db).into();
|
||||
let expected_sig = func.internal_signature(&db);
|
||||
|
||||
// With no decorators, internal and external signature are the same
|
||||
assert_eq!(func.signature(&db), &expected_sig);
|
||||
|
|
@ -813,7 +935,7 @@ mod tests {
|
|||
.unwrap();
|
||||
let func = get_function_f(&db, "/src/a.py");
|
||||
|
||||
let expected_sig = CallableSignature::todo("return type of decorated function");
|
||||
let expected_sig = Signature::todo("return type of decorated function");
|
||||
|
||||
// With no decorators, internal and external signature are the same
|
||||
assert_eq!(func.signature(&db), &expected_sig);
|
||||
|
|
|
|||
Loading…
Reference in New Issue