[red-knot] Method calls and the descriptor protocol (#16121)
## Summary
This PR achieves the following:
* Add support for checking method calls, and inferring return types from
method calls. For example:
```py
reveal_type("abcde".find("abc")) # revealed: int
reveal_type("foo".encode(encoding="utf-8")) # revealed: bytes
"abcde".find(123) # error: [invalid-argument-type]
class C:
def f(self) -> int:
pass
reveal_type(C.f) # revealed: <function `f`>
reveal_type(C().f) # revealed: <bound method: `f` of `C`>
C.f() # error: [missing-argument]
reveal_type(C().f()) # revealed: int
```
* Implement the descriptor protocol, i.e. properly call the `__get__`
method when a descriptor object is accessed through a class object or an
instance of a class. For example:
```py
from typing import Literal
class Ten:
def __get__(self, instance: object, owner: type | None = None) ->
Literal[10]:
return 10
class C:
ten: Ten = Ten()
reveal_type(C.ten) # revealed: Literal[10]
reveal_type(C().ten) # revealed: Literal[10]
```
* Add support for member lookup on intersection types.
* Support type inference for `inspect.getattr_static(obj, attr)` calls.
This was mostly used as a debugging tool during development, but seems
more generally useful. It can be used to bypass the descriptor protocol.
For the example above:
```py
from inspect import getattr_static
reveal_type(getattr_static(C, "ten")) # revealed: Ten
```
* Add a new `Type::Callable(…)` variant with the following sub-variants:
* `Type::Callable(CallableType::BoundMethod(…))` — represents bound
method objects, e.g. `C().f` above
* `Type::Callable(CallableType::MethodWrapperDunderGet(…))` — represents
`f.__get__` where `f` is a function
* `Type::Callable(WrapperDescriptorDunderGet)` — represents
`FunctionType.__get__`
* Add new known classes:
* `types.MethodType`
* `types.MethodWrapperType`
* `types.WrapperDescriptorType`
* `builtins.range`
## Performance analysis
On this branch, we do more work. We need to do more call checking, since
we now check all method calls. We also need to do ~twice as many member
lookups, because we need to check if a `__get__` attribute exists on
accessed members.
A brief analysis on `tomllib` shows that we now call `Type::call` 1780
times, compared to 612 calls before.
## Limitations
* Data descriptors are not yet supported, i.e. we do not infer correct
types for descriptor attribute accesses in `Store` context and do not
check writes to descriptor attributes. I felt like this was something
that could be split out as a follow-up without risking a major
architectural change.
* We currently distinguish between `Type::member` (with descriptor
protocol) and `Type::static_member` (without descriptor protocol). The
former corresponds to `obj.attr`, the latter corresponds to
`getattr_static(obj, "attr")`. However, to model some details correctly,
we would also need to distinguish between a static member lookup *with*
and *without* instance variables. The lookup without instance variables
corresponds to `find_name_in_mro`
[here](https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance).
We currently approximate both using `member_static`, which leads to two
open TODOs. Changing this would be a larger refactoring of
`Type::own_instance_member`, so I chose to leave it out of this PR.
## Test Plan
* New `call/methods.md` test suite for method calls
* New tests in `descriptor_protocol.md`
* New `call/getattr_static.md` test suite for `inspect.getattr_static`
* Various updated tests
This commit is contained in:
@@ -109,6 +109,7 @@ pub enum KnownModule {
|
||||
#[allow(dead_code)]
|
||||
Abc, // currently only used in tests
|
||||
Collections,
|
||||
Inspect,
|
||||
KnotExtensions,
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ impl KnownModule {
|
||||
Self::Sys => "sys",
|
||||
Self::Abc => "abc",
|
||||
Self::Collections => "collections",
|
||||
Self::Inspect => "inspect",
|
||||
Self::KnotExtensions => "knot_extensions",
|
||||
}
|
||||
}
|
||||
@@ -149,6 +151,7 @@ impl KnownModule {
|
||||
"sys" => Some(Self::Sys),
|
||||
"abc" => Some(Self::Abc),
|
||||
"collections" => Some(Self::Collections),
|
||||
"inspect" => Some(Self::Inspect),
|
||||
"knot_extensions" => Some(Self::KnotExtensions),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,11 @@ use super::Type;
|
||||
pub(crate) struct CallArguments<'a, 'db>(Vec<Argument<'a, 'db>>);
|
||||
|
||||
impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
/// Create a [`CallArguments`] with no arguments.
|
||||
pub(crate) fn none() -> Self {
|
||||
Self(Vec::new())
|
||||
}
|
||||
|
||||
/// Create a [`CallArguments`] from an iterator over non-variadic positional argument types.
|
||||
pub(crate) fn positional(positional_tys: impl IntoIterator<Item = Type<'db>>) -> Self {
|
||||
positional_tys
|
||||
@@ -29,6 +34,11 @@ impl<'a, 'db> CallArguments<'a, 'db> {
|
||||
pub(crate) fn first_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.first().map(Argument::ty)
|
||||
}
|
||||
|
||||
// TODO this should be eliminated in favor of [`bind_call`]
|
||||
pub(crate) fn second_argument(&self) -> Option<Type<'db>> {
|
||||
self.0.get(1).map(Argument::ty)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, 'a, 'b> IntoIterator for &'b CallArguments<'a, 'db> {
|
||||
|
||||
@@ -193,6 +193,13 @@ impl<'db> CallBinding<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn three_parameter_types(&self) -> Option<(Type<'db>, Type<'db>, Type<'db>)> {
|
||||
match self.parameter_types() {
|
||||
[first, second, third] => Some((*first, *second, *third)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
|
||||
match self.callable_ty {
|
||||
Type::FunctionLiteral(function) => Some(function.name(db)),
|
||||
|
||||
@@ -72,6 +72,7 @@ impl<'db> ClassBase<'db> {
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
|
||||
@@ -8,8 +8,8 @@ use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType, Type,
|
||||
UnionType,
|
||||
CallableType, ClassLiteralType, InstanceType, IntersectionType, KnownClass, StringLiteralType,
|
||||
Type, UnionType,
|
||||
};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -88,6 +88,24 @@ impl Display for DisplayRepresentation<'_> {
|
||||
},
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.repr(self.db)),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Callable(CallableType::BoundMethod(bound_method)) => {
|
||||
write!(
|
||||
f,
|
||||
"<bound method `{method}` of `{instance}`>",
|
||||
method = bound_method.function(self.db).name(self.db),
|
||||
instance = bound_method.self_instance(self.db).display(self.db)
|
||||
)
|
||||
}
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(function)) => {
|
||||
write!(
|
||||
f,
|
||||
"<method-wrapper `__get__` of `{function}`>",
|
||||
function = function.name(self.db)
|
||||
)
|
||||
}
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet) => {
|
||||
f.write_str("<wrapper-descriptor `__get__` of `function` objects>")
|
||||
}
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
Type::IntLiteral(n) => n.fmt(f),
|
||||
|
||||
@@ -3753,6 +3753,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
(
|
||||
op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
@@ -3780,7 +3781,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match operand_type.try_call_dunder(
|
||||
self.db(),
|
||||
unary_dunder_method,
|
||||
&CallArguments::positional([operand_type]),
|
||||
&CallArguments::none(),
|
||||
) {
|
||||
Ok(outcome) => outcome.return_type(self.db()),
|
||||
Err(e) => {
|
||||
@@ -3967,6 +3968,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// fall back on looking for dunder methods on one of the operand types.
|
||||
(
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
@@ -3983,6 +3985,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::Tuple(_),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::Callable(..)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::SubclassOf(_)
|
||||
@@ -4029,7 +4032,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
reflected_dunder,
|
||||
&CallArguments::positional([right_ty, left_ty]),
|
||||
&CallArguments::positional([left_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
.or_else(|_| {
|
||||
@@ -4037,7 +4040,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.try_call_dunder(
|
||||
self.db(),
|
||||
op.dunder(),
|
||||
&CallArguments::positional([left_ty, right_ty]),
|
||||
&CallArguments::positional([right_ty]),
|
||||
)
|
||||
.map(|outcome| outcome.return_type(self.db()))
|
||||
})
|
||||
@@ -4953,7 +4956,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
match value_ty.try_call_dunder(
|
||||
self.db(),
|
||||
"__getitem__",
|
||||
&CallArguments::positional([value_ty, slice_ty]),
|
||||
&CallArguments::positional([slice_ty]),
|
||||
) {
|
||||
Ok(outcome) => return outcome.return_type(self.db()),
|
||||
Err(err @ CallDunderError::PossiblyUnbound { .. }) => {
|
||||
|
||||
@@ -29,9 +29,10 @@ use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
use crate::db::tests::{setup_db, TestDb};
|
||||
use crate::symbol::{builtins_symbol, known_module_symbol};
|
||||
use crate::types::{
|
||||
IntersectionBuilder, KnownClass, KnownInstanceType, SubclassOfType, TupleType, Type, UnionType,
|
||||
BoundMethodType, CallableType, IntersectionBuilder, KnownClass, KnownInstanceType,
|
||||
SubclassOfType, TupleType, Type, UnionType,
|
||||
};
|
||||
use crate::KnownModule;
|
||||
use crate::{Db, KnownModule};
|
||||
use quickcheck::{Arbitrary, Gen};
|
||||
|
||||
/// A test representation of a type that can be transformed unambiguously into a real Type,
|
||||
@@ -67,6 +68,24 @@ pub(crate) enum Ty {
|
||||
SubclassOfAbcClass(&'static str),
|
||||
AlwaysTruthy,
|
||||
AlwaysFalsy,
|
||||
BuiltinsFunction(&'static str),
|
||||
BuiltinsBoundMethod {
|
||||
class: &'static str,
|
||||
method: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
fn create_bound_method<'db>(
|
||||
db: &'db dyn Db,
|
||||
function: Type<'db>,
|
||||
builtins_class: Type<'db>,
|
||||
) -> Type<'db> {
|
||||
Type::Callable(CallableType::BoundMethod(BoundMethodType::new(
|
||||
db,
|
||||
function.expect_function_literal(),
|
||||
builtins_class.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
@@ -123,6 +142,13 @@ impl Ty {
|
||||
),
|
||||
Ty::AlwaysTruthy => Type::AlwaysTruthy,
|
||||
Ty::AlwaysFalsy => Type::AlwaysFalsy,
|
||||
Ty::BuiltinsFunction(name) => builtins_symbol(db, name).expect_type(),
|
||||
Ty::BuiltinsBoundMethod { class, method } => {
|
||||
let builtins_class = builtins_symbol(db, class).expect_type();
|
||||
let function = builtins_class.static_member(db, method).expect_type();
|
||||
|
||||
create_bound_method(db, function, builtins_class)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,6 +199,16 @@ fn arbitrary_core_type(g: &mut Gen) -> Ty {
|
||||
Ty::SubclassOfAbcClass("ABCMeta"),
|
||||
Ty::AlwaysTruthy,
|
||||
Ty::AlwaysFalsy,
|
||||
Ty::BuiltinsFunction("chr"),
|
||||
Ty::BuiltinsFunction("ascii"),
|
||||
Ty::BuiltinsBoundMethod {
|
||||
class: "str",
|
||||
method: "isascii",
|
||||
},
|
||||
Ty::BuiltinsBoundMethod {
|
||||
class: "int",
|
||||
method: "bit_length",
|
||||
},
|
||||
])
|
||||
.unwrap()
|
||||
.clone()
|
||||
|
||||
@@ -21,6 +21,13 @@ pub(crate) struct Signature<'db> {
|
||||
}
|
||||
|
||||
impl<'db> Signature<'db> {
|
||||
pub(crate) fn new(parameters: Parameters<'db>, return_ty: Option<Type<'db>>) -> Self {
|
||||
Self {
|
||||
parameters,
|
||||
return_ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a todo signature: (*args: Todo, **kwargs: Todo) -> Todo
|
||||
pub(crate) fn todo() -> Self {
|
||||
Self {
|
||||
@@ -64,6 +71,10 @@ impl<'db> Signature<'db> {
|
||||
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
|
||||
|
||||
impl<'db> Parameters<'db> {
|
||||
pub(crate) fn new(parameters: impl IntoIterator<Item = Parameter<'db>>) -> Self {
|
||||
Self(parameters.into_iter().collect())
|
||||
}
|
||||
|
||||
/// Return todo parameters: (*args: Todo, **kwargs: Todo)
|
||||
fn todo() -> Self {
|
||||
Self(vec![
|
||||
@@ -233,6 +244,18 @@ pub(crate) struct Parameter<'db> {
|
||||
}
|
||||
|
||||
impl<'db> Parameter<'db> {
|
||||
pub(crate) fn new(
|
||||
name: Option<Name>,
|
||||
annotated_ty: Option<Type<'db>>,
|
||||
kind: ParameterKind<'db>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
annotated_ty,
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_node_and_kind(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
|
||||
@@ -64,8 +64,8 @@ impl<'db> SubclassOfType<'db> {
|
||||
!self.is_dynamic()
|
||||
}
|
||||
|
||||
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
Type::from(self.subclass_of).member(db, name)
|
||||
pub(crate) fn static_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
Type::from(self.subclass_of).static_member(db, name)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` is a subtype of `other`.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::types::CallableType;
|
||||
|
||||
use super::{
|
||||
class_base::ClassBase, ClassLiteralType, DynamicType, InstanceType, KnownInstanceType,
|
||||
TodoType, Type,
|
||||
@@ -54,6 +56,27 @@ pub(super) fn union_elements_ordering<'db>(left: &Type<'db>, right: &Type<'db>)
|
||||
(Type::FunctionLiteral(_), _) => Ordering::Less,
|
||||
(_, Type::FunctionLiteral(_)) => Ordering::Greater,
|
||||
|
||||
(
|
||||
Type::Callable(CallableType::BoundMethod(left)),
|
||||
Type::Callable(CallableType::BoundMethod(right)),
|
||||
) => left.cmp(right),
|
||||
(Type::Callable(CallableType::BoundMethod(_)), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::BoundMethod(_))) => Ordering::Greater,
|
||||
|
||||
(
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(left)),
|
||||
Type::Callable(CallableType::MethodWrapperDunderGet(right)),
|
||||
) => left.cmp(right),
|
||||
(Type::Callable(CallableType::MethodWrapperDunderGet(_)), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::MethodWrapperDunderGet(_))) => Ordering::Greater,
|
||||
|
||||
(
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet),
|
||||
Type::Callable(CallableType::WrapperDescriptorDunderGet),
|
||||
) => Ordering::Equal,
|
||||
(Type::Callable(CallableType::WrapperDescriptorDunderGet), _) => Ordering::Less,
|
||||
(_, Type::Callable(CallableType::WrapperDescriptorDunderGet)) => Ordering::Greater,
|
||||
|
||||
(Type::Tuple(left), Type::Tuple(right)) => left.cmp(right),
|
||||
(Type::Tuple(_), _) => Ordering::Less,
|
||||
(_, Type::Tuple(_)) => Ordering::Greater,
|
||||
|
||||
Reference in New Issue
Block a user