mirror of https://github.com/astral-sh/ruff
[red-knot] Support `super` (#17174)
## Summary closes #16615 This PR includes: - Introduces a new type: `Type::BoundSuper` - Implements member lookup for `Type::BoundSuper`, resolving attributes by traversing the MRO starting from the specified class - Adds support for inferring appropriate arguments (`pivot_class` and `owner`) for `super()` when it is used without arguments When `super(..)` appears in code, it can be inferred into one of the following: - `Type::Unknown`: when a runtime error would occur (e.g. calling `super()` out of method scope, or when parameter validation inside `super` fails) - `KnownClass::Super::to_instance()`: when the result is an *unbound super object* or when a dynamic type is used as parameters (MRO traversing is meaningless) - `Type::BoundSuper`: the common case, representing a properly constructed `super` instance that is ready for MRO traversal and attribute resolution ### Terminology Python defines the terms *bound super object* and *unbound super object*. An **unbound super object** is created when `super` is called with only one argument (e.g. `super(A)`). This object may later be bound via the `super.__get__` method. However, this form is rarely used in practice. A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the implicit form `super()`, where both arguments are inferred from the context. This is the most common usage. ### Follow-ups - Add diagnostics for `super()` calls that would result in runtime errors (marked as TODO) - Add property tests for `Type::BoundSuper` ## Test Plan - Added `mdtest/class/super.md` --------- Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
parent
1a79722ee0
commit
649610cc98
|
|
@ -1870,20 +1870,6 @@ reveal_type(Foo.BAR.value) # revealed: @Todo(Attribute access on enum classes)
|
||||||
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
reveal_type(Foo.__members__) # revealed: @Todo(Attribute access on enum classes)
|
||||||
```
|
```
|
||||||
|
|
||||||
## `super()`
|
|
||||||
|
|
||||||
`super()` is not supported yet, but we do not emit false positives on `super()` calls.
|
|
||||||
|
|
||||||
```py
|
|
||||||
class Foo:
|
|
||||||
def bar(self) -> int:
|
|
||||||
return 42
|
|
||||||
|
|
||||||
class Bar(Foo):
|
|
||||||
def bar(self) -> int:
|
|
||||||
return super().bar()
|
|
||||||
```
|
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,400 @@
|
||||||
|
# Super
|
||||||
|
|
||||||
|
Python defines the terms *bound super object* and *unbound super object*.
|
||||||
|
|
||||||
|
An **unbound super object** is created when `super` is called with only one argument. (e.g.
|
||||||
|
`super(A)`). This object may later be bound using the `super.__get__` method. However, this form is
|
||||||
|
rarely used in practice.
|
||||||
|
|
||||||
|
A **bound super object** is created either by calling `super(pivot_class, owner)` or by using the
|
||||||
|
implicit form `super()`, where both the pivot class and the owner are inferred. This is the most
|
||||||
|
common usage.
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
### Explicit Super Object
|
||||||
|
|
||||||
|
`super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the
|
||||||
|
specified pivot class.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def a(self): ...
|
||||||
|
aa: int = 1
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def b(self): ...
|
||||||
|
bb: int = 2
|
||||||
|
|
||||||
|
class C(B):
|
||||||
|
def c(self): ...
|
||||||
|
cc: int = 3
|
||||||
|
|
||||||
|
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||||
|
|
||||||
|
super(C, C()).a
|
||||||
|
super(C, C()).b
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[C], C>` has no attribute `c`"
|
||||||
|
super(C, C()).c
|
||||||
|
|
||||||
|
super(B, C()).a
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `b`"
|
||||||
|
super(B, C()).b
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[B], C>` has no attribute `c`"
|
||||||
|
super(B, C()).c
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `a`"
|
||||||
|
super(A, C()).a
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `b`"
|
||||||
|
super(A, C()).b
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[A], C>` has no attribute `c`"
|
||||||
|
super(A, C()).c
|
||||||
|
|
||||||
|
reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown
|
||||||
|
reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown
|
||||||
|
reveal_type(super(C, C()).aa) # revealed: int
|
||||||
|
reveal_type(super(C, C()).bb) # revealed: int
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implicit Super Object
|
||||||
|
|
||||||
|
The implicit form `super()` is same as `super(__class__, <first argument>)`. The `__class__` refers
|
||||||
|
to the class that contains the function where `super()` is used. The first argument refers to the
|
||||||
|
current method’s first parameter (typically `self` or `cls`).
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __init__(self, a: int): ...
|
||||||
|
@classmethod
|
||||||
|
def f(cls): ...
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def __init__(self, a: int):
|
||||||
|
# TODO: Once `Self` is supported, this should be `<super: Literal[B], B>`
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
super().__init__(a)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def f(cls):
|
||||||
|
# TODO: Once `Self` is supported, this should be `<super: Literal[B], Literal[B]>`
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
super().f()
|
||||||
|
|
||||||
|
super(B, B(42)).__init__(42)
|
||||||
|
super(B, B).f()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unbound Super Object
|
||||||
|
|
||||||
|
Calling `super(cls)` without a second argument returns an *unbound super object*. This is treated as
|
||||||
|
a plain `super` instance and does not support name lookup via the MRO.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
a: int = 42
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
reveal_type(super(B)) # revealed: super
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `super` has no attribute `a`"
|
||||||
|
super(B).a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Attribute Assignment
|
||||||
|
|
||||||
|
`super()` objects do not allow attribute assignment — even if the attribute is resolved
|
||||||
|
successfully.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
a: int = 3
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
reveal_type(super(B, B()).a) # revealed: int
|
||||||
|
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `<super: Literal[B], B>`"
|
||||||
|
super(B, B()).a = 3
|
||||||
|
# error: [invalid-assignment] "Cannot assign to attribute `a` on type `super`"
|
||||||
|
super(B).a = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Types
|
||||||
|
|
||||||
|
If any of the arguments is dynamic, we cannot determine the MRO to traverse. When accessing a
|
||||||
|
member, it should effectively behave like a dynamic type.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
a: int = 1
|
||||||
|
|
||||||
|
def f(x):
|
||||||
|
reveal_type(x) # revealed: Unknown
|
||||||
|
|
||||||
|
reveal_type(super(x, x)) # revealed: <super: Unknown, Unknown>
|
||||||
|
reveal_type(super(A, x)) # revealed: <super: Literal[A], Unknown>
|
||||||
|
reveal_type(super(x, A())) # revealed: <super: Unknown, A>
|
||||||
|
|
||||||
|
reveal_type(super(x, x).a) # revealed: Unknown
|
||||||
|
reveal_type(super(A, x).a) # revealed: Unknown
|
||||||
|
reveal_type(super(x, A()).a) # revealed: Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implicit `super()` in Complex Structure
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def test(self):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[A], Unknown>
|
||||||
|
|
||||||
|
class B:
|
||||||
|
def test(self):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
|
||||||
|
class C(A.B):
|
||||||
|
def test(self):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[C], Unknown>
|
||||||
|
|
||||||
|
def inner(t: C):
|
||||||
|
reveal_type(super()) # revealed: <super: Literal[B], C>
|
||||||
|
lambda x: reveal_type(super()) # revealed: <super: Literal[B], Unknown>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built-ins and Literals
|
||||||
|
|
||||||
|
```py
|
||||||
|
reveal_type(super(bool, True)) # revealed: <super: Literal[bool], bool>
|
||||||
|
reveal_type(super(bool, bool())) # revealed: <super: Literal[bool], bool>
|
||||||
|
reveal_type(super(int, bool())) # revealed: <super: Literal[int], bool>
|
||||||
|
reveal_type(super(int, 3)) # revealed: <super: Literal[int], int>
|
||||||
|
reveal_type(super(str, "")) # revealed: <super: Literal[str], str>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Descriptor Behavior with Super
|
||||||
|
|
||||||
|
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
|
||||||
|
differ depending on whether the second argument to `super` is a class or an instance.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def a1(self): ...
|
||||||
|
@classmethod
|
||||||
|
def a2(cls): ...
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
# A.__dict__["a1"].__get__(B(), B)
|
||||||
|
reveal_type(super(B, B()).a1) # revealed: bound method B.a1() -> Unknown
|
||||||
|
# A.__dict__["a2"].__get__(B(), B)
|
||||||
|
reveal_type(super(B, B()).a2) # revealed: bound method type[B].a2() -> Unknown
|
||||||
|
|
||||||
|
# A.__dict__["a1"].__get__(None, B)
|
||||||
|
reveal_type(super(B, B).a1) # revealed: def a1(self) -> Unknown
|
||||||
|
# A.__dict__["a2"].__get__(None, B)
|
||||||
|
reveal_type(super(B, B).a2) # revealed: bound method Literal[B].a2() -> Unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Union of Supers
|
||||||
|
|
||||||
|
When the owner is a union type, `super()` is built separately for each branch, and the resulting
|
||||||
|
super objects are combined into a union.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A: ...
|
||||||
|
|
||||||
|
class B:
|
||||||
|
b: int = 42
|
||||||
|
|
||||||
|
class C(A, B): ...
|
||||||
|
class D(B, A): ...
|
||||||
|
|
||||||
|
def f(x: C | D):
|
||||||
|
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||||
|
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[B], Literal[A], Literal[object]]
|
||||||
|
|
||||||
|
s = super(A, x)
|
||||||
|
reveal_type(s) # revealed: <super: Literal[A], C> | <super: Literal[A], D>
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `b` on type `<super: Literal[A], C> | <super: Literal[A], D>` is possibly unbound"
|
||||||
|
s.b
|
||||||
|
|
||||||
|
def f(flag: bool):
|
||||||
|
x = str() if flag else str("hello")
|
||||||
|
reveal_type(x) # revealed: Literal["", "hello"]
|
||||||
|
reveal_type(super(str, x)) # revealed: <super: Literal[str], str>
|
||||||
|
|
||||||
|
def f(x: int | str):
|
||||||
|
# error: [invalid-super-argument] "`str` is not an instance or subclass of `Literal[int]` in `super(Literal[int], str)` call"
|
||||||
|
super(int, x)
|
||||||
|
```
|
||||||
|
|
||||||
|
Even when `super()` is constructed separately for each branch of a union, it should behave correctly
|
||||||
|
in all cases.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f(flag: bool):
|
||||||
|
if flag:
|
||||||
|
class A:
|
||||||
|
x = 1
|
||||||
|
y: int = 1
|
||||||
|
|
||||||
|
a: str = "hello"
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
s = super(B, B())
|
||||||
|
else:
|
||||||
|
class C:
|
||||||
|
x = 2
|
||||||
|
y: int | str = "test"
|
||||||
|
|
||||||
|
class D(C): ...
|
||||||
|
s = super(D, D())
|
||||||
|
|
||||||
|
reveal_type(s) # revealed: <super: Literal[B], B> | <super: Literal[D], D>
|
||||||
|
|
||||||
|
reveal_type(s.x) # revealed: Unknown | Literal[1, 2]
|
||||||
|
reveal_type(s.y) # revealed: int | str
|
||||||
|
|
||||||
|
# error: [possibly-unbound-attribute] "Attribute `a` on type `<super: Literal[B], B> | <super: Literal[D], D>` is possibly unbound"
|
||||||
|
reveal_type(s.a) # revealed: str
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supers with Generic Classes
|
||||||
|
|
||||||
|
```py
|
||||||
|
from knot_extensions import TypeOf, static_assert, is_subtype_of
|
||||||
|
|
||||||
|
class A[T]:
|
||||||
|
def f(self, a: T) -> T:
|
||||||
|
return a
|
||||||
|
|
||||||
|
class B[T](A[T]):
|
||||||
|
def f(self, b: T) -> T:
|
||||||
|
return super().f(b)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Invalid Usages
|
||||||
|
|
||||||
|
### Unresolvable `super()` Calls
|
||||||
|
|
||||||
|
If an appropriate class and argument cannot be found, a runtime error will occur.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
reveal_type(super()) # revealed: Unknown
|
||||||
|
|
||||||
|
def f():
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
super()
|
||||||
|
|
||||||
|
# No first argument in its scope
|
||||||
|
class A:
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
s = super()
|
||||||
|
|
||||||
|
def f(self):
|
||||||
|
def g():
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
super()
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
lambda: super()
|
||||||
|
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
(super() for _ in range(10))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def h():
|
||||||
|
# error: [unavailable-implicit-super-arguments] "Cannot determine implicit arguments for 'super()' in this context"
|
||||||
|
super()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Failing Condition Checks
|
||||||
|
|
||||||
|
`super()` requires its first argument to be a valid class, and its second argument to be either an
|
||||||
|
instance or a subclass of the first. If either condition is violated, a `TypeError` is raised at
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def f(x: int):
|
||||||
|
# error: [invalid-super-argument] "`int` is not a valid class"
|
||||||
|
super(x, x)
|
||||||
|
|
||||||
|
type IntAlias = int
|
||||||
|
# error: [invalid-super-argument] "`typing.TypeAliasType` is not a valid class"
|
||||||
|
super(IntAlias, 0)
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[""]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[""])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(int, str()))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[str]` is not an instance or subclass of `Literal[int]` in `super(Literal[int], Literal[str])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(int, str))
|
||||||
|
|
||||||
|
class A: ...
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`A` is not an instance or subclass of `Literal[B]` in `super(Literal[B], A)` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, A()))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`object` is not an instance or subclass of `Literal[B]` in `super(Literal[B], object)` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, object()))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[A]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[A])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, A))
|
||||||
|
|
||||||
|
# error: [invalid-super-argument] "`Literal[object]` is not an instance or subclass of `Literal[B]` in `super(Literal[B], Literal[object])` call"
|
||||||
|
# revealed: Unknown
|
||||||
|
reveal_type(super(B, object))
|
||||||
|
|
||||||
|
super(object, object()).__class__
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Member Access via `super`
|
||||||
|
|
||||||
|
Accessing instance members through `super()` is not allowed.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
class A:
|
||||||
|
def __init__(self, a: int):
|
||||||
|
self.a = a
|
||||||
|
|
||||||
|
class B(A):
|
||||||
|
def __init__(self, a: int):
|
||||||
|
super().__init__(a)
|
||||||
|
# TODO: Once `Self` is supported, this should raise `unresolved-attribute` error
|
||||||
|
super().a
|
||||||
|
|
||||||
|
# error: [unresolved-attribute] "Type `<super: Literal[B], B>` has no attribute `a`"
|
||||||
|
super(B, B(42)).a
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dunder Method Resolution
|
||||||
|
|
||||||
|
Dunder methods defined in the `owner` (from `super(pivot_class, owner)`) should not affect the super
|
||||||
|
object itself. In other words, `super` should not be treated as if it inherits attributes of the
|
||||||
|
`owner`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class A:
|
||||||
|
def __getitem__(self, key: int) -> int:
|
||||||
|
return 42
|
||||||
|
|
||||||
|
class B(A): ...
|
||||||
|
|
||||||
|
reveal_type(A()[0]) # revealed: int
|
||||||
|
reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(key: int) -> int
|
||||||
|
# error: [non-subscriptable] "Cannot subscript object of type `<super: Literal[B], B>` with no `__getitem__` method"
|
||||||
|
super(B, B())[0]
|
||||||
|
```
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
|
use itertools::Either;
|
||||||
|
|
||||||
use std::slice::Iter;
|
use std::slice::Iter;
|
||||||
use std::str::FromStr;
|
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::{CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, NOT_ITERABLE};
|
use diagnostic::{
|
||||||
|
CALL_POSSIBLY_UNBOUND_METHOD, INVALID_CONTEXT_MANAGER, INVALID_SUPER_ARGUMENT, NOT_ITERABLE,
|
||||||
|
UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS,
|
||||||
|
};
|
||||||
use ruff_db::files::{File, FileRange};
|
use ruff_db::files::{File, FileRange};
|
||||||
use ruff_python_ast as ast;
|
|
||||||
use ruff_python_ast::name::Name;
|
use ruff_python_ast::name::Name;
|
||||||
|
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
use type_ordering::union_or_intersection_elements_ordering;
|
use type_ordering::union_or_intersection_elements_ordering;
|
||||||
|
|
||||||
|
|
@ -422,6 +427,10 @@ pub enum Type<'db> {
|
||||||
/// An instance of a typevar in a generic class or function. When the generic class or function
|
/// An instance of a typevar in a generic class or function. When the generic class or function
|
||||||
/// is specialized, we will replace this typevar with its specialization.
|
/// is specialized, we will replace this typevar with its specialization.
|
||||||
TypeVar(TypeVarInstance<'db>),
|
TypeVar(TypeVarInstance<'db>),
|
||||||
|
// A bound super object like `super()` or `super(A, A())`
|
||||||
|
// This type doesn't handle an unbound super object like `super(A)`; for that we just use
|
||||||
|
// a `Type::Instance` of `builtins.super`.
|
||||||
|
BoundSuper(BoundSuperType<'db>),
|
||||||
// TODO protocols, overloads, generics
|
// TODO protocols, overloads, generics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -521,6 +530,16 @@ impl<'db> Type<'db> {
|
||||||
.any(|constraint| constraint.contains_todo(db)),
|
.any(|constraint| constraint.contains_todo(db)),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
Self::BoundSuper(bound_super) => {
|
||||||
|
matches!(
|
||||||
|
bound_super.pivot_class(db),
|
||||||
|
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol)
|
||||||
|
) || matches!(
|
||||||
|
bound_super.owner(db),
|
||||||
|
SuperOwnerKind::Dynamic(DynamicType::Todo(_) | DynamicType::TodoProtocol)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Self::Tuple(tuple) => tuple.elements(db).iter().any(|ty| ty.contains_todo(db)),
|
Self::Tuple(tuple) => tuple.elements(db).iter().any(|ty| ty.contains_todo(db)),
|
||||||
|
|
||||||
Self::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)),
|
Self::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)),
|
||||||
|
|
@ -783,6 +802,7 @@ impl<'db> Type<'db> {
|
||||||
| Type::ClassLiteral(_)
|
| Type::ClassLiteral(_)
|
||||||
| Type::KnownInstance(_)
|
| Type::KnownInstance(_)
|
||||||
| Type::IntLiteral(_)
|
| Type::IntLiteral(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::SubclassOf(_) => self,
|
| Type::SubclassOf(_) => self,
|
||||||
Type::GenericAlias(generic) => {
|
Type::GenericAlias(generic) => {
|
||||||
let specialization = generic.specialization(db).normalized(db);
|
let specialization = generic.specialization(db).normalized(db);
|
||||||
|
|
@ -1053,6 +1073,9 @@ impl<'db> Type<'db> {
|
||||||
// as that type is equivalent to `type[Any, ...]` (and therefore not a fully static type).
|
// as that type is equivalent to `type[Any, ...]` (and therefore not a fully static type).
|
||||||
(Type::Tuple(_), _) => KnownClass::Tuple.to_instance(db).is_subtype_of(db, target),
|
(Type::Tuple(_), _) => KnownClass::Tuple.to_instance(db).is_subtype_of(db, target),
|
||||||
|
|
||||||
|
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
|
||||||
|
(Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target),
|
||||||
|
|
||||||
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`,
|
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`,
|
||||||
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
|
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
|
||||||
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||||
|
|
@ -1805,6 +1828,11 @@ impl<'db> Type<'db> {
|
||||||
(Type::PropertyInstance(_), _) | (_, Type::PropertyInstance(_)) => KnownClass::Property
|
(Type::PropertyInstance(_), _) | (_, Type::PropertyInstance(_)) => KnownClass::Property
|
||||||
.to_instance(db)
|
.to_instance(db)
|
||||||
.is_disjoint_from(db, other),
|
.is_disjoint_from(db, other),
|
||||||
|
|
||||||
|
(Type::BoundSuper(_), Type::BoundSuper(_)) => !self.is_equivalent_to(db, other),
|
||||||
|
(Type::BoundSuper(_), other) | (other, Type::BoundSuper(_)) => KnownClass::Super
|
||||||
|
.to_instance(db)
|
||||||
|
.is_disjoint_from(db, other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1840,6 +1868,10 @@ impl<'db> Type<'db> {
|
||||||
},
|
},
|
||||||
|
|
||||||
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(),
|
Type::SubclassOf(subclass_of_ty) => subclass_of_ty.is_fully_static(),
|
||||||
|
Type::BoundSuper(bound_super) => {
|
||||||
|
!matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_))
|
||||||
|
&& !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_))
|
||||||
|
}
|
||||||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => {
|
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => {
|
||||||
// TODO: Ideally, we would iterate over the MRO of the class, check if all
|
// TODO: Ideally, we would iterate over the MRO of the class, check if all
|
||||||
// bases are fully static, and only return `true` if that is the case.
|
// bases are fully static, and only return `true` if that is the case.
|
||||||
|
|
@ -1907,6 +1939,7 @@ impl<'db> Type<'db> {
|
||||||
|
|
||||||
// We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton.
|
// We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton.
|
||||||
Type::SubclassOf(..) => false,
|
Type::SubclassOf(..) => false,
|
||||||
|
Type::BoundSuper(..) => false,
|
||||||
Type::BooleanLiteral(_)
|
Type::BooleanLiteral(_)
|
||||||
| Type::FunctionLiteral(..)
|
| Type::FunctionLiteral(..)
|
||||||
| Type::WrapperDescriptor(..)
|
| Type::WrapperDescriptor(..)
|
||||||
|
|
@ -2015,6 +2048,11 @@ impl<'db> Type<'db> {
|
||||||
class.known(db).is_some_and(KnownClass::is_single_valued)
|
class.known(db).is_some_and(KnownClass::is_single_valued)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type::BoundSuper(_) => {
|
||||||
|
// At runtime two super instances never compare equal, even if their arguments are identical.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
Type::Dynamic(_)
|
Type::Dynamic(_)
|
||||||
| Type::Never
|
| Type::Never
|
||||||
| Type::Union(..)
|
| Type::Union(..)
|
||||||
|
|
@ -2139,6 +2177,12 @@ impl<'db> Type<'db> {
|
||||||
subclass_of_ty.find_name_in_mro_with_policy(db, name, policy)
|
subclass_of_ty.find_name_in_mro_with_policy(db, name, policy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: `super(pivot, owner).__class__` is `builtins.super`, not the owner's class.
|
||||||
|
// `BoundSuper` should look up the name in the MRO of `builtins.super`.
|
||||||
|
Type::BoundSuper(_) => KnownClass::Super
|
||||||
|
.to_class_literal(db)
|
||||||
|
.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
|
||||||
// MRO of the class `object`.
|
// MRO of the class `object`.
|
||||||
|
|
@ -2282,6 +2326,13 @@ impl<'db> Type<'db> {
|
||||||
.to_instance(db)
|
.to_instance(db)
|
||||||
.instance_member(db, name),
|
.instance_member(db, name),
|
||||||
|
|
||||||
|
// Note: `super(pivot, owner).__dict__` refers to the `__dict__` of the `builtins.super` instance,
|
||||||
|
// not that of the owner.
|
||||||
|
// This means we should only look up instance members defined on the `builtins.super()` instance itself.
|
||||||
|
// If you want to look up a member in the MRO of the `super`'s owner,
|
||||||
|
// refer to [`Type::member`] instead.
|
||||||
|
Type::BoundSuper(_) => KnownClass::Super.to_instance(db).instance_member(db, name),
|
||||||
|
|
||||||
// TODO: we currently don't model the fact that class literals and subclass-of types have
|
// TODO: we currently don't model the fact that class literals and subclass-of types have
|
||||||
// a `__dict__` that is filled with class level attributes. Modeling this is currently not
|
// a `__dict__` that is filled with class level attributes. Modeling this is currently not
|
||||||
// required, as `instance_member` is only called for instance-like types through `member`,
|
// required, as `instance_member` is only called for instance-like types through `member`,
|
||||||
|
|
@ -2676,10 +2727,6 @@ impl<'db> Type<'db> {
|
||||||
Symbol::bound(Type::IntLiteral(segment.into())).into()
|
Symbol::bound(Type::IntLiteral(segment.into())).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
Type::Instance(InstanceType { class }) if class.is_known(db, KnownClass::Super) => {
|
|
||||||
SymbolAndQualifiers::todo("super() support")
|
|
||||||
}
|
|
||||||
|
|
||||||
Type::PropertyInstance(property) if name == "fget" => {
|
Type::PropertyInstance(property) if name == "fget" => {
|
||||||
Symbol::bound(property.getter(db).unwrap_or(Type::none(db))).into()
|
Symbol::bound(property.getter(db).unwrap_or(Type::none(db))).into()
|
||||||
}
|
}
|
||||||
|
|
@ -2804,6 +2851,19 @@ impl<'db> Type<'db> {
|
||||||
policy,
|
policy,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unlike other objects, `super` has a unique member lookup behavior.
|
||||||
|
// It's simpler than other objects:
|
||||||
|
//
|
||||||
|
// 1. Search for the attribute in the MRO, starting just after the pivot class.
|
||||||
|
// 2. If the attribute is a descriptor, invoke its `__get__` method.
|
||||||
|
Type::BoundSuper(bound_super) => {
|
||||||
|
let owner_attr = bound_super.find_name_in_mro_after_pivot(db, name_str, policy);
|
||||||
|
|
||||||
|
bound_super
|
||||||
|
.try_call_dunder_get_on_attribute(db, owner_attr.clone())
|
||||||
|
.unwrap_or(owner_attr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3007,6 +3067,7 @@ impl<'db> Type<'db> {
|
||||||
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
|
Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()),
|
||||||
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
|
Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
|
||||||
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
|
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
|
||||||
|
Type::BoundSuper(_) => Truthiness::AlwaysTrue,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(truthiness)
|
Ok(truthiness)
|
||||||
|
|
@ -3552,6 +3613,44 @@ impl<'db> Type<'db> {
|
||||||
Signatures::single(signature)
|
Signatures::single(signature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some(KnownClass::Super) => {
|
||||||
|
// ```py
|
||||||
|
// class super:
|
||||||
|
// @overload
|
||||||
|
// def __init__(self, t: Any, obj: Any, /) -> None: ...
|
||||||
|
// @overload
|
||||||
|
// def __init__(self, t: Any, /) -> None: ...
|
||||||
|
// @overload
|
||||||
|
// def __init__(self) -> None: ...
|
||||||
|
// ```
|
||||||
|
let signature = CallableSignature::from_overloads(
|
||||||
|
self,
|
||||||
|
[
|
||||||
|
Signature::new(
|
||||||
|
Parameters::new([
|
||||||
|
Parameter::positional_only(Some(Name::new_static("t")))
|
||||||
|
.with_annotated_type(Type::any()),
|
||||||
|
Parameter::positional_only(Some(Name::new_static("obj")))
|
||||||
|
.with_annotated_type(Type::any()),
|
||||||
|
]),
|
||||||
|
Some(KnownClass::Super.to_instance(db)),
|
||||||
|
),
|
||||||
|
Signature::new(
|
||||||
|
Parameters::new([Parameter::positional_only(Some(
|
||||||
|
Name::new_static("t"),
|
||||||
|
))
|
||||||
|
.with_annotated_type(Type::any())]),
|
||||||
|
Some(KnownClass::Super.to_instance(db)),
|
||||||
|
),
|
||||||
|
Signature::new(
|
||||||
|
Parameters::empty(),
|
||||||
|
Some(KnownClass::Super.to_instance(db)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
Signatures::single(signature)
|
||||||
|
}
|
||||||
|
|
||||||
Some(KnownClass::Property) => {
|
Some(KnownClass::Property) => {
|
||||||
let getter_signature = Signature::new(
|
let getter_signature = Signature::new(
|
||||||
Parameters::new([
|
Parameters::new([
|
||||||
|
|
@ -4057,6 +4156,7 @@ impl<'db> Type<'db> {
|
||||||
| Type::Tuple(_)
|
| Type::Tuple(_)
|
||||||
| Type::TypeVar(_)
|
| Type::TypeVar(_)
|
||||||
| Type::LiteralString
|
| Type::LiteralString
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::AlwaysTruthy
|
| Type::AlwaysTruthy
|
||||||
| Type::AlwaysFalsy => None,
|
| Type::AlwaysFalsy => None,
|
||||||
}
|
}
|
||||||
|
|
@ -4118,6 +4218,7 @@ impl<'db> Type<'db> {
|
||||||
| Type::DataclassDecorator(_)
|
| Type::DataclassDecorator(_)
|
||||||
| Type::Never
|
| Type::Never
|
||||||
| Type::FunctionLiteral(_)
|
| Type::FunctionLiteral(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::PropertyInstance(_) => Err(InvalidTypeExpressionError {
|
| Type::PropertyInstance(_) => Err(InvalidTypeExpressionError {
|
||||||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)],
|
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)],
|
||||||
fallback_type: Type::unknown(),
|
fallback_type: Type::unknown(),
|
||||||
|
|
@ -4360,6 +4461,7 @@ impl<'db> Type<'db> {
|
||||||
.expect("Type::Todo should be a valid ClassBase"),
|
.expect("Type::Todo should be a valid ClassBase"),
|
||||||
),
|
),
|
||||||
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
||||||
|
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4479,6 +4581,7 @@ impl<'db> Type<'db> {
|
||||||
| Type::StringLiteral(_)
|
| Type::StringLiteral(_)
|
||||||
| Type::BytesLiteral(_)
|
| Type::BytesLiteral(_)
|
||||||
| Type::SliceLiteral(_)
|
| Type::SliceLiteral(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
// Instance contains a ClassType, which has already been specialized if needed, like
|
// Instance contains a ClassType, which has already been specialized if needed, like
|
||||||
// above with BoundMethod's self_instance.
|
// above with BoundMethod's self_instance.
|
||||||
| Type::Instance(_)
|
| Type::Instance(_)
|
||||||
|
|
@ -4571,6 +4674,7 @@ impl<'db> Type<'db> {
|
||||||
| Self::WrapperDescriptor(_)
|
| Self::WrapperDescriptor(_)
|
||||||
| Self::DataclassDecorator(_)
|
| Self::DataclassDecorator(_)
|
||||||
| Self::PropertyInstance(_)
|
| Self::PropertyInstance(_)
|
||||||
|
| Self::BoundSuper(_)
|
||||||
| Self::Tuple(_) => self.to_meta_type(db).definition(db),
|
| Self::Tuple(_) => self.to_meta_type(db).definition(db),
|
||||||
|
|
||||||
Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))),
|
Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))),
|
||||||
|
|
@ -6552,6 +6656,287 @@ impl<'db> TupleType<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub(crate) enum BoundSuperError<'db> {
|
||||||
|
InvalidPivotClassType {
|
||||||
|
pivot_class: Type<'db>,
|
||||||
|
},
|
||||||
|
FailingConditionCheck {
|
||||||
|
pivot_class: Type<'db>,
|
||||||
|
owner: Type<'db>,
|
||||||
|
},
|
||||||
|
UnavailableImplicitArguments,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundSuperError<'_> {
|
||||||
|
pub(super) fn report_diagnostic(&self, context: &InferContext, node: AnyNodeRef) {
|
||||||
|
match self {
|
||||||
|
BoundSuperError::InvalidPivotClassType { pivot_class } => {
|
||||||
|
context.report_lint_old(
|
||||||
|
&INVALID_SUPER_ARGUMENT,
|
||||||
|
node,
|
||||||
|
format_args!(
|
||||||
|
"`{pivot_class}` is not a valid class",
|
||||||
|
pivot_class = pivot_class.display(context.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
BoundSuperError::FailingConditionCheck { pivot_class, owner } => {
|
||||||
|
context.report_lint_old(
|
||||||
|
&INVALID_SUPER_ARGUMENT,
|
||||||
|
node,
|
||||||
|
format_args!(
|
||||||
|
"`{owner}` is not an instance or subclass of `{pivot_class}` in `super({pivot_class}, {owner})` call",
|
||||||
|
pivot_class = pivot_class.display(context.db()),
|
||||||
|
owner = owner.display(context.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
BoundSuperError::UnavailableImplicitArguments => {
|
||||||
|
context.report_lint_old(
|
||||||
|
&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS,
|
||||||
|
node,
|
||||||
|
format_args!(
|
||||||
|
"Cannot determine implicit arguments for 'super()' in this context",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub enum SuperOwnerKind<'db> {
|
||||||
|
Dynamic(DynamicType),
|
||||||
|
Class(ClassType<'db>),
|
||||||
|
Instance(InstanceType<'db>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> SuperOwnerKind<'db> {
|
||||||
|
fn iter_mro(self, db: &'db dyn Db) -> impl Iterator<Item = ClassBase<'db>> {
|
||||||
|
match self {
|
||||||
|
SuperOwnerKind::Dynamic(dynamic) => Either::Left(ClassBase::Dynamic(dynamic).mro(db)),
|
||||||
|
SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)),
|
||||||
|
SuperOwnerKind::Instance(instance) => Either::Right(instance.class.iter_mro(db)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_type(self) -> Type<'db> {
|
||||||
|
match self {
|
||||||
|
SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic),
|
||||||
|
SuperOwnerKind::Class(class) => class.into(),
|
||||||
|
SuperOwnerKind::Instance(instance) => instance.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn into_class(self) -> Option<ClassType<'db>> {
|
||||||
|
match self {
|
||||||
|
SuperOwnerKind::Dynamic(_) => None,
|
||||||
|
SuperOwnerKind::Class(class) => Some(class),
|
||||||
|
SuperOwnerKind::Instance(instance) => Some(instance.class),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_type(db: &'db dyn Db, ty: Type<'db>) -> Option<Self> {
|
||||||
|
match ty {
|
||||||
|
Type::Dynamic(dynamic) => Some(SuperOwnerKind::Dynamic(dynamic)),
|
||||||
|
Type::ClassLiteral(class_literal) => Some(SuperOwnerKind::Class(
|
||||||
|
class_literal.apply_optional_specialization(db, None),
|
||||||
|
)),
|
||||||
|
Type::Instance(instance) => Some(SuperOwnerKind::Instance(instance)),
|
||||||
|
Type::BooleanLiteral(_) => {
|
||||||
|
SuperOwnerKind::try_from_type(db, KnownClass::Bool.to_instance(db))
|
||||||
|
}
|
||||||
|
Type::IntLiteral(_) => {
|
||||||
|
SuperOwnerKind::try_from_type(db, KnownClass::Int.to_instance(db))
|
||||||
|
}
|
||||||
|
Type::StringLiteral(_) => {
|
||||||
|
SuperOwnerKind::try_from_type(db, KnownClass::Str.to_instance(db))
|
||||||
|
}
|
||||||
|
Type::LiteralString => {
|
||||||
|
SuperOwnerKind::try_from_type(db, KnownClass::Str.to_instance(db))
|
||||||
|
}
|
||||||
|
Type::BytesLiteral(_) => {
|
||||||
|
SuperOwnerKind::try_from_type(db, KnownClass::Bytes.to_instance(db))
|
||||||
|
}
|
||||||
|
Type::KnownInstance(known_instance) => {
|
||||||
|
SuperOwnerKind::try_from_type(db, known_instance.instance_fallback(db))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represent a bound super object like `super(PivotClass, owner)`
|
||||||
|
#[salsa::interned(debug)]
|
||||||
|
pub struct BoundSuperType<'db> {
|
||||||
|
#[return_ref]
|
||||||
|
pub pivot_class: ClassBase<'db>,
|
||||||
|
#[return_ref]
|
||||||
|
pub owner: SuperOwnerKind<'db>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'db> BoundSuperType<'db> {
|
||||||
|
/// Attempts to build a `Type::BoundSuper` based on the given `pivot_class` and `owner`.
|
||||||
|
///
|
||||||
|
/// This mimics the behavior of Python's built-in `super(pivot, owner)` at runtime.
|
||||||
|
/// - `super(pivot, owner_class)` is valid only if `issubclass(owner_class, pivot)`
|
||||||
|
/// - `super(pivot, owner_instance)` is valid only if `isinstance(owner_instance, pivot)`
|
||||||
|
///
|
||||||
|
/// However, the checking is skipped when any of the arguments is a dynamic type.
|
||||||
|
fn build(
|
||||||
|
db: &'db dyn Db,
|
||||||
|
pivot_class_type: Type<'db>,
|
||||||
|
owner_type: Type<'db>,
|
||||||
|
) -> Result<Type<'db>, BoundSuperError<'db>> {
|
||||||
|
if let Type::Union(union) = owner_type {
|
||||||
|
return Ok(UnionType::from_elements(
|
||||||
|
db,
|
||||||
|
union
|
||||||
|
.elements(db)
|
||||||
|
.iter()
|
||||||
|
.map(|ty| BoundSuperType::build(db, pivot_class_type, *ty))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pivot_class = ClassBase::try_from_type(db, pivot_class_type).ok_or({
|
||||||
|
BoundSuperError::InvalidPivotClassType {
|
||||||
|
pivot_class: pivot_class_type,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let owner = SuperOwnerKind::try_from_type(db, owner_type)
|
||||||
|
.and_then(|owner| {
|
||||||
|
let Some(pivot_class) = pivot_class.into_class() else {
|
||||||
|
return Some(owner);
|
||||||
|
};
|
||||||
|
let Some(owner_class) = owner.into_class() else {
|
||||||
|
return Some(owner);
|
||||||
|
};
|
||||||
|
if owner_class.is_subclass_of(db, pivot_class) {
|
||||||
|
Some(owner)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or(BoundSuperError::FailingConditionCheck {
|
||||||
|
pivot_class: pivot_class_type,
|
||||||
|
owner: owner_type,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Type::BoundSuper(BoundSuperType::new(
|
||||||
|
db,
|
||||||
|
pivot_class,
|
||||||
|
owner,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Skips elements in the MRO up to and including the pivot class.
|
||||||
|
///
|
||||||
|
/// If the pivot class is a dynamic type, its MRO can't be determined,
|
||||||
|
/// so we fall back to using the MRO of `DynamicType::Unknown`.
|
||||||
|
fn skip_until_after_pivot(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
mro_iter: impl Iterator<Item = ClassBase<'db>>,
|
||||||
|
) -> impl Iterator<Item = ClassBase<'db>> {
|
||||||
|
let Some(pivot_class) = self.pivot_class(db).into_class() else {
|
||||||
|
return Either::Left(ClassBase::Dynamic(DynamicType::Unknown).mro(db));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pivot_found = false;
|
||||||
|
|
||||||
|
Either::Right(mro_iter.skip_while(move |superclass| {
|
||||||
|
if pivot_found {
|
||||||
|
false
|
||||||
|
} else if Some(pivot_class) == superclass.into_class() {
|
||||||
|
pivot_found = true;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tries to call `__get__` on the attribute.
|
||||||
|
/// The arguments passed to `__get__` depend on whether the owner is an instance or a class.
|
||||||
|
/// See the `CPython` implementation for reference:
|
||||||
|
/// <https://github.com/python/cpython/blob/3b3720f1a26ab34377542b48eb6a6565f78ff892/Objects/typeobject.c#L11690-L11693>
|
||||||
|
fn try_call_dunder_get_on_attribute(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
attribute: SymbolAndQualifiers<'db>,
|
||||||
|
) -> Option<SymbolAndQualifiers<'db>> {
|
||||||
|
let owner = self.owner(db);
|
||||||
|
|
||||||
|
match owner {
|
||||||
|
// If the owner is a dynamic type, we can't tell whether it's a class or an instance.
|
||||||
|
// Also, invoking a descriptor on a dynamic attribute is meaningless, so we don't handle this.
|
||||||
|
SuperOwnerKind::Dynamic(_) => None,
|
||||||
|
SuperOwnerKind::Class(_) => Some(
|
||||||
|
Type::try_call_dunder_get_on_attribute(
|
||||||
|
db,
|
||||||
|
attribute,
|
||||||
|
Type::none(db),
|
||||||
|
owner.into_type(),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
),
|
||||||
|
SuperOwnerKind::Instance(_) => Some(
|
||||||
|
Type::try_call_dunder_get_on_attribute(
|
||||||
|
db,
|
||||||
|
attribute,
|
||||||
|
owner.into_type(),
|
||||||
|
owner.into_type().to_meta_type(db),
|
||||||
|
)
|
||||||
|
.0,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Similar to `Type::find_name_in_mro_with_policy`, but performs lookup starting *after* the
|
||||||
|
/// pivot class in the MRO, based on the `owner` type instead of the `super` type.
|
||||||
|
fn find_name_in_mro_after_pivot(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: &str,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
|
) -> SymbolAndQualifiers<'db> {
|
||||||
|
let owner = self.owner(db);
|
||||||
|
match owner {
|
||||||
|
SuperOwnerKind::Dynamic(_) => owner
|
||||||
|
.into_type()
|
||||||
|
.find_name_in_mro_with_policy(db, name, policy)
|
||||||
|
.expect("Calling `find_name_in_mro` on dynamic type should return `Some`"),
|
||||||
|
SuperOwnerKind::Class(class) | SuperOwnerKind::Instance(InstanceType { class }) => {
|
||||||
|
let (class_literal, _) = class.class_literal(db);
|
||||||
|
// TODO properly support super() with generic types
|
||||||
|
// * requires a fix for https://github.com/astral-sh/ruff/issues/17432
|
||||||
|
// * also requires understanding how we should handle cases like this:
|
||||||
|
// ```python
|
||||||
|
// b_int: B[int]
|
||||||
|
// b_unknown: B
|
||||||
|
//
|
||||||
|
// super(B, b_int)
|
||||||
|
// super(B[int], b_unknown)
|
||||||
|
// ```
|
||||||
|
match class_literal {
|
||||||
|
ClassLiteralType::Generic(_) => {
|
||||||
|
Symbol::bound(todo_type!("super in generic class")).into()
|
||||||
|
}
|
||||||
|
ClassLiteralType::NonGeneric(_) => class_literal.class_member_from_mro(
|
||||||
|
db,
|
||||||
|
name,
|
||||||
|
policy,
|
||||||
|
self.skip_until_after_pivot(db, owner.iter_mro(db)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure that the `Type` enum does not grow unexpectedly.
|
// Make sure that the `Type` enum does not grow unexpectedly.
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
#[cfg(target_pointer_width = "64")]
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
|
|
||||||
|
|
@ -707,6 +707,16 @@ impl<'db> ClassLiteralType<'db> {
|
||||||
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
|
return Symbol::bound(TupleType::from_elements(db, tuple_elements)).into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.class_member_from_mro(db, name, policy, self.iter_mro(db, specialization))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn class_member_from_mro(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
name: &str,
|
||||||
|
policy: MemberLookupPolicy,
|
||||||
|
mro_iter: impl Iterator<Item = ClassBase<'db>>,
|
||||||
|
) -> SymbolAndQualifiers<'db> {
|
||||||
// If we encounter a dynamic type in this class's MRO, we'll save that dynamic type
|
// If we encounter a dynamic type in this class's MRO, we'll save that dynamic type
|
||||||
// in this variable. After we've traversed the MRO, we'll either:
|
// in this variable. After we've traversed the MRO, we'll either:
|
||||||
// (1) Use that dynamic type as the type for this attribute,
|
// (1) Use that dynamic type as the type for this attribute,
|
||||||
|
|
@ -718,7 +728,7 @@ impl<'db> ClassLiteralType<'db> {
|
||||||
let mut lookup_result: LookupResult<'db> =
|
let mut lookup_result: LookupResult<'db> =
|
||||||
Err(LookupError::Unbound(TypeQualifiers::empty()));
|
Err(LookupError::Unbound(TypeQualifiers::empty()));
|
||||||
|
|
||||||
for superclass in self.iter_mro(db, specialization) {
|
for superclass in mro_iter {
|
||||||
match superclass {
|
match superclass {
|
||||||
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
|
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
|
||||||
// TODO: We currently skip `Protocol` when looking up class members, in order to
|
// TODO: We currently skip `Protocol` when looking up class members, in order to
|
||||||
|
|
@ -1399,6 +1409,7 @@ impl<'db> KnownClass {
|
||||||
| Self::ParamSpecArgs
|
| Self::ParamSpecArgs
|
||||||
| Self::ParamSpecKwargs
|
| Self::ParamSpecKwargs
|
||||||
| Self::TypeVarTuple
|
| Self::TypeVarTuple
|
||||||
|
| Self::Super
|
||||||
| Self::WrapperDescriptorType
|
| Self::WrapperDescriptorType
|
||||||
| Self::UnionType
|
| Self::UnionType
|
||||||
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
|
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
|
||||||
|
|
@ -1437,7 +1448,6 @@ impl<'db> KnownClass {
|
||||||
| Self::Float
|
| Self::Float
|
||||||
| Self::Sized
|
| Self::Sized
|
||||||
| Self::Enum
|
| Self::Enum
|
||||||
| Self::Super
|
|
||||||
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
||||||
// and raises a `TypeError` in Python >=3.14
|
// and raises a `TypeError` in Python >=3.14
|
||||||
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use itertools::Either;
|
||||||
/// non-specialized generic class in any type expression (including the list of base classes), we
|
/// non-specialized generic class in any type expression (including the list of base classes), we
|
||||||
/// automatically construct the default specialization for that class.
|
/// automatically construct the default specialization for that class.
|
||||||
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
|
||||||
pub(crate) enum ClassBase<'db> {
|
pub enum ClassBase<'db> {
|
||||||
Dynamic(DynamicType),
|
Dynamic(DynamicType),
|
||||||
Class(ClassType<'db>),
|
Class(ClassType<'db>),
|
||||||
}
|
}
|
||||||
|
|
@ -96,6 +96,7 @@ impl<'db> ClassBase<'db> {
|
||||||
| Type::ModuleLiteral(_)
|
| Type::ModuleLiteral(_)
|
||||||
| Type::SubclassOf(_)
|
| Type::SubclassOf(_)
|
||||||
| Type::TypeVar(_)
|
| Type::TypeVar(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::AlwaysFalsy
|
| Type::AlwaysFalsy
|
||||||
| Type::AlwaysTruthy => None,
|
| Type::AlwaysTruthy => None,
|
||||||
Type::KnownInstance(known_instance) => match known_instance {
|
Type::KnownInstance(known_instance) => match known_instance {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||||
registry.register_lint(&INVALID_METACLASS);
|
registry.register_lint(&INVALID_METACLASS);
|
||||||
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
|
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
|
||||||
registry.register_lint(&INVALID_RAISE);
|
registry.register_lint(&INVALID_RAISE);
|
||||||
|
registry.register_lint(&INVALID_SUPER_ARGUMENT);
|
||||||
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
|
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
|
||||||
registry.register_lint(&INVALID_TYPE_FORM);
|
registry.register_lint(&INVALID_TYPE_FORM);
|
||||||
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
|
registry.register_lint(&INVALID_TYPE_VARIABLE_CONSTRAINTS);
|
||||||
|
|
@ -52,6 +53,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||||
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
|
registry.register_lint(&SUBCLASS_OF_FINAL_CLASS);
|
||||||
registry.register_lint(&TYPE_ASSERTION_FAILURE);
|
registry.register_lint(&TYPE_ASSERTION_FAILURE);
|
||||||
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
|
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
|
||||||
|
registry.register_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS);
|
||||||
registry.register_lint(&UNDEFINED_REVEAL);
|
registry.register_lint(&UNDEFINED_REVEAL);
|
||||||
registry.register_lint(&UNKNOWN_ARGUMENT);
|
registry.register_lint(&UNKNOWN_ARGUMENT);
|
||||||
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
registry.register_lint(&UNRESOLVED_ATTRIBUTE);
|
||||||
|
|
@ -442,6 +444,45 @@ declare_lint! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_lint! {
|
||||||
|
/// ## What it does
|
||||||
|
/// Detects `super()` calls where:
|
||||||
|
/// - the first argument is not a valid class literal, or
|
||||||
|
/// - the second argument is not an instance or subclass of the first argument.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// `super(type, obj)` expects:
|
||||||
|
/// - the first argument to be a class,
|
||||||
|
/// - and the second argument to satisfy one of the following:
|
||||||
|
/// - `isinstance(obj, type)` is `True`
|
||||||
|
/// - `issubclass(obj, type)` is `True`
|
||||||
|
///
|
||||||
|
/// Violating this relationship will raise a `TypeError` at runtime.
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
/// ```python
|
||||||
|
/// class A:
|
||||||
|
/// ...
|
||||||
|
/// class B(A):
|
||||||
|
/// ...
|
||||||
|
///
|
||||||
|
/// super(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)`
|
||||||
|
///
|
||||||
|
/// super(A(), B()) # error: `A()` is not a class
|
||||||
|
///
|
||||||
|
/// super(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)`
|
||||||
|
/// super(B, A) # error: `A` does not satisfy `issubclass(A, B)`
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## References
|
||||||
|
/// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
|
||||||
|
pub(crate) static INVALID_SUPER_ARGUMENT = {
|
||||||
|
summary: "detects invalid arguments for `super()`",
|
||||||
|
status: LintStatus::preview("1.0.0"),
|
||||||
|
default_level: Level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare_lint! {
|
declare_lint! {
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an
|
/// Checks for a value other than `False` assigned to the `TYPE_CHECKING` variable, or an
|
||||||
|
|
@ -723,6 +764,45 @@ declare_lint! {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare_lint! {
|
||||||
|
/// ## What it does
|
||||||
|
/// Detects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable.
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// When `super()` is used without arguments, Python tries to find two things:
|
||||||
|
/// the nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls).
|
||||||
|
/// If either of these is missing, the call will fail at runtime with a `RuntimeError`.
|
||||||
|
///
|
||||||
|
/// ## Examples
|
||||||
|
/// ```python
|
||||||
|
/// super() # error: no enclosing class or function found
|
||||||
|
///
|
||||||
|
/// def func():
|
||||||
|
/// super() # error: no enclosing class or first argument exists
|
||||||
|
///
|
||||||
|
/// class A:
|
||||||
|
/// f = super() # error: no enclosing function to provide the first argument
|
||||||
|
///
|
||||||
|
/// def method(self):
|
||||||
|
/// def nested():
|
||||||
|
/// super() # error: first argument does not exist in this nested function
|
||||||
|
///
|
||||||
|
/// lambda: super() # error: first argument does not exist in this lambda
|
||||||
|
///
|
||||||
|
/// (super() for _ in range(10)) # error: argument is not available in generator expression
|
||||||
|
///
|
||||||
|
/// super() # okay! both enclosing class and first argument are available
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## References
|
||||||
|
/// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
|
||||||
|
pub(crate) static UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS = {
|
||||||
|
summary: "detects invalid `super()` calls where implicit arguments are unavailable.",
|
||||||
|
status: LintStatus::preview("1.0.0"),
|
||||||
|
default_level: Level::Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare_lint! {
|
declare_lint! {
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
/// Checks for calls to `reveal_type` without importing it.
|
/// Checks for calls to `reveal_type` without importing it.
|
||||||
|
|
|
||||||
|
|
@ -217,6 +217,14 @@ impl Display for DisplayRepresentation<'_> {
|
||||||
}
|
}
|
||||||
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
|
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
|
||||||
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
|
Type::AlwaysFalsy => f.write_str("AlwaysFalsy"),
|
||||||
|
Type::BoundSuper(bound_super) => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"<super: {pivot}, {owner}>",
|
||||||
|
pivot = Type::from(bound_super.pivot_class(self.db)).display(self.db),
|
||||||
|
owner = bound_super.owner(self.db).into_type().display(self.db)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,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::{BoundSuperError, BoundSuperType};
|
||||||
|
|
||||||
/// 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
|
||||||
|
|
@ -2430,6 +2431,34 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Super instances do not allow attribute assignment
|
||||||
|
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Super) => {
|
||||||
|
if emit_diagnostics {
|
||||||
|
self.context.report_lint_old(
|
||||||
|
&INVALID_ASSIGNMENT,
|
||||||
|
target,
|
||||||
|
format_args!(
|
||||||
|
"Cannot assign to attribute `{attribute}` on type `{}`",
|
||||||
|
object_ty.display(self.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Type::BoundSuper(_) => {
|
||||||
|
if emit_diagnostics {
|
||||||
|
self.context.report_lint_old(
|
||||||
|
&INVALID_ASSIGNMENT,
|
||||||
|
target,
|
||||||
|
format_args!(
|
||||||
|
"Cannot assign to attribute `{attribute}` on type `{}`",
|
||||||
|
object_ty.display(self.db()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
Type::Dynamic(..) | Type::Never => true,
|
Type::Dynamic(..) | Type::Never => true,
|
||||||
|
|
||||||
Type::Instance(..)
|
Type::Instance(..)
|
||||||
|
|
@ -4104,6 +4133,41 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the type of the first parameter if the given scope is function-like (i.e. function or lambda).
|
||||||
|
/// Returns `None` if the scope is not function-like, or has no parameters.
|
||||||
|
fn first_param_type_in_scope(&self, scope: ScopeId) -> Option<Type<'db>> {
|
||||||
|
let first_param = match scope.node(self.db()) {
|
||||||
|
NodeWithScopeKind::Function(f) => f.parameters.iter().next(),
|
||||||
|
NodeWithScopeKind::Lambda(l) => l.parameters.as_ref()?.iter().next(),
|
||||||
|
_ => None,
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let definition = self.index.expect_single_definition(first_param);
|
||||||
|
|
||||||
|
Some(infer_definition_types(self.db(), definition).binding_type(definition))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of the nearest enclosing class for the given scope.
|
||||||
|
///
|
||||||
|
/// This function walks up the ancestor scopes starting from the given scope,
|
||||||
|
/// and finds the closest class definition.
|
||||||
|
///
|
||||||
|
/// Returns `None` if no enclosing class is found.a
|
||||||
|
fn enclosing_class_symbol(&self, scope: ScopeId) -> Option<Type<'db>> {
|
||||||
|
self.index
|
||||||
|
.ancestor_scopes(scope.file_scope_id(self.db()))
|
||||||
|
.find_map(|(_, ancestor_scope)| {
|
||||||
|
if let NodeWithScopeKind::Class(class) = ancestor_scope.node() {
|
||||||
|
let definition = self.index.expect_single_definition(class.node());
|
||||||
|
let result = infer_definition_types(self.db(), definition);
|
||||||
|
|
||||||
|
Some(result.declaration_type(definition).inner_type())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
|
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
|
||||||
let ast::ExprCall {
|
let ast::ExprCall {
|
||||||
range: _,
|
range: _,
|
||||||
|
|
@ -4144,6 +4208,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| KnownClass::Type
|
| KnownClass::Type
|
||||||
| KnownClass::Object
|
| KnownClass::Object
|
||||||
| KnownClass::Property
|
| KnownClass::Property
|
||||||
|
| KnownClass::Super
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -4165,40 +4230,44 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
|
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
|
||||||
|
|
||||||
match bindings.check_types(self.db(), &mut call_argument_types) {
|
match bindings.check_types(self.db(), &mut call_argument_types) {
|
||||||
Ok(bindings) => {
|
Ok(mut bindings) => {
|
||||||
for binding in &bindings {
|
for binding in &mut bindings {
|
||||||
let Some(known_function) = binding
|
let binding_type = binding.callable_type;
|
||||||
.callable_type
|
let Some((_, overload)) = binding.matching_overload_mut() else {
|
||||||
.into_function_literal()
|
|
||||||
.and_then(|function_type| function_type.known(self.db()))
|
|
||||||
else {
|
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some((_, overload)) = binding.matching_overload() else {
|
match binding_type {
|
||||||
|
Type::FunctionLiteral(function_literal) => {
|
||||||
|
let Some(known_function) = function_literal.known(self.db()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
match known_function {
|
match known_function {
|
||||||
KnownFunction::RevealType => {
|
KnownFunction::RevealType => {
|
||||||
if let [Some(revealed_type)] = overload.parameter_types() {
|
if let [Some(revealed_type)] = overload.parameter_types() {
|
||||||
if let Some(builder) = self
|
if let Some(builder) = self.context.report_diagnostic(
|
||||||
.context
|
DiagnosticId::RevealedType,
|
||||||
.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
|
Severity::Info,
|
||||||
{
|
) {
|
||||||
let mut diag = builder.into_diagnostic("Revealed type");
|
let mut diag = builder.into_diagnostic("Revealed type");
|
||||||
let span = self.context.span(call_expression);
|
let span = self.context.span(call_expression);
|
||||||
diag.annotate(Annotation::primary(span).message(format_args!(
|
diag.annotate(Annotation::primary(span).message(
|
||||||
|
format_args!(
|
||||||
"`{}`",
|
"`{}`",
|
||||||
revealed_type.display(self.db())
|
revealed_type.display(self.db())
|
||||||
)));
|
),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KnownFunction::AssertType => {
|
KnownFunction::AssertType => {
|
||||||
if let [Some(actual_ty), Some(asserted_ty)] = overload.parameter_types()
|
if let [Some(actual_ty), Some(asserted_ty)] =
|
||||||
|
overload.parameter_types()
|
||||||
|
{
|
||||||
|
if !actual_ty
|
||||||
|
.is_gradual_equivalent_to(self.db(), *asserted_ty)
|
||||||
{
|
{
|
||||||
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
|
|
||||||
self.context.report_lint_old(
|
self.context.report_lint_old(
|
||||||
&TYPE_ASSERTION_FAILURE,
|
&TYPE_ASSERTION_FAILURE,
|
||||||
call_expression,
|
call_expression,
|
||||||
|
|
@ -4226,21 +4295,25 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KnownFunction::StaticAssert => {
|
KnownFunction::StaticAssert => {
|
||||||
if let [Some(parameter_ty), message] = overload.parameter_types() {
|
if let [Some(parameter_ty), message] =
|
||||||
|
overload.parameter_types()
|
||||||
|
{
|
||||||
let truthiness = match parameter_ty.try_bool(self.db()) {
|
let truthiness = match parameter_ty.try_bool(self.db()) {
|
||||||
Ok(truthiness) => truthiness,
|
Ok(truthiness) => truthiness,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let condition = arguments
|
let condition = arguments
|
||||||
.find_argument("condition", 0)
|
.find_argument("condition", 0)
|
||||||
.map(|argument| match argument {
|
.map(|argument| match argument {
|
||||||
ruff_python_ast::ArgOrKeyword::Arg(expr) => {
|
ruff_python_ast::ArgOrKeyword::Arg(
|
||||||
ast::AnyNodeRef::from(expr)
|
expr,
|
||||||
}
|
) => ast::AnyNodeRef::from(expr),
|
||||||
ruff_python_ast::ArgOrKeyword::Keyword(keyword) => {
|
ruff_python_ast::ArgOrKeyword::Keyword(
|
||||||
ast::AnyNodeRef::from(keyword)
|
keyword,
|
||||||
}
|
) => ast::AnyNodeRef::from(keyword),
|
||||||
})
|
})
|
||||||
.unwrap_or(ast::AnyNodeRef::from(call_expression));
|
.unwrap_or(ast::AnyNodeRef::from(
|
||||||
|
call_expression,
|
||||||
|
));
|
||||||
|
|
||||||
err.report_diagnostic(&self.context, condition);
|
err.report_diagnostic(&self.context, condition);
|
||||||
|
|
||||||
|
|
@ -4256,7 +4329,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
self.context.report_lint_old(
|
self.context.report_lint_old(
|
||||||
&STATIC_ASSERT_ERROR,
|
&STATIC_ASSERT_ERROR,
|
||||||
call_expression,
|
call_expression,
|
||||||
format_args!("Static assertion error: {message}"),
|
format_args!(
|
||||||
|
"Static assertion error: {message}"
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else if *parameter_ty == Type::BooleanLiteral(false) {
|
} else if *parameter_ty == Type::BooleanLiteral(false) {
|
||||||
self.context.report_lint_old(
|
self.context.report_lint_old(
|
||||||
|
|
@ -4292,7 +4367,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
{
|
{
|
||||||
let db = self.db();
|
let db = self.db();
|
||||||
if (source_type.is_equivalent_to(db, *casted_type)
|
if (source_type.is_equivalent_to(db, *casted_type)
|
||||||
|| source_type.normalized(db) == casted_type.normalized(db))
|
|| source_type.normalized(db)
|
||||||
|
== casted_type.normalized(db))
|
||||||
&& !source_type.contains_todo(db)
|
&& !source_type.contains_todo(db)
|
||||||
{
|
{
|
||||||
self.context.report_lint_old(
|
self.context.report_lint_old(
|
||||||
|
|
@ -4309,6 +4385,76 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Type::ClassLiteral(class)
|
||||||
|
if class.is_known(self.db(), KnownClass::Super) =>
|
||||||
|
{
|
||||||
|
// Handle the case where `super()` is called with no arguments.
|
||||||
|
// In this case, we need to infer the two arguments:
|
||||||
|
// 1. The nearest enclosing class
|
||||||
|
// 2. The first parameter of the current function (typically `self` or `cls`)
|
||||||
|
match overload.parameter_types() {
|
||||||
|
[] => {
|
||||||
|
let scope = self.scope();
|
||||||
|
|
||||||
|
let Some(enclosing_class) = self.enclosing_class_symbol(scope)
|
||||||
|
else {
|
||||||
|
overload.set_return_type(Type::unknown());
|
||||||
|
BoundSuperError::UnavailableImplicitArguments
|
||||||
|
.report_diagnostic(
|
||||||
|
&self.context,
|
||||||
|
call_expression.into(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(first_param) = self.first_param_type_in_scope(scope)
|
||||||
|
else {
|
||||||
|
overload.set_return_type(Type::unknown());
|
||||||
|
BoundSuperError::UnavailableImplicitArguments
|
||||||
|
.report_diagnostic(
|
||||||
|
&self.context,
|
||||||
|
call_expression.into(),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let bound_super = BoundSuperType::build(
|
||||||
|
self.db(),
|
||||||
|
enclosing_class,
|
||||||
|
first_param,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(
|
||||||
|
&self.context,
|
||||||
|
call_expression.into(),
|
||||||
|
);
|
||||||
|
Type::unknown()
|
||||||
|
});
|
||||||
|
|
||||||
|
overload.set_return_type(bound_super);
|
||||||
|
}
|
||||||
|
[Some(pivot_class_type), Some(owner_type)] => {
|
||||||
|
let bound_super = BoundSuperType::build(
|
||||||
|
self.db(),
|
||||||
|
*pivot_class_type,
|
||||||
|
*owner_type,
|
||||||
|
)
|
||||||
|
.unwrap_or_else(|err| {
|
||||||
|
err.report_diagnostic(
|
||||||
|
&self.context,
|
||||||
|
call_expression.into(),
|
||||||
|
);
|
||||||
|
Type::unknown()
|
||||||
|
});
|
||||||
|
|
||||||
|
overload.set_return_type(bound_super);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
bindings.return_type(self.db())
|
bindings.return_type(self.db())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4711,6 +4857,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| Type::BytesLiteral(_)
|
| Type::BytesLiteral(_)
|
||||||
| Type::SliceLiteral(_)
|
| Type::SliceLiteral(_)
|
||||||
| Type::Tuple(_)
|
| Type::Tuple(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::TypeVar(_),
|
| Type::TypeVar(_),
|
||||||
) => {
|
) => {
|
||||||
let unary_dunder_method = match op {
|
let unary_dunder_method = match op {
|
||||||
|
|
@ -4989,6 +5136,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| Type::BytesLiteral(_)
|
| Type::BytesLiteral(_)
|
||||||
| Type::SliceLiteral(_)
|
| Type::SliceLiteral(_)
|
||||||
| Type::Tuple(_)
|
| Type::Tuple(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::TypeVar(_),
|
| Type::TypeVar(_),
|
||||||
Type::FunctionLiteral(_)
|
Type::FunctionLiteral(_)
|
||||||
| Type::Callable(..)
|
| Type::Callable(..)
|
||||||
|
|
@ -5012,6 +5160,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||||
| Type::BytesLiteral(_)
|
| Type::BytesLiteral(_)
|
||||||
| Type::SliceLiteral(_)
|
| Type::SliceLiteral(_)
|
||||||
| Type::Tuple(_)
|
| Type::Tuple(_)
|
||||||
|
| Type::BoundSuper(_)
|
||||||
| Type::TypeVar(_),
|
| Type::TypeVar(_),
|
||||||
op,
|
op,
|
||||||
) => {
|
) => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,10 @@ use std::cmp::Ordering;
|
||||||
|
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
|
|
||||||
use super::{class_base::ClassBase, DynamicType, InstanceType, KnownInstanceType, TodoType, Type};
|
use super::{
|
||||||
|
class_base::ClassBase, DynamicType, InstanceType, KnownInstanceType, SuperOwnerKind, TodoType,
|
||||||
|
Type,
|
||||||
|
};
|
||||||
|
|
||||||
/// Return an [`Ordering`] that describes the canonical order in which two types should appear
|
/// Return an [`Ordering`] that describes the canonical order in which two types should appear
|
||||||
/// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them
|
/// in an [`crate::types::IntersectionType`] or a [`crate::types::UnionType`] in order for them
|
||||||
|
|
@ -135,6 +138,33 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||||
(Type::AlwaysFalsy, _) => Ordering::Less,
|
(Type::AlwaysFalsy, _) => Ordering::Less,
|
||||||
(_, Type::AlwaysFalsy) => Ordering::Greater,
|
(_, Type::AlwaysFalsy) => Ordering::Greater,
|
||||||
|
|
||||||
|
(Type::BoundSuper(left), Type::BoundSuper(right)) => {
|
||||||
|
(match (left.pivot_class(db), right.pivot_class(db)) {
|
||||||
|
(ClassBase::Class(left), ClassBase::Class(right)) => left.cmp(right),
|
||||||
|
(ClassBase::Class(_), _) => Ordering::Less,
|
||||||
|
(_, ClassBase::Class(_)) => Ordering::Greater,
|
||||||
|
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
|
||||||
|
dynamic_elements_ordering(*left, *right)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then_with(|| match (left.owner(db), right.owner(db)) {
|
||||||
|
(SuperOwnerKind::Class(left), SuperOwnerKind::Class(right)) => left.cmp(right),
|
||||||
|
(SuperOwnerKind::Class(_), _) => Ordering::Less,
|
||||||
|
(_, SuperOwnerKind::Class(_)) => Ordering::Greater,
|
||||||
|
(
|
||||||
|
SuperOwnerKind::Instance(InstanceType { class: left }),
|
||||||
|
SuperOwnerKind::Instance(InstanceType { class: right }),
|
||||||
|
) => left.cmp(right),
|
||||||
|
(SuperOwnerKind::Instance(_), _) => Ordering::Less,
|
||||||
|
(_, SuperOwnerKind::Instance(_)) => Ordering::Greater,
|
||||||
|
(SuperOwnerKind::Dynamic(left), SuperOwnerKind::Dynamic(right)) => {
|
||||||
|
dynamic_elements_ordering(*left, *right)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Type::BoundSuper(_), _) => Ordering::Less,
|
||||||
|
(_, Type::BoundSuper(_)) => Ordering::Greater,
|
||||||
|
|
||||||
(Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) => {
|
(Type::KnownInstance(left_instance), Type::KnownInstance(right_instance)) => {
|
||||||
match (left_instance, right_instance) {
|
match (left_instance, right_instance) {
|
||||||
(KnownInstanceType::Any, _) => Ordering::Less,
|
(KnownInstanceType::Any, _) => Ordering::Less,
|
||||||
|
|
|
||||||
|
|
@ -480,6 +480,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"invalid-super-argument": {
|
||||||
|
"title": "detects invalid arguments for `super()`",
|
||||||
|
"description": "## What it does\nDetects `super()` calls where:\n- the first argument is not a valid class literal, or\n- the second argument is not an instance or subclass of the first argument.\n\n## Why is this bad?\n`super(type, obj)` expects:\n- the first argument to be a class,\n- and the second argument to satisfy one of the following:\n - `isinstance(obj, type)` is `True`\n - `issubclass(obj, type)` is `True`\n\nViolating this relationship will raise a `TypeError` at runtime.\n\n## Examples\n```python\nclass A:\n ...\nclass B(A):\n ...\n\nsuper(A, B()) # it's okay! `A` satisfies `isinstance(B(), A)`\n\nsuper(A(), B()) # error: `A()` is not a class\n\nsuper(B, A()) # error: `A()` does not satisfy `isinstance(A(), B)`\nsuper(B, A) # error: `A` does not satisfy `issubclass(A, B)`\n```\n\n## References\n- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)",
|
||||||
|
"default": "error",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Level"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"invalid-syntax-in-forward-annotation": {
|
"invalid-syntax-in-forward-annotation": {
|
||||||
"title": "detects invalid syntax in forward annotations",
|
"title": "detects invalid syntax in forward annotations",
|
||||||
"description": "TODO #14889",
|
"description": "TODO #14889",
|
||||||
|
|
@ -660,6 +670,16 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"unavailable-implicit-super-arguments": {
|
||||||
|
"title": "detects invalid `super()` calls where implicit arguments are unavailable.",
|
||||||
|
"description": "## What it does\nDetects invalid `super()` calls where implicit arguments like the enclosing class or first method argument are unavailable.\n\n## Why is this bad?\nWhen `super()` is used without arguments, Python tries to find two things:\nthe nearest enclosing class and the first argument of the immediately enclosing function (typically self or cls).\nIf either of these is missing, the call will fail at runtime with a `RuntimeError`.\n\n## Examples\n```python\nsuper() # error: no enclosing class or function found\n\ndef func():\n super() # error: no enclosing class or first argument exists\n\nclass A:\n f = super() # error: no enclosing function to provide the first argument\n\n def method(self):\n def nested():\n super() # error: first argument does not exist in this nested function\n\n lambda: super() # error: first argument does not exist in this lambda\n\n (super() for _ in range(10)) # error: argument is not available in generator expression\n\n super() # okay! both enclosing class and first argument are available\n```\n\n## References\n- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)",
|
||||||
|
"default": "error",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/Level"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"undefined-reveal": {
|
"undefined-reveal": {
|
||||||
"title": "detects usages of `reveal_type` without importing it",
|
"title": "detects usages of `reveal_type` without importing it",
|
||||||
"description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\nTODO #14889",
|
"description": "## What it does\nChecks for calls to `reveal_type` without importing it.\n\n## Why is this bad?\nUsing `reveal_type` without importing it will raise a `NameError` at runtime.\n\n## Examples\nTODO #14889",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue