[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:
Douglas Creager 2025-03-17 10:35:52 -04:00 committed by GitHub
parent 3ccc8dbbf9
commit 23ccb52fa6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1301 additions and 1251 deletions

View File

@ -363,7 +363,7 @@ reveal_type(X() + Y()) # revealed: int
```py
class NotBoolable:
__bool__ = 3
__bool__: int = 3
a = NotBoolable()

View File

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

View File

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

View File

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

View File

@ -355,7 +355,7 @@ element) of a chained comparison.
```py
class NotBoolable:
__bool__ = 3
__bool__: int = 3
class Comparable:
def __lt__(self, item) -> NotBoolable:

View File

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

View File

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

View File

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

View File

@ -48,7 +48,7 @@ def _(target: int):
```py
class NotBoolable:
__bool__ = 3
__bool__: int = 3
def _(target: int, flag: NotBoolable):
y = 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -210,7 +210,7 @@ reveal_type(not PossiblyUnboundBool())
```py
class NotBoolable:
__bool__ = 3
__bool__: int = 3
# error: [unsupported-bool-conversion]
not NotBoolable()

View File

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

View File

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

View File

@ -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 = &parameters[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 = &parameters[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()

View File

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

View File

@ -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())
}
},

View File

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