Refactor CallOutcome to Result (#16161)

This commit is contained in:
Micha Reiser
2025-02-18 12:34:39 +00:00
committed by GitHub
parent 5cd0de3e4c
commit 4ed5db0d42
19 changed files with 717 additions and 737 deletions

View File

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

View File

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

View File

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

View File

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