Compare commits
5 Commits
david/sqla
...
micha/call
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20cfc116db | ||
|
|
44eaf112f2 | ||
|
|
302b64b139 | ||
|
|
34ef941f0c | ||
|
|
daf19cc40a |
@@ -5,7 +5,6 @@ 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 type_ordering::union_elements_ordering;
|
||||
@@ -36,7 +35,7 @@ use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symb
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::call::{
|
||||
bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind,
|
||||
bind_call, CallArguments, CallBinding, CallDunderLenOutcome, CallDunderOutcome, CallOutcome,
|
||||
};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::INVALID_TYPE_FORM;
|
||||
@@ -1892,21 +1891,40 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the type of `len()` on a type if it is known more precisely than `int`,
|
||||
/// Return the type of calling the `__len__` method on a type.
|
||||
fn __len__(&self, db: &'db dyn Db) -> CallDunderLenOutcome<'db> {
|
||||
let statically_known_len = match self {
|
||||
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
|
||||
Type::StringLiteral(string) => Some(string.python_len(db)),
|
||||
Type::Tuple(tuple) => Some(tuple.len(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(len) = statically_known_len {
|
||||
return CallDunderLenOutcome::StaticallyKnown(len);
|
||||
}
|
||||
|
||||
match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) {
|
||||
CallDunderOutcome::MethodNotAvailable => CallDunderLenOutcome::MethodNotAvailable,
|
||||
CallDunderOutcome::Call(outcome) => CallDunderLenOutcome::Call(outcome),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the type of `len(type)` on a type if it is known more precisely than `int`,
|
||||
/// or `None` otherwise.
|
||||
///
|
||||
/// In the second case, the return type of `len()` in `typeshed` (`int`)
|
||||
/// is used as a fallback.
|
||||
#[must_use]
|
||||
fn len(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
|
||||
match ty {
|
||||
// TODO: Emit diagnostic for non-integers and negative integers
|
||||
Type::IntLiteral(value) => (value >= 0).then_some(ty),
|
||||
Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())),
|
||||
Type::Union(union) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in union.elements(db) {
|
||||
builder = builder.add(non_negative_int_literal(db, *element)?);
|
||||
for &element in union.elements(db) {
|
||||
builder = builder.add(non_negative_int_literal(db, element)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
@@ -1914,70 +1932,17 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
let usize_len = match self {
|
||||
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
|
||||
Type::StringLiteral(string) => Some(string.python_len(db)),
|
||||
Type::Tuple(tuple) => Some(tuple.len(db)),
|
||||
_ => None,
|
||||
};
|
||||
let len = self.__len__(db).return_type(db)?;
|
||||
|
||||
if let Some(usize_len) = usize_len {
|
||||
return usize_len.try_into().ok().map(Type::IntLiteral);
|
||||
}
|
||||
|
||||
let return_ty = match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) {
|
||||
// TODO: emit a diagnostic
|
||||
CallDunderResult::MethodNotAvailable => return None,
|
||||
|
||||
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
|
||||
outcome.return_type(db)?
|
||||
}
|
||||
};
|
||||
|
||||
non_negative_int_literal(db, return_ty)
|
||||
non_negative_int_literal(db, len)
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an object of this type.
|
||||
#[must_use]
|
||||
fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> {
|
||||
match self {
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), Some(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()
|
||||
@@ -2052,14 +2017,6 @@ impl<'db> Type<'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.
|
||||
@@ -2101,23 +2058,26 @@ impl<'db> Type<'db> {
|
||||
|
||||
instance_ty @ Type::Instance(_) => {
|
||||
match instance_ty.call_dunder(db, "__call__", &arguments.with_self(instance_ty)) {
|
||||
CallDunderResult::CallOutcome(CallOutcome::NotCallable { .. }) => {
|
||||
CallDunderOutcome::Call(CallOutcome::NotCallable { .. }) => {
|
||||
// Turn "`<type of illegal '__call__'>` not callable" into
|
||||
// "`X` not callable"
|
||||
CallOutcome::NotCallable {
|
||||
not_callable_ty: self,
|
||||
}
|
||||
}
|
||||
CallDunderResult::CallOutcome(outcome) => outcome,
|
||||
CallDunderResult::PossiblyUnbound(call_outcome) => {
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
CallDunderResult::MethodNotAvailable => {
|
||||
// Turn "possibly unbound object of type `Literal['__call__']`"
|
||||
// into "`X` not callable (possibly unbound `__call__` method)"
|
||||
CallDunderOutcome::Call(CallOutcome::PossiblyUnboundDunderCall {
|
||||
called_ty: _,
|
||||
call_outcome,
|
||||
}) => CallOutcome::PossiblyUnboundDunderCall {
|
||||
called_ty: self,
|
||||
call_outcome,
|
||||
},
|
||||
|
||||
CallDunderOutcome::Call(outcome) => outcome,
|
||||
|
||||
CallDunderOutcome::MethodNotAvailable => {
|
||||
// Turn "`X.__call__` unbound" into "`X` not callable"
|
||||
CallOutcome::NotCallable {
|
||||
not_callable_ty: self,
|
||||
@@ -2151,7 +2111,6 @@ 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,
|
||||
@@ -2197,15 +2156,18 @@ impl<'db> Type<'db> {
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> CallDunderResult<'db> {
|
||||
) -> CallDunderOutcome<'db> {
|
||||
match self.to_meta_type(db).member(db, name) {
|
||||
Symbol::Type(callable_ty, Boundness::Bound) => {
|
||||
CallDunderResult::CallOutcome(callable_ty.call(db, arguments))
|
||||
CallDunderOutcome::Call(callable_ty.call(db, arguments))
|
||||
}
|
||||
Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => {
|
||||
CallDunderResult::PossiblyUnbound(callable_ty.call(db, arguments))
|
||||
CallDunderOutcome::Call(CallOutcome::PossiblyUnboundDunderCall {
|
||||
call_outcome: Box::new(callable_ty.call(db, arguments)),
|
||||
called_ty: callable_ty,
|
||||
})
|
||||
}
|
||||
Symbol::Unbound => CallDunderResult::MethodNotAvailable,
|
||||
Symbol::Unbound => CallDunderOutcome::MethodNotAvailable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2227,8 +2189,7 @@ 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) => {
|
||||
CallDunderOutcome::Call(ref call_outcome) => {
|
||||
let Some(iterator_ty) = call_outcome.return_type(db) else {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
@@ -2239,7 +2200,7 @@ impl<'db> Type<'db> {
|
||||
.call_dunder(db, "__next__", &CallArguments::positional([iterator_ty]))
|
||||
.return_type(db)
|
||||
{
|
||||
if matches!(dunder_iter_result, CallDunderResult::PossiblyUnbound(..)) {
|
||||
if call_outcome.is_possibly_unbound() {
|
||||
IterationOutcome::PossiblyUnboundDunderIter {
|
||||
iterable_ty: self,
|
||||
element_ty,
|
||||
@@ -2253,7 +2214,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
};
|
||||
}
|
||||
CallDunderResult::MethodNotAvailable => {}
|
||||
CallDunderOutcome::MethodNotAvailable => {}
|
||||
}
|
||||
|
||||
// Although it's not considered great practice,
|
||||
@@ -4074,10 +4035,7 @@ impl<'db> Class<'db> {
|
||||
|
||||
// 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()),
|
||||
CallOutcome::Callable { binding } => Ok(binding.return_type()),
|
||||
};
|
||||
|
||||
return return_ty_result.map(|ty| ty.to_meta_type(db));
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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::diagnostic::CALL_NON_CALLABLE;
|
||||
use super::{Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
mod arguments;
|
||||
@@ -12,23 +10,12 @@ mod bind;
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{bind_call, CallBinding};
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum StaticAssertionErrorKind<'db> {
|
||||
ArgumentIsFalse,
|
||||
ArgumentIsFalsy(Type<'db>),
|
||||
ArgumentTruthinessIsAmbiguous(Type<'db>),
|
||||
CustomError(&'db str),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum CallOutcome<'db> {
|
||||
pub(crate) enum CallOutcome<'db> {
|
||||
Callable {
|
||||
binding: CallBinding<'db>,
|
||||
},
|
||||
RevealType {
|
||||
binding: CallBinding<'db>,
|
||||
revealed_ty: Type<'db>,
|
||||
},
|
||||
NotCallable {
|
||||
not_callable_ty: Type<'db>,
|
||||
},
|
||||
@@ -40,14 +27,6 @@ pub(super) enum CallOutcome<'db> {
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'db> CallOutcome<'db> {
|
||||
@@ -61,14 +40,6 @@ impl<'db> CallOutcome<'db> {
|
||||
CallOutcome::NotCallable { not_callable_ty }
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `CallOutcome::Union` with given wrapped outcomes.
|
||||
pub(super) fn union(
|
||||
called_ty: Type<'db>,
|
||||
@@ -80,22 +51,14 @@ impl<'db> CallOutcome<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
pub(super) fn is_possibly_unbound(&self) -> bool {
|
||||
matches!(self, Self::PossiblyUnboundDunderCall { .. })
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
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,
|
||||
@@ -114,11 +77,6 @@ impl<'db> CallOutcome<'db> {
|
||||
})
|
||||
.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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,22 +162,12 @@ impl<'db> CallOutcome<'db> {
|
||||
// only non-callable diagnostics in the union case, which is inconsistent.
|
||||
match self {
|
||||
Self::Callable { binding } => {
|
||||
// TODO: Move this out of the `CallOutcome` and into `TypeInferenceBuilder`?
|
||||
// This check is required everywhere where we call `return_type_result`
|
||||
// from the TypeInferenceBuilder.
|
||||
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(),
|
||||
@@ -239,24 +187,12 @@ impl<'db> CallOutcome<'db> {
|
||||
} => {
|
||||
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);
|
||||
@@ -280,88 +216,27 @@ impl<'db> CallOutcome<'db> {
|
||||
}),
|
||||
}
|
||||
}
|
||||
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>),
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub(super) enum CallDunderOutcome<'db> {
|
||||
Call(CallOutcome<'db>),
|
||||
MethodNotAvailable,
|
||||
}
|
||||
|
||||
impl<'db> CallDunderResult<'db> {
|
||||
impl<'db> CallDunderOutcome<'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::Call(outcome) => {
|
||||
if outcome.is_possibly_unbound() {
|
||||
None
|
||||
} else {
|
||||
outcome.return_type(db)
|
||||
}
|
||||
}
|
||||
Self::MethodNotAvailable => None,
|
||||
}
|
||||
}
|
||||
@@ -421,3 +296,29 @@ impl<'db> NotCallableError<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub(super) enum CallDunderLenOutcome<'db> {
|
||||
/// The length is statically known.
|
||||
StaticallyKnown(usize),
|
||||
|
||||
/// The length is determined by calling `__len__`.
|
||||
Call(CallOutcome<'db>),
|
||||
|
||||
/// The object doesn't have a `__len__` method and, thus, doesn't implement sized.
|
||||
MethodNotAvailable,
|
||||
}
|
||||
|
||||
impl<'db> CallDunderLenOutcome<'db> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
CallDunderLenOutcome::StaticallyKnown(len) => {
|
||||
// TODO: Fall back to `int` if value is too large?
|
||||
i64::try_from(*len).ok().map(Type::IntLiteral)
|
||||
}
|
||||
CallDunderLenOutcome::Call(call_outcome) => call_outcome.return_type(db),
|
||||
CallDunderLenOutcome::MethodNotAvailable => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
@@ -49,7 +50,9 @@ use crate::semantic_index::semantic_index;
|
||||
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::stdlib::builtins_module_scope;
|
||||
use crate::types::call::{Argument, CallArguments};
|
||||
use crate::types::call::{
|
||||
Argument, CallArguments, CallDunderLenOutcome, CallDunderOutcome, CallOutcome,
|
||||
};
|
||||
use crate::types::diagnostic::{
|
||||
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
|
||||
@@ -58,18 +61,19 @@ use crate::types::diagnostic::{
|
||||
INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_CONTEXT_MANAGER,
|
||||
INVALID_DECLARATION, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
|
||||
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
|
||||
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
|
||||
UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
};
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
|
||||
todo_type, typing_extensions_symbol, 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, typing_extensions_symbol, 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};
|
||||
@@ -3245,9 +3249,117 @@ 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_outcome = self.check_call(function_type, &call_arguments, call_expression);
|
||||
|
||||
call_outcome.unwrap_with_diagnostic(&self.context, call_expression.into())
|
||||
}
|
||||
|
||||
fn check_call(
|
||||
&self,
|
||||
callee: Type<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
call_expression: &ast::ExprCall,
|
||||
) -> CallOutcome<'db> {
|
||||
let call = callee.call(self.db(), arguments);
|
||||
|
||||
let Type::FunctionLiteral(function_type) = callee else {
|
||||
return call;
|
||||
};
|
||||
|
||||
let CallOutcome::Callable { binding } = &call else {
|
||||
return call;
|
||||
};
|
||||
|
||||
let Some(known) = function_type.known(self.db()) else {
|
||||
return call;
|
||||
};
|
||||
|
||||
match known {
|
||||
KnownFunction::RevealType => {
|
||||
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
self.context.report_diagnostic(
|
||||
call_expression.into(),
|
||||
DiagnosticId::RevealedType,
|
||||
Severity::Info,
|
||||
format_args!("Revealed type is `{}`", revealed_ty.display(self.db())),
|
||||
);
|
||||
}
|
||||
|
||||
KnownFunction::AssertType => {
|
||||
let [actual_ty, asserted_ty] = binding.parameter_types() else {
|
||||
return call;
|
||||
};
|
||||
|
||||
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 => {
|
||||
let Some((parameter_ty, message)) = binding.two_parameter_types() else {
|
||||
return call;
|
||||
};
|
||||
|
||||
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())
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
KnownFunction::Len => {
|
||||
let Some(sized) = binding.one_parameter_type() else {
|
||||
return call;
|
||||
};
|
||||
|
||||
if let CallDunderLenOutcome::Call(_) = sized.__len__(self.db()) {
|
||||
// TODO: Assert that the argument implements the `Sized` protocol (correctly).
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
call
|
||||
}
|
||||
|
||||
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
|
||||
@@ -3567,8 +3679,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
};
|
||||
|
||||
if let CallDunderResult::CallOutcome(call)
|
||||
| CallDunderResult::PossiblyUnbound(call) = operand_type.call_dunder(
|
||||
if let CallDunderOutcome::Call(call) = operand_type.call_dunder(
|
||||
self.db(),
|
||||
unary_dunder_method,
|
||||
&CallArguments::positional([operand_type]),
|
||||
|
||||
Reference in New Issue
Block a user