mirror of https://github.com/astral-sh/ruff
[red-knot] Add `__init__` arguments check when doing `try_call` on a class literal (#16512)
## Summary * Addresses #16511 for simple cases where only `__init__` method is bound on class or doesn't exist at all. * fixes a bug with argument counting in bound method diagnostics Caveats: * No handling of `__new__` or modified `__call__` on metaclass. * This leads to a couple of false positive errors in tests ## Test Plan - A couple new cases in mdtests - cargo nextest run -p red_knot_python_semantic --no-fail-fast --------- Co-authored-by: Carl Meyer <carl@astral.sh> Co-authored-by: David Peter <sharkdp@users.noreply.github.com>
This commit is contained in:
parent
ed14dbb1a2
commit
fab7d820bd
|
|
@ -8,7 +8,11 @@ Currently, red-knot doesn't support `typing.NewType` in type annotations.
|
||||||
from typing_extensions import NewType
|
from typing_extensions import NewType
|
||||||
from types import GenericAlias
|
from types import GenericAlias
|
||||||
|
|
||||||
|
X = GenericAlias(type, ())
|
||||||
A = NewType("A", int)
|
A = NewType("A", int)
|
||||||
|
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
|
||||||
|
# to be compatible with `type`
|
||||||
|
# error: [invalid-argument-type] "Object of type `NewType` cannot be assigned to parameter 2 (`origin`) of function `__new__`; expected type `type`"
|
||||||
B = GenericAlias(A, ())
|
B = GenericAlias(A, ())
|
||||||
|
|
||||||
def _(
|
def _(
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,325 @@
|
||||||
# Constructor
|
# Constructor
|
||||||
|
|
||||||
|
When classes are instantiated, Python calls the meta-class `__call__` method, which can either be
|
||||||
|
customized by the user or `type.__call__` is used.
|
||||||
|
|
||||||
|
The latter calls the `__new__` method of the class, which is responsible for creating the instance
|
||||||
|
and then calls the `__init__` method on the resulting instance to initialize it with the same
|
||||||
|
arguments.
|
||||||
|
|
||||||
|
Both `__new__` and `__init__` are looked up using full descriptor protocol, but `__new__` is then
|
||||||
|
called as an implicit static, rather than bound method with `cls` passed as the first argument.
|
||||||
|
`__init__` has no special handling, it is fetched as bound method and is called just like any other
|
||||||
|
dunder method.
|
||||||
|
|
||||||
|
`type.__call__` does other things too, but this is not yet handled by us.
|
||||||
|
|
||||||
|
Since every class has `object` in it's MRO, the default implementations are `object.__new__` and
|
||||||
|
`object.__init__`. They have some special behavior, namely:
|
||||||
|
|
||||||
|
- If neither `__new__` nor `__init__` are defined anywhere in the MRO of class (except for `object`)
|
||||||
|
\- no arguments are accepted and `TypeError` is raised if any are passed.
|
||||||
|
- If `__new__` is defined, but `__init__` is not - `object.__init__` will allow arbitrary arguments!
|
||||||
|
|
||||||
|
As of today there are a number of behaviors that we do not support:
|
||||||
|
|
||||||
|
- `__new__` is assumed to return an instance of the class on which it is called
|
||||||
|
- User defined `__call__` on metaclass is ignored
|
||||||
|
|
||||||
|
## Creating an instance of the `object` class itself
|
||||||
|
|
||||||
|
Test the behavior of the `object` class itself. As implementation has to ignore `object` own methods
|
||||||
|
as defined in typeshed due to behavior not expressible in typeshed (see above how `__init__` behaves
|
||||||
|
differently depending on whether `__new__` is defined or not), we have to test the behavior of
|
||||||
|
`object` itself.
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(object()) # revealed: object
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to class `object`: expected 0, got 1"
|
||||||
|
reveal_type(object(1)) # revealed: object
|
||||||
|
```
|
||||||
|
|
||||||
|
## No init or new
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Foo: ...
|
class Foo: ...
|
||||||
|
|
||||||
reveal_type(Foo()) # revealed: Foo
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 0, got 1"
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__new__` present on the class itself
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
def __new__(cls, x: int) -> "Foo":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__new__` present on a superclass
|
||||||
|
|
||||||
|
If the `__new__` method is defined on a superclass, we can still infer the signature of the
|
||||||
|
constructor from it.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing_extensions import Self
|
||||||
|
|
||||||
|
class Base:
|
||||||
|
def __new__(cls, x: int) -> Self: ...
|
||||||
|
|
||||||
|
class Foo(Base): ...
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditional `__new__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool) -> None:
|
||||||
|
class Foo:
|
||||||
|
if flag:
|
||||||
|
def __new__(cls, x: int): ...
|
||||||
|
else:
|
||||||
|
def __new__(cls, x: int, y: int = 1): ...
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of function `__new__`; expected type `int`"
|
||||||
|
reveal_type(Foo("1")) # revealed: Foo
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to function `__new__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## A descriptor in place of `__new__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
class SomeCallable:
|
||||||
|
def __call__(self, cls, x: int) -> "Foo":
|
||||||
|
obj = object.__new__(cls)
|
||||||
|
obj.x = x
|
||||||
|
return obj
|
||||||
|
|
||||||
|
class Descriptor:
|
||||||
|
def __get__(self, instance, owner) -> SomeCallable:
|
||||||
|
return SomeCallable()
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
__new__: Descriptor = Descriptor()
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## A callable instance in place of `__new__`
|
||||||
|
|
||||||
|
### Bound
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Callable:
|
||||||
|
def __call__(self, cls, x: int) -> "Foo":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
__new__ = Callable()
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possibly Unbound
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool) -> None:
|
||||||
|
class Callable:
|
||||||
|
if flag:
|
||||||
|
def __call__(self, cls, x: int) -> "Foo":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
__new__ = Callable()
|
||||||
|
|
||||||
|
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||||
|
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
|
||||||
|
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__init__` present on the class itself
|
||||||
|
|
||||||
|
If the class has an `__init__` method, we can infer the signature of the constructor from it.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
def __init__(self, x: int): ...
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__init__` present on a superclass
|
||||||
|
|
||||||
|
If the `__init__` method is defined on a superclass, we can still infer the signature of the
|
||||||
|
constructor from it.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Base:
|
||||||
|
def __init__(self, x: int): ...
|
||||||
|
|
||||||
|
class Foo(Base): ...
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conditional `__init__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool) -> None:
|
||||||
|
class Foo:
|
||||||
|
if flag:
|
||||||
|
def __init__(self, x: int): ...
|
||||||
|
else:
|
||||||
|
def __init__(self, x: int, y: int = 1): ...
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# error: [invalid-argument-type] "Object of type `Literal["1"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`"
|
||||||
|
reveal_type(Foo("1")) # revealed: Foo
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## A descriptor in place of `__init__`
|
||||||
|
|
||||||
|
```py
|
||||||
|
class SomeCallable:
|
||||||
|
# TODO: at runtime `__init__` is checked to return `None` and
|
||||||
|
# a `TypeError` is raised if it doesn't. However, apparently
|
||||||
|
# this is not true when the descriptor is used as `__init__`.
|
||||||
|
# However, we may still want to check this.
|
||||||
|
def __call__(self, x: int) -> str:
|
||||||
|
return "a"
|
||||||
|
|
||||||
|
class Descriptor:
|
||||||
|
def __get__(self, instance, owner) -> SomeCallable:
|
||||||
|
return SomeCallable()
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
__init__: Descriptor = Descriptor()
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## A callable instance in place of `__init__`
|
||||||
|
|
||||||
|
### Bound
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Callable:
|
||||||
|
def __call__(self, x: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
__init__ = Callable()
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possibly Unbound
|
||||||
|
|
||||||
|
```py
|
||||||
|
def _(flag: bool) -> None:
|
||||||
|
class Callable:
|
||||||
|
if flag:
|
||||||
|
def __call__(self, x: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Foo:
|
||||||
|
__init__ = Callable()
|
||||||
|
|
||||||
|
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
# TODO should be - error: [missing-argument] "No argument provided for required parameter `x` of bound method `__call__`"
|
||||||
|
# but we currently infer the signature of `__call__` as unknown, so it accepts any arguments
|
||||||
|
# error: [call-non-callable] "Object of type `Callable` is not callable (possibly unbound `__call__` method)"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
## `__new__` and `__init__` both present
|
||||||
|
|
||||||
|
### Identical signatures
|
||||||
|
|
||||||
|
A common case is to have `__new__` and `__init__` with identical signatures (except for the first
|
||||||
|
argument). We report errors for both `__new__` and `__init__` if the arguments are incorrect.
|
||||||
|
|
||||||
|
At runtime `__new__` is called first and will fail without executing `__init__` if the arguments are
|
||||||
|
incorrect. However, we decided that it is better to report errors for both methods, since after
|
||||||
|
fixing the `__new__` method, the user may forget to fix the `__init__` method.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
def __new__(cls, x: int) -> "Foo":
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, x: int): ...
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of function `__new__`"
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compatible signatures
|
||||||
|
|
||||||
|
But they can also be compatible, but not identical. We should correctly report errors only for the
|
||||||
|
mthod that would fail.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Foo:
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
return object.__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, x: int) -> None:
|
||||||
|
self.x = x
|
||||||
|
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||||
|
reveal_type(Foo()) # revealed: Foo
|
||||||
|
reveal_type(Foo(1)) # revealed: Foo
|
||||||
|
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||||
|
reveal_type(Foo(1, 2)) # revealed: Foo
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,11 @@ class C:
|
||||||
def _(subclass_of_c: type[C]):
|
def _(subclass_of_c: type[C]):
|
||||||
reveal_type(subclass_of_c(1)) # revealed: C
|
reveal_type(subclass_of_c(1)) # revealed: C
|
||||||
|
|
||||||
# TODO: Those should all be errors
|
# error: [invalid-argument-type] "Object of type `Literal["a"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `int`"
|
||||||
reveal_type(subclass_of_c("a")) # revealed: C
|
reveal_type(subclass_of_c("a")) # revealed: C
|
||||||
|
# error: [missing-argument] "No argument provided for required parameter `x` of bound method `__init__`"
|
||||||
reveal_type(subclass_of_c()) # revealed: C
|
reveal_type(subclass_of_c()) # revealed: C
|
||||||
|
# error: [too-many-positional-arguments] "Too many positional arguments to bound method `__init__`: expected 1, got 2"
|
||||||
reveal_type(subclass_of_c(1, 2)) # revealed: C
|
reveal_type(subclass_of_c(1, 2)) # revealed: C
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ class E[T]:
|
||||||
def __init__(self, x: T) -> None: ...
|
def __init__(self, x: T) -> None: ...
|
||||||
|
|
||||||
# TODO: revealed: E[int] or E[Literal[1]]
|
# TODO: revealed: E[int] or E[Literal[1]]
|
||||||
|
# TODO should not emit an error
|
||||||
|
# error: [invalid-argument-type] "Object of type `Literal[1]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`"
|
||||||
reveal_type(E(1)) # revealed: E
|
reveal_type(E(1)) # revealed: E
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -118,7 +120,8 @@ The types inferred from a type context and from a constructor parameter must be
|
||||||
other:
|
other:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# TODO: error
|
# TODO: the error should not leak the `T` typevar and should mention `E[int]`
|
||||||
|
# error: [invalid-argument-type] "Object of type `Literal["five"]` cannot be assigned to parameter 2 (`x`) of bound method `__init__`; expected type `T`"
|
||||||
wrong_innards: E[int] = E("five")
|
wrong_innards: E[int] = E("five")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ class Number:
|
||||||
def __invert__(self) -> Literal[True]:
|
def __invert__(self) -> Literal[True]:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
a = Number()
|
a = Number(0)
|
||||||
|
|
||||||
reveal_type(+a) # revealed: int
|
reveal_type(+a) # revealed: int
|
||||||
reveal_type(-a) # revealed: int
|
reveal_type(-a) # revealed: int
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::str::FromStr;
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use call::{CallDunderError, CallError, CallErrorKind};
|
use call::{CallDunderError, CallError, CallErrorKind};
|
||||||
use context::InferContext;
|
use context::InferContext;
|
||||||
use diagnostic::{INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
|
use diagnostic::{CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
|
||||||
use itertools::EitherOrBoth;
|
use itertools::EitherOrBoth;
|
||||||
use ruff_db::files::{File, FileRange};
|
use ruff_db::files::{File, FileRange};
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
|
|
@ -151,20 +151,60 @@ enum InstanceFallbackShadowsNonDataDescriptor {
|
||||||
No,
|
No,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dunder methods are looked up on the meta-type of a type without potentially falling
|
bitflags! {
|
||||||
/// back on attributes on the type itself. For example, when implicitly invoked on an
|
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
|
||||||
/// instance, dunder methods are not looked up as instance attributes. And when invoked
|
pub(crate) struct MemberLookupPolicy: u8 {
|
||||||
/// on a class, dunder methods are only looked up on the metaclass, not the class itself.
|
/// Dunder methods are looked up on the meta-type of a type without potentially falling
|
||||||
///
|
/// back on attributes on the type itself. For example, when implicitly invoked on an
|
||||||
/// All other attributes use the `WithInstanceFallback` policy.
|
/// instance, dunder methods are not looked up as instance attributes. And when invoked
|
||||||
#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash)]
|
/// on a class, dunder methods are only looked up on the metaclass, not the class itself.
|
||||||
enum MemberLookupPolicy {
|
///
|
||||||
|
/// All other attributes use the `WithInstanceFallback` policy.
|
||||||
|
///
|
||||||
|
/// If this flag is set - look up the attribute on the meta-type only.
|
||||||
|
const NO_INSTANCE_FALLBACK = 1 << 0;
|
||||||
|
|
||||||
|
/// When looking up an attribute on a class, we sometimes need to avoid
|
||||||
|
/// looking up attributes defined on the `object` class. Usually because
|
||||||
|
/// typeshed doesn't properly encode runtime behavior (e.g. see how `__new__` & `__init__`
|
||||||
|
/// are handled during class creation).
|
||||||
|
///
|
||||||
|
/// If this flag is set - exclude attributes defined on `object` when looking up attributes.
|
||||||
|
const MRO_NO_OBJECT_FALLBACK = 1 << 1;
|
||||||
|
|
||||||
|
/// When looking up an attribute on a class, we sometimes need to avoid
|
||||||
|
/// looking up attributes defined on `type` if this is the metaclass of the class.
|
||||||
|
///
|
||||||
|
/// This is similar to no object fallback above
|
||||||
|
const META_CLASS_NO_TYPE_FALLBACK = 1 << 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MemberLookupPolicy {
|
||||||
/// Only look up the attribute on the meta-type.
|
/// Only look up the attribute on the meta-type.
|
||||||
NoInstanceFallback,
|
///
|
||||||
/// Look up the attribute on the meta-type, but fall back to attributes on the instance
|
/// If false - Look up the attribute on the meta-type, but fall back to attributes on the instance
|
||||||
/// if the meta-type attribute is not found or if the meta-type attribute is not a data
|
/// if the meta-type attribute is not found or if the meta-type attribute is not a data
|
||||||
/// descriptor.
|
/// descriptor.
|
||||||
WithInstanceFallback,
|
pub(crate) const fn no_instance_fallback(self) -> bool {
|
||||||
|
self.contains(Self::NO_INSTANCE_FALLBACK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exclude attributes defined on `object` when looking up attributes.
|
||||||
|
pub(crate) const fn mro_no_object_fallback(self) -> bool {
|
||||||
|
self.contains(Self::MRO_NO_OBJECT_FALLBACK)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exclude attributes defined on `type` when looking up meta-class-attributes.
|
||||||
|
pub(crate) const fn meta_class_no_type_fallback(self) -> bool {
|
||||||
|
self.contains(Self::META_CLASS_NO_TYPE_FALLBACK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MemberLookupPolicy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::empty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AttributeKind {
|
impl AttributeKind {
|
||||||
|
|
@ -444,6 +484,10 @@ impl<'db> Type<'db> {
|
||||||
.expect("Expected a Type::ClassLiteral variant")
|
.expect("Expected a Type::ClassLiteral variant")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn is_subclass_of(&self) -> bool {
|
||||||
|
matches!(self, Type::SubclassOf(..))
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn is_class_literal(&self) -> bool {
|
pub const fn is_class_literal(&self) -> bool {
|
||||||
matches!(self, Type::ClassLiteral(..))
|
matches!(self, Type::ClassLiteral(..))
|
||||||
}
|
}
|
||||||
|
|
@ -1840,9 +1884,18 @@ impl<'db> Type<'db> {
|
||||||
/// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
|
/// [descriptor guide]: https://docs.python.org/3/howto/descriptor.html#invocation-from-an-instance
|
||||||
/// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223
|
/// [`_PyType_Lookup`]: https://github.com/python/cpython/blob/e285232c76606e3be7bf216efb1be1e742423e4b/Objects/typeobject.c#L5223
|
||||||
fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option<SymbolAndQualifiers<'db>> {
|
fn find_name_in_mro(&self, db: &'db dyn Db, name: &str) -> Option<SymbolAndQualifiers<'db>> {
|
||||||
|
self.find_name_in_mro_with_policy(db, name, MemberLookupPolicy::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_name_in_mro_with_policy(
|
||||||
|
&self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: &str,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
|
) -> Option<SymbolAndQualifiers<'db>> {
|
||||||
match self {
|
match self {
|
||||||
Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| {
|
Type::Union(union) => Some(union.map_with_boundness_and_qualifiers(db, |elem| {
|
||||||
elem.find_name_in_mro(db, name)
|
elem.find_name_in_mro_with_policy(db, name, policy)
|
||||||
// If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class
|
// If some elements are classes, and some are not, we simply fall back to `Unbound` for the non-class
|
||||||
// elements instead of short-circuiting the whole result to `None`. We would need a more detailed
|
// elements instead of short-circuiting the whole result to `None`. We would need a more detailed
|
||||||
// return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is
|
// return type otherwise, and since `find_name_in_mro` is usually called via `class_member`, this is
|
||||||
|
|
@ -1851,7 +1904,7 @@ impl<'db> Type<'db> {
|
||||||
})),
|
})),
|
||||||
Type::Intersection(inter) => {
|
Type::Intersection(inter) => {
|
||||||
Some(inter.map_with_boundness_and_qualifiers(db, |elem| {
|
Some(inter.map_with_boundness_and_qualifiers(db, |elem| {
|
||||||
elem.find_name_in_mro(db, name)
|
elem.find_name_in_mro_with_policy(db, name, policy)
|
||||||
// Fall back to Unbound, similar to the union case (see above).
|
// Fall back to Unbound, similar to the union case (see above).
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}))
|
}))
|
||||||
|
|
@ -1903,7 +1956,7 @@ impl<'db> Type<'db> {
|
||||||
"__get__" | "__set__" | "__delete__",
|
"__get__" | "__set__" | "__delete__",
|
||||||
) => Some(Symbol::Unbound.into()),
|
) => Some(Symbol::Unbound.into()),
|
||||||
|
|
||||||
_ => Some(class_literal.class_member(db, name)),
|
_ => Some(class_literal.class_member(db, name, policy)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1926,7 +1979,9 @@ impl<'db> Type<'db> {
|
||||||
{
|
{
|
||||||
Some(Symbol::Unbound.into())
|
Some(Symbol::Unbound.into())
|
||||||
}
|
}
|
||||||
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.find_name_in_mro(db, name),
|
Type::SubclassOf(subclass_of_ty) => {
|
||||||
|
subclass_of_ty.find_name_in_mro_with_policy(db, name, policy)
|
||||||
|
}
|
||||||
|
|
||||||
// We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type).
|
// We eagerly normalize type[object], i.e. Type::SubclassOf(object) to `type`, i.e. Type::Instance(type).
|
||||||
// So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the
|
// So looking up a name in the MRO of `Type::Instance(type)` is equivalent to looking up the name in the
|
||||||
|
|
@ -1934,7 +1989,7 @@ impl<'db> Type<'db> {
|
||||||
Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => {
|
Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Type) => {
|
||||||
KnownClass::Object
|
KnownClass::Object
|
||||||
.to_class_literal(db)
|
.to_class_literal(db)
|
||||||
.find_name_in_mro(db, name)
|
.find_name_in_mro_with_policy(db, name, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
Type::FunctionLiteral(_)
|
Type::FunctionLiteral(_)
|
||||||
|
|
@ -1964,17 +2019,28 @@ impl<'db> Type<'db> {
|
||||||
///
|
///
|
||||||
/// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling
|
/// Basically corresponds to `self.to_meta_type().find_name_in_mro(name)`, except for the handling
|
||||||
/// of union and intersection types.
|
/// of union and intersection types.
|
||||||
#[salsa::tracked]
|
|
||||||
fn class_member(self, db: &'db dyn Db, name: Name) -> SymbolAndQualifiers<'db> {
|
fn class_member(self, db: &'db dyn Db, name: Name) -> SymbolAndQualifiers<'db> {
|
||||||
|
self.class_member_with_policy(db, name, MemberLookupPolicy::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[salsa::tracked]
|
||||||
|
fn class_member_with_policy(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: Name,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
|
) -> SymbolAndQualifiers<'db> {
|
||||||
tracing::trace!("class_member: {}.{}", self.display(db), name);
|
tracing::trace!("class_member: {}.{}", self.display(db), name);
|
||||||
match self {
|
match self {
|
||||||
Type::Union(union) => union
|
Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| {
|
||||||
.map_with_boundness_and_qualifiers(db, |elem| elem.class_member(db, name.clone())),
|
elem.class_member_with_policy(db, name.clone(), policy)
|
||||||
Type::Intersection(inter) => inter
|
}),
|
||||||
.map_with_boundness_and_qualifiers(db, |elem| elem.class_member(db, name.clone())),
|
Type::Intersection(inter) => inter.map_with_boundness_and_qualifiers(db, |elem| {
|
||||||
|
elem.class_member_with_policy(db, name.clone(), policy)
|
||||||
|
}),
|
||||||
_ => self
|
_ => self
|
||||||
.to_meta_type(db)
|
.to_meta_type(db)
|
||||||
.find_name_in_mro(db, name.as_str())
|
.find_name_in_mro_with_policy(db, name.as_str(), policy)
|
||||||
.expect(
|
.expect(
|
||||||
"`Type::find_name_in_mro()` should return `Some()` when called on a meta-type",
|
"`Type::find_name_in_mro()` should return `Some()` when called on a meta-type",
|
||||||
),
|
),
|
||||||
|
|
@ -2235,6 +2301,7 @@ impl<'db> Type<'db> {
|
||||||
name: &str,
|
name: &str,
|
||||||
fallback: SymbolAndQualifiers<'db>,
|
fallback: SymbolAndQualifiers<'db>,
|
||||||
policy: InstanceFallbackShadowsNonDataDescriptor,
|
policy: InstanceFallbackShadowsNonDataDescriptor,
|
||||||
|
member_policy: MemberLookupPolicy,
|
||||||
) -> SymbolAndQualifiers<'db> {
|
) -> SymbolAndQualifiers<'db> {
|
||||||
let (
|
let (
|
||||||
SymbolAndQualifiers {
|
SymbolAndQualifiers {
|
||||||
|
|
@ -2244,7 +2311,7 @@ impl<'db> Type<'db> {
|
||||||
meta_attr_kind,
|
meta_attr_kind,
|
||||||
) = Self::try_call_dunder_get_on_attribute(
|
) = Self::try_call_dunder_get_on_attribute(
|
||||||
db,
|
db,
|
||||||
self.class_member(db, name.into()),
|
self.class_member_with_policy(db, name.into(), member_policy),
|
||||||
self,
|
self,
|
||||||
self.to_meta_type(db),
|
self.to_meta_type(db),
|
||||||
);
|
);
|
||||||
|
|
@ -2323,7 +2390,7 @@ impl<'db> Type<'db> {
|
||||||
/// lookup, like a failed `__get__` call on a descriptor.
|
/// lookup, like a failed `__get__` call on a descriptor.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
pub(crate) fn member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
||||||
self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::WithInstanceFallback)
|
self.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Similar to [`Type::member`], but allows the caller to specify what policy should be used
|
/// Similar to [`Type::member`], but allows the caller to specify what policy should be used
|
||||||
|
|
@ -2454,15 +2521,17 @@ impl<'db> Type<'db> {
|
||||||
|
|
||||||
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
|
Type::ModuleLiteral(module) => module.static_member(db, name_str).into(),
|
||||||
|
|
||||||
Type::AlwaysFalsy | Type::AlwaysTruthy => self.class_member(db, name),
|
Type::AlwaysFalsy | Type::AlwaysTruthy => {
|
||||||
|
self.class_member_with_policy(db, name, policy)
|
||||||
|
}
|
||||||
|
|
||||||
_ if policy == MemberLookupPolicy::NoInstanceFallback => self
|
_ if policy.no_instance_fallback() => self.invoke_descriptor_protocol(
|
||||||
.invoke_descriptor_protocol(
|
db,
|
||||||
db,
|
name_str,
|
||||||
name_str,
|
Symbol::Unbound.into(),
|
||||||
Symbol::Unbound.into(),
|
InstanceFallbackShadowsNonDataDescriptor::No,
|
||||||
InstanceFallbackShadowsNonDataDescriptor::No,
|
policy,
|
||||||
),
|
),
|
||||||
|
|
||||||
Type::Instance(..)
|
Type::Instance(..)
|
||||||
| Type::BooleanLiteral(..)
|
| Type::BooleanLiteral(..)
|
||||||
|
|
@ -2483,15 +2552,21 @@ impl<'db> Type<'db> {
|
||||||
name_str,
|
name_str,
|
||||||
fallback,
|
fallback,
|
||||||
InstanceFallbackShadowsNonDataDescriptor::No,
|
InstanceFallbackShadowsNonDataDescriptor::No,
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
|
|
||||||
let custom_getattr_result = || {
|
let custom_getattr_result = || {
|
||||||
// Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with dynamic imports.
|
// Typeshed has a fake `__getattr__` on `types.ModuleType` to help out with
|
||||||
// We explicitly hide it here to prevent arbitrary attributes from being available on modules.
|
// dynamic imports. We explicitly hide it here to prevent arbitrary attributes
|
||||||
if self
|
// from being available on modules. Same for `types.GenericAlias` - its
|
||||||
.into_instance()
|
// `__getattr__` method will delegate to `__origin__` to allow looking up
|
||||||
.is_some_and(|instance| instance.class.is_known(db, KnownClass::ModuleType))
|
// attributes on the original type. But in typeshed its return type is `Any`.
|
||||||
{
|
// It will need a special handling, so it remember the origin type to properly
|
||||||
|
// resolve the attribute.
|
||||||
|
if self.into_instance().is_some_and(|instance| {
|
||||||
|
instance.class.is_known(db, KnownClass::ModuleType)
|
||||||
|
|| instance.class.is_known(db, KnownClass::GenericAlias)
|
||||||
|
}) {
|
||||||
return Symbol::Unbound.into();
|
return Symbol::Unbound.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2525,7 +2600,7 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
||||||
let class_attr_plain = self.find_name_in_mro(db, name_str).expect(
|
let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str, policy).expect(
|
||||||
"Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`",
|
"Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -2550,6 +2625,7 @@ impl<'db> Type<'db> {
|
||||||
name_str,
|
name_str,
|
||||||
class_attr_fallback,
|
class_attr_fallback,
|
||||||
InstanceFallbackShadowsNonDataDescriptor::Yes,
|
InstanceFallbackShadowsNonDataDescriptor::Yes,
|
||||||
|
policy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3066,6 +3142,12 @@ impl<'db> Type<'db> {
|
||||||
},
|
},
|
||||||
|
|
||||||
Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) {
|
Type::ClassLiteral(ClassLiteralType { class }) => match class.known(db) {
|
||||||
|
// TODO: Ideally we'd use `try_call_constructor` for all constructor calls.
|
||||||
|
// Currently we don't for a few special known types, either because their
|
||||||
|
// constructors are defined with overloads, or because we want to special case
|
||||||
|
// their return type beyond what typeshed provides (though this support could
|
||||||
|
// likely be moved into the `try_call_constructor` path). Once we support
|
||||||
|
// overloads, re-evaluate the need for these arms.
|
||||||
Some(KnownClass::Bool) => {
|
Some(KnownClass::Bool) => {
|
||||||
// ```py
|
// ```py
|
||||||
// class bool(int):
|
// class bool(int):
|
||||||
|
|
@ -3165,6 +3247,21 @@ impl<'db> Type<'db> {
|
||||||
);
|
);
|
||||||
Signatures::single(signature)
|
Signatures::single(signature)
|
||||||
}
|
}
|
||||||
|
Some(KnownClass::Object) => {
|
||||||
|
// ```py
|
||||||
|
// class object:
|
||||||
|
// def __init__(self) -> None: ...
|
||||||
|
// def __new__(cls) -> Self: ...
|
||||||
|
// ```
|
||||||
|
let signature = CallableSignature::from_overloads(
|
||||||
|
self,
|
||||||
|
[Signature::new(
|
||||||
|
Parameters::empty(),
|
||||||
|
Some(KnownClass::Object.to_instance(db)),
|
||||||
|
)],
|
||||||
|
);
|
||||||
|
Signatures::single(signature)
|
||||||
|
}
|
||||||
|
|
||||||
Some(KnownClass::Property) => {
|
Some(KnownClass::Property) => {
|
||||||
let getter_signature = Signature::new(
|
let getter_signature = Signature::new(
|
||||||
|
|
@ -3234,8 +3331,11 @@ impl<'db> Type<'db> {
|
||||||
Signatures::single(signature)
|
Signatures::single(signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
// Most class literal constructor calls are handled by `try_call_constructor` and
|
||||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
// not via getting the signature here. This signature can still be used in some
|
||||||
|
// cases (e.g. evaluating callable subtyping). TODO improve this definition
|
||||||
|
// (intersection of `__new__` and `__init__` signatures? and respect metaclass
|
||||||
|
// `__call__`).
|
||||||
_ => {
|
_ => {
|
||||||
let signature = CallableSignature::single(
|
let signature = CallableSignature::single(
|
||||||
self,
|
self,
|
||||||
|
|
@ -3247,6 +3347,10 @@ impl<'db> Type<'db> {
|
||||||
|
|
||||||
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||||
ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db),
|
ClassBase::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type).signatures(db),
|
||||||
|
// Most type[] constructor calls are handled by `try_call_constructor` and not via
|
||||||
|
// getting the signature here. This signature can still be used in some cases (e.g.
|
||||||
|
// evaluating callable subtyping). TODO improve this definition (intersection of
|
||||||
|
// `__new__` and `__init__` signatures? and respect metaclass `__call__`).
|
||||||
ClassBase::Class(class) => Type::class_literal(class).signatures(db),
|
ClassBase::Class(class) => Type::class_literal(class).signatures(db),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -3260,7 +3364,7 @@ impl<'db> Type<'db> {
|
||||||
.member_lookup_with_policy(
|
.member_lookup_with_policy(
|
||||||
db,
|
db,
|
||||||
Name::new_static("__call__"),
|
Name::new_static("__call__"),
|
||||||
MemberLookupPolicy::NoInstanceFallback,
|
MemberLookupPolicy::NO_INSTANCE_FALLBACK,
|
||||||
)
|
)
|
||||||
.symbol
|
.symbol
|
||||||
{
|
{
|
||||||
|
|
@ -3324,7 +3428,7 @@ impl<'db> Type<'db> {
|
||||||
mut argument_types: CallArgumentTypes<'_, 'db>,
|
mut argument_types: CallArgumentTypes<'_, 'db>,
|
||||||
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
) -> Result<Bindings<'db>, CallDunderError<'db>> {
|
||||||
match self
|
match self
|
||||||
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NoInstanceFallback)
|
.member_lookup_with_policy(db, name.into(), MemberLookupPolicy::NO_INSTANCE_FALLBACK)
|
||||||
.symbol
|
.symbol
|
||||||
{
|
{
|
||||||
Symbol::Type(dunder_callable, boundness) => {
|
Symbol::Type(dunder_callable, boundness) => {
|
||||||
|
|
@ -3485,6 +3589,124 @@ impl<'db> Type<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Given a class literal or non-dynamic SubclassOf type, try calling it (creating an instance)
|
||||||
|
/// and return the resulting instance type.
|
||||||
|
///
|
||||||
|
/// Models `type.__call__` behavior.
|
||||||
|
/// TODO: model metaclass `__call__`.
|
||||||
|
///
|
||||||
|
/// E.g., for the following code, infer the type of `Foo()`:
|
||||||
|
/// ```python
|
||||||
|
/// class Foo:
|
||||||
|
/// pass
|
||||||
|
///
|
||||||
|
/// Foo()
|
||||||
|
/// ```
|
||||||
|
fn try_call_constructor(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
argument_types: CallArgumentTypes<'_, 'db>,
|
||||||
|
) -> Result<Type<'db>, ConstructorCallError<'db>> {
|
||||||
|
debug_assert!(matches!(self, Type::ClassLiteral(_) | Type::SubclassOf(_)));
|
||||||
|
|
||||||
|
// As of now we do not model custom `__call__` on meta-classes, so the code below
|
||||||
|
// only deals with interplay between `__new__` and `__init__` methods.
|
||||||
|
// The logic is roughly as follows:
|
||||||
|
// 1. If `__new__` is defined anywhere in the MRO (except for `object`, since it is always
|
||||||
|
// present), we call it and analyze outcome. We then analyze `__init__` call, but only
|
||||||
|
// if it is defined somewhere except object. This is because `object.__init__`
|
||||||
|
// allows arbitrary arguments if and only if `__new__` is defined, but typeshed
|
||||||
|
// defines `__init__` for `object` with no arguments.
|
||||||
|
// 2. If `__new__` is not found, we call `__init__`. Here, we allow it to fallback all
|
||||||
|
// the way to `object` (single `self` argument call). This time it is correct to
|
||||||
|
// fallback to `object.__init__`, since it will indeed check that no arguments are
|
||||||
|
// passed.
|
||||||
|
//
|
||||||
|
// Note that we currently ignore `__new__` return type, since we do not yet support `Self`
|
||||||
|
// and most builtin classes use it as return type annotation. We always return the instance
|
||||||
|
// type.
|
||||||
|
|
||||||
|
// Lookup `__new__` method in the MRO up to, but not including, `object`. Also, we must
|
||||||
|
// avoid `__new__` on `type` since per descriptor protocol, if `__new__` is not defined on
|
||||||
|
// a class, metaclass attribute would take precedence. But by avoiding `__new__` on
|
||||||
|
// `object` we would inadvertently unhide `__new__` on `type`, which is not what we want.
|
||||||
|
// An alternative might be to not skip `object.__new__` but instead mark it such that it's
|
||||||
|
// easy to check if that's the one we found?
|
||||||
|
let new_call_outcome: Option<Result<Bindings<'db>, CallDunderError<'db>>> = match self
|
||||||
|
.member_lookup_with_policy(
|
||||||
|
db,
|
||||||
|
"__new__".into(),
|
||||||
|
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK
|
||||||
|
| MemberLookupPolicy::META_CLASS_NO_TYPE_FALLBACK,
|
||||||
|
)
|
||||||
|
.symbol
|
||||||
|
{
|
||||||
|
Symbol::Type(dunder_callable, boundness) => {
|
||||||
|
let signatures = dunder_callable.signatures(db);
|
||||||
|
// `__new__` is a static method, so we must inject the `cls` argument.
|
||||||
|
let mut argument_types = argument_types.prepend_synthetic(self);
|
||||||
|
|
||||||
|
Some(
|
||||||
|
match Bindings::match_parameters(signatures, &mut argument_types)
|
||||||
|
.check_types(db, &mut argument_types)
|
||||||
|
{
|
||||||
|
Ok(bindings) => {
|
||||||
|
if boundness == Boundness::PossiblyUnbound {
|
||||||
|
Err(CallDunderError::PossiblyUnbound(Box::new(bindings)))
|
||||||
|
} else {
|
||||||
|
Ok(bindings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => Err(err.into()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// No explicit `__new__` method found
|
||||||
|
Symbol::Unbound => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: we should use the actual return type of `__new__` to determine the instance type
|
||||||
|
let instance_ty = self
|
||||||
|
.to_instance(db)
|
||||||
|
.expect("Class literal type and subclass-of types should always be convertible to instance type");
|
||||||
|
|
||||||
|
let init_call_outcome = if new_call_outcome.is_none()
|
||||||
|
|| !instance_ty
|
||||||
|
.member_lookup_with_policy(
|
||||||
|
db,
|
||||||
|
"__init__".into(),
|
||||||
|
MemberLookupPolicy::MRO_NO_OBJECT_FALLBACK,
|
||||||
|
)
|
||||||
|
.symbol
|
||||||
|
.is_unbound()
|
||||||
|
{
|
||||||
|
Some(instance_ty.try_call_dunder(db, "__init__", argument_types))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
match (new_call_outcome, init_call_outcome) {
|
||||||
|
// All calls are successful or not called at all
|
||||||
|
(None | Some(Ok(_)), None | Some(Ok(_))) => Ok(instance_ty),
|
||||||
|
(None | Some(Ok(_)), Some(Err(error))) => {
|
||||||
|
// no custom `__new__` or it was called and succeeded, but `__init__` failed.
|
||||||
|
Err(ConstructorCallError::Init(instance_ty, error))
|
||||||
|
}
|
||||||
|
(Some(Err(error)), None | Some(Ok(_))) => {
|
||||||
|
// custom `__new__` was called and failed, but init is ok
|
||||||
|
Err(ConstructorCallError::New(instance_ty, error))
|
||||||
|
}
|
||||||
|
(Some(Err(new_error)), Some(Err(init_error))) => {
|
||||||
|
// custom `__new__` was called and failed, and `__init__` is also not ok
|
||||||
|
Err(ConstructorCallError::NewAndInit(
|
||||||
|
instance_ty,
|
||||||
|
new_error,
|
||||||
|
init_error,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn to_instance(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
pub fn to_instance(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||||
match self {
|
match self {
|
||||||
|
|
@ -4718,6 +4940,98 @@ impl<'db> BoolError<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Error returned if a class instantiation call failed
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ConstructorCallError<'db> {
|
||||||
|
Init(Type<'db>, CallDunderError<'db>),
|
||||||
|
New(Type<'db>, CallDunderError<'db>),
|
||||||
|
NewAndInit(Type<'db>, CallDunderError<'db>, CallDunderError<'db>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> ConstructorCallError<'db> {
|
||||||
|
fn return_type(&self) -> Type<'db> {
|
||||||
|
match self {
|
||||||
|
Self::Init(ty, _) => *ty,
|
||||||
|
Self::New(ty, _) => *ty,
|
||||||
|
Self::NewAndInit(ty, _, _) => *ty,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn report_diagnostic(
|
||||||
|
&self,
|
||||||
|
context: &InferContext<'db>,
|
||||||
|
context_expression_type: Type<'db>,
|
||||||
|
context_expression_node: ast::AnyNodeRef,
|
||||||
|
) {
|
||||||
|
let report_init_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
|
||||||
|
CallDunderError::MethodNotAvailable => {
|
||||||
|
// If we are using vendored typeshed, it should be impossible to have missing
|
||||||
|
// or unbound `__init__` method on a class, as all classes have `object` in MRO.
|
||||||
|
// Thus the following may only trigger if a custom typeshed is used.
|
||||||
|
context.report_lint(
|
||||||
|
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||||
|
context_expression_node,
|
||||||
|
format_args!(
|
||||||
|
"`__init__` method is missing on type `{}`. Make sure your `object` in typeshed has its definition.",
|
||||||
|
context_expression_type.display(context.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CallDunderError::PossiblyUnbound(bindings) => {
|
||||||
|
context.report_lint(
|
||||||
|
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||||
|
context_expression_node,
|
||||||
|
format_args!(
|
||||||
|
"Method `__init__` on type `{}` is possibly unbound.",
|
||||||
|
context_expression_type.display(context.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
bindings.report_diagnostics(context, context_expression_node);
|
||||||
|
}
|
||||||
|
CallDunderError::CallError(_, bindings) => {
|
||||||
|
bindings.report_diagnostics(context, context_expression_node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let report_new_error = |call_dunder_error: &CallDunderError<'db>| match call_dunder_error {
|
||||||
|
CallDunderError::MethodNotAvailable => {
|
||||||
|
// We are explicitly checking for `__new__` before attempting to call it,
|
||||||
|
// so this should never happen.
|
||||||
|
unreachable!("`__new__` method may not be called if missing");
|
||||||
|
}
|
||||||
|
CallDunderError::PossiblyUnbound(bindings) => {
|
||||||
|
context.report_lint(
|
||||||
|
&CALL_POSSIBLY_UNBOUND_METHOD,
|
||||||
|
context_expression_node,
|
||||||
|
format_args!(
|
||||||
|
"Method `__new__` on type `{}` is possibly unbound.",
|
||||||
|
context_expression_type.display(context.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
bindings.report_diagnostics(context, context_expression_node);
|
||||||
|
}
|
||||||
|
CallDunderError::CallError(_, bindings) => {
|
||||||
|
bindings.report_diagnostics(context, context_expression_node);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Init(_, call_dunder_error) => {
|
||||||
|
report_init_error(call_dunder_error);
|
||||||
|
}
|
||||||
|
Self::New(_, call_dunder_error) => {
|
||||||
|
report_new_error(call_dunder_error);
|
||||||
|
}
|
||||||
|
Self::NewAndInit(_, new_call_dunder_error, init_call_dunder_error) => {
|
||||||
|
report_new_error(new_call_dunder_error);
|
||||||
|
report_init_error(init_call_dunder_error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub enum Truthiness {
|
pub enum Truthiness {
|
||||||
/// For an object `x`, `bool(x)` will always return `True`
|
/// For an object `x`, `bool(x)` will always return `True`
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,21 @@ impl<'a, 'db> CallArgumentTypes<'a, 'db> {
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new [`CallArgumentTypes`] by prepending a synthetic argument to the front of this
|
||||||
|
/// argument list.
|
||||||
|
pub(crate) fn prepend_synthetic(&self, synthetic: Type<'db>) -> Self {
|
||||||
|
Self {
|
||||||
|
arguments: CallArguments(
|
||||||
|
std::iter::once(Argument::Synthetic)
|
||||||
|
.chain(self.arguments.iter())
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
types: std::iter::once(synthetic)
|
||||||
|
.chain(self.types.iter().copied())
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
|
pub(crate) fn iter(&self) -> impl Iterator<Item = (Argument<'a>, Type<'db>)> + '_ {
|
||||||
self.arguments.iter().zip(self.types.iter().copied())
|
self.arguments.iter().zip(self.types.iter().copied())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -924,8 +924,14 @@ impl<'db> Binding<'db> {
|
||||||
first_excess_argument_index,
|
first_excess_argument_index,
|
||||||
num_synthetic_args,
|
num_synthetic_args,
|
||||||
),
|
),
|
||||||
expected_positional_count: parameters.positional().count(),
|
expected_positional_count: parameters
|
||||||
provided_positional_count: next_positional,
|
.positional()
|
||||||
|
.count()
|
||||||
|
// using saturating_sub to avoid negative values due to invalid syntax in source code
|
||||||
|
.saturating_sub(num_synthetic_args),
|
||||||
|
provided_positional_count: next_positional
|
||||||
|
// using saturating_sub to avoid negative values due to invalid syntax in source code
|
||||||
|
.saturating_sub(num_synthetic_args),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let mut missing = vec![];
|
let mut missing = vec![];
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
|
class_base::ClassBase, infer_expression_type, infer_unpack_types, IntersectionBuilder,
|
||||||
KnownFunction, Mro, MroError, MroIterator, SubclassOfType, Truthiness, Type, TypeAliasType,
|
KnownFunction, MemberLookupPolicy, Mro, MroError, MroIterator, SubclassOfType, Truthiness,
|
||||||
TypeQualifiers, TypeVarInstance,
|
Type, TypeAliasType, TypeQualifiers, TypeVarInstance,
|
||||||
};
|
};
|
||||||
use crate::semantic_index::definition::Definition;
|
use crate::semantic_index::definition::Definition;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -323,7 +323,12 @@ impl<'db> Class<'db> {
|
||||||
/// The member resolves to a member on the class itself or any of its proper superclasses.
|
/// The member resolves to a member on the class itself or any of its proper superclasses.
|
||||||
///
|
///
|
||||||
/// TODO: Should this be made private...?
|
/// TODO: Should this be made private...?
|
||||||
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
pub(super) fn class_member(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: &str,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
|
) -> SymbolAndQualifiers<'db> {
|
||||||
if name == "__mro__" {
|
if name == "__mro__" {
|
||||||
let tuple_elements = self.iter_mro(db).map(Type::from);
|
let tuple_elements = self.iter_mro(db).map(Type::from);
|
||||||
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
|
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
|
||||||
|
|
@ -354,6 +359,18 @@ impl<'db> Class<'db> {
|
||||||
dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass));
|
dynamic_type_to_intersect_with.get_or_insert(Type::from(superclass));
|
||||||
}
|
}
|
||||||
ClassBase::Class(class) => {
|
ClassBase::Class(class) => {
|
||||||
|
if class.is_known(db, KnownClass::Object)
|
||||||
|
// Only exclude `object` members if this is not an `object` class itself
|
||||||
|
&& (policy.mro_no_object_fallback() && !self.is_known(db, KnownClass::Object))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if class.is_known(db, KnownClass::Type) && policy.meta_class_no_type_fallback()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
lookup_result = lookup_result.or_else(|lookup_error| {
|
lookup_result = lookup_result.or_else(|lookup_error| {
|
||||||
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
|
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
|
||||||
});
|
});
|
||||||
|
|
@ -776,8 +793,13 @@ impl<'db> ClassLiteralType<'db> {
|
||||||
self.class.body_scope(db)
|
self.class.body_scope(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn class_member(self, db: &'db dyn Db, name: &str) -> SymbolAndQualifiers<'db> {
|
pub(super) fn class_member(
|
||||||
self.class.class_member(db, name)
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: &str,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
|
) -> SymbolAndQualifiers<'db> {
|
||||||
|
self.class.class_member(db, name, policy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ use super::slots::check_class_slots;
|
||||||
use super::string_annotation::{
|
use super::string_annotation::{
|
||||||
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||||
};
|
};
|
||||||
use super::CallDunderError;
|
use super::{CallDunderError, ClassLiteralType};
|
||||||
|
|
||||||
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
|
/// 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
|
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
|
||||||
|
|
@ -2380,10 +2380,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| Type::WrapperDescriptor(_)
|
| Type::WrapperDescriptor(_)
|
||||||
| Type::TypeVar(..)
|
| Type::TypeVar(..)
|
||||||
| Type::AlwaysTruthy
|
| Type::AlwaysTruthy
|
||||||
| Type::AlwaysFalsy => match object_ty.class_member(db, attribute.into()) {
|
| Type::AlwaysFalsy => {
|
||||||
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
|
match object_ty.class_member(db, attribute.into()) {
|
||||||
if emit_diagnostics {
|
meta_attr @ SymbolAndQualifiers { .. } if meta_attr.is_class_var() => {
|
||||||
self.context.report_lint(
|
if emit_diagnostics {
|
||||||
|
self.context.report_lint(
|
||||||
&INVALID_ATTRIBUTE_ACCESS,
|
&INVALID_ATTRIBUTE_ACCESS,
|
||||||
target,
|
target,
|
||||||
format_args!(
|
format_args!(
|
||||||
|
|
@ -2391,26 +2392,30 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
ty = object_ty.display(self.db()),
|
ty = object_ty.display(self.db()),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
}
|
}
|
||||||
false
|
SymbolAndQualifiers {
|
||||||
}
|
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
|
||||||
SymbolAndQualifiers {
|
qualifiers: _,
|
||||||
symbol: Symbol::Type(meta_attr_ty, meta_attr_boundness),
|
} => {
|
||||||
qualifiers: _,
|
let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) =
|
||||||
} => {
|
meta_attr_ty.class_member(db, "__set__".into()).symbol
|
||||||
let assignable_to_meta_attr = if let Symbol::Type(meta_dunder_set, _) =
|
{
|
||||||
meta_attr_ty.class_member(db, "__set__".into()).symbol
|
let successful_call = meta_dunder_set
|
||||||
{
|
.try_call(
|
||||||
let successful_call = meta_dunder_set
|
db,
|
||||||
.try_call(
|
CallArgumentTypes::positional([
|
||||||
db,
|
meta_attr_ty,
|
||||||
CallArgumentTypes::positional([meta_attr_ty, object_ty, value_ty]),
|
object_ty,
|
||||||
)
|
value_ty,
|
||||||
.is_ok();
|
]),
|
||||||
|
)
|
||||||
|
.is_ok();
|
||||||
|
|
||||||
if !successful_call && emit_diagnostics {
|
if !successful_call && emit_diagnostics {
|
||||||
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
|
// TODO: Here, it would be nice to emit an additional diagnostic that explains why the call failed
|
||||||
self.context.report_lint(
|
self.context.report_lint(
|
||||||
&INVALID_ASSIGNMENT,
|
&INVALID_ASSIGNMENT,
|
||||||
target,
|
target,
|
||||||
format_args!(
|
format_args!(
|
||||||
|
|
@ -2418,15 +2423,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
object_ty.display(db)
|
object_ty.display(db)
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
successful_call
|
successful_call
|
||||||
} else {
|
} else {
|
||||||
ensure_assignable_to(meta_attr_ty)
|
ensure_assignable_to(meta_attr_ty)
|
||||||
};
|
};
|
||||||
|
|
||||||
let assignable_to_instance_attribute =
|
let assignable_to_instance_attribute = if meta_attr_boundness
|
||||||
if meta_attr_boundness == Boundness::PossiblyUnbound {
|
== Boundness::PossiblyUnbound
|
||||||
|
{
|
||||||
let (assignable, boundness) =
|
let (assignable, boundness) =
|
||||||
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
|
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
|
||||||
object_ty.instance_member(db, attribute).symbol
|
object_ty.instance_member(db, attribute).symbol
|
||||||
|
|
@ -2453,43 +2459,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
true
|
true
|
||||||
};
|
};
|
||||||
|
|
||||||
assignable_to_meta_attr && assignable_to_instance_attribute
|
assignable_to_meta_attr && assignable_to_instance_attribute
|
||||||
}
|
}
|
||||||
|
|
||||||
SymbolAndQualifiers {
|
SymbolAndQualifiers {
|
||||||
symbol: Symbol::Unbound,
|
symbol: Symbol::Unbound,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
|
if let Symbol::Type(instance_attr_ty, instance_attr_boundness) =
|
||||||
object_ty.instance_member(db, attribute).symbol
|
object_ty.instance_member(db, attribute).symbol
|
||||||
{
|
{
|
||||||
if instance_attr_boundness == Boundness::PossiblyUnbound {
|
if instance_attr_boundness == Boundness::PossiblyUnbound {
|
||||||
report_possibly_unbound_attribute(
|
report_possibly_unbound_attribute(
|
||||||
&self.context,
|
&self.context,
|
||||||
target,
|
target,
|
||||||
attribute,
|
|
||||||
object_ty,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_assignable_to(instance_attr_ty)
|
|
||||||
} else {
|
|
||||||
if emit_diagnostics {
|
|
||||||
self.context.report_lint(
|
|
||||||
&UNRESOLVED_ATTRIBUTE,
|
|
||||||
target,
|
|
||||||
format_args!(
|
|
||||||
"Unresolved attribute `{}` on type `{}`.",
|
|
||||||
attribute,
|
attribute,
|
||||||
object_ty.display(db)
|
object_ty,
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
false
|
ensure_assignable_to(instance_attr_ty)
|
||||||
|
} else {
|
||||||
|
if emit_diagnostics {
|
||||||
|
self.context.report_lint(
|
||||||
|
&UNRESOLVED_ATTRIBUTE,
|
||||||
|
target,
|
||||||
|
format_args!(
|
||||||
|
"Unresolved attribute `{}` on type `{}`.",
|
||||||
|
attribute,
|
||||||
|
object_ty.display(db)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
|
||||||
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
Type::ClassLiteral(..) | Type::SubclassOf(..) => {
|
||||||
match object_ty.class_member(db, attribute.into()) {
|
match object_ty.class_member(db, attribute.into()) {
|
||||||
|
|
@ -3970,8 +3977,48 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
// arguments after matching them to parameters, but before checking that the argument types
|
// arguments after matching them to parameters, but before checking that the argument types
|
||||||
// are assignable to any parameter annotations.
|
// are assignable to any parameter annotations.
|
||||||
let mut call_arguments = Self::parse_arguments(arguments);
|
let mut call_arguments = Self::parse_arguments(arguments);
|
||||||
let function_type = self.infer_expression(func);
|
let callable_type = self.infer_expression(func);
|
||||||
let signatures = function_type.signatures(self.db());
|
|
||||||
|
// For class literals we model the entire class instantiation logic, so it is handled
|
||||||
|
// in a separate function.
|
||||||
|
let class = match callable_type {
|
||||||
|
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||||
|
ClassBase::Dynamic(_) => None,
|
||||||
|
ClassBase::Class(class) => Some(class),
|
||||||
|
},
|
||||||
|
Type::ClassLiteral(ClassLiteralType { class }) => Some(class),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if class.is_some_and(|class| {
|
||||||
|
// For some known classes we have manual signatures defined and use the `try_call` path
|
||||||
|
// below. TODO: it should be possible to move these special cases into the
|
||||||
|
// `try_call_constructor` path instead, or even remove some entirely once we support
|
||||||
|
// overloads fully.
|
||||||
|
class.known(self.db()).is_none_or(|class| {
|
||||||
|
!matches!(
|
||||||
|
class,
|
||||||
|
KnownClass::Bool
|
||||||
|
| KnownClass::Str
|
||||||
|
| KnownClass::Type
|
||||||
|
| KnownClass::Object
|
||||||
|
| KnownClass::Property
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()];
|
||||||
|
let call_argument_types =
|
||||||
|
self.infer_argument_types(arguments, call_arguments, &argument_forms);
|
||||||
|
|
||||||
|
return callable_type
|
||||||
|
.try_call_constructor(self.db(), call_argument_types)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(&self.context, callable_type, call_expression.into());
|
||||||
|
err.return_type()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let signatures = callable_type.signatures(self.db());
|
||||||
let bindings = Bindings::match_parameters(signatures, &mut call_arguments);
|
let bindings = Bindings::match_parameters(signatures, &mut call_arguments);
|
||||||
let mut call_argument_types =
|
let mut call_argument_types =
|
||||||
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
|
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::symbol::SymbolAndQualifiers;
|
use crate::symbol::SymbolAndQualifiers;
|
||||||
|
|
||||||
use super::{ClassBase, ClassLiteralType, Db, KnownClass, Type};
|
use super::{ClassBase, ClassLiteralType, Db, KnownClass, MemberLookupPolicy, Type};
|
||||||
|
|
||||||
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
|
/// A type that represents `type[C]`, i.e. the class object `C` and class objects that are subclasses of `C`.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
|
||||||
|
|
@ -66,12 +66,13 @@ impl<'db> SubclassOfType<'db> {
|
||||||
!self.is_dynamic()
|
!self.is_dynamic()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn find_name_in_mro(
|
pub(crate) fn find_name_in_mro_with_policy(
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
) -> Option<SymbolAndQualifiers<'db>> {
|
) -> Option<SymbolAndQualifiers<'db>> {
|
||||||
Type::from(self.subclass_of).find_name_in_mro(db, name)
|
Type::from(self.subclass_of).find_name_in_mro_with_policy(db, name, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if `self` is a subtype of `other`.
|
/// Return `true` if `self` is a subtype of `other`.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue