[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:
cake-monotone 2025-04-17 03:41:55 +09:00 committed by GitHub
parent 1a79722ee0
commit 649610cc98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1224 additions and 155 deletions

View File

@ -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)
```
## `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
Some of the tests in the *Class and instance variables* section draw inspiration from

View File

@ -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 methods 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]
```

View File

@ -1,13 +1,18 @@
use itertools::Either;
use std::slice::Iter;
use std::str::FromStr;
use bitflags::bitflags;
use call::{CallDunderError, CallError, CallErrorKind};
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_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
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
/// is specialized, we will replace this typevar with its specialization.
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
}
@ -521,6 +530,16 @@ impl<'db> Type<'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::Union(union) => union.elements(db).iter().any(|ty| ty.contains_todo(db)),
@ -783,6 +802,7 @@ impl<'db> Type<'db> {
| Type::ClassLiteral(_)
| Type::KnownInstance(_)
| Type::IntLiteral(_)
| Type::BoundSuper(_)
| Type::SubclassOf(_) => self,
Type::GenericAlias(generic) => {
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).
(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`,
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
(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
.to_instance(db)
.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::BoundSuper(bound_super) => {
!matches!(bound_super.pivot_class(db), ClassBase::Dynamic(_))
&& !matches!(bound_super.owner(db), SuperOwnerKind::Dynamic(_))
}
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::Instance(_) => {
// 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.
@ -1907,6 +1939,7 @@ impl<'db> Type<'db> {
// We eagerly transform `SubclassOf` to `ClassLiteral` for final types, so `SubclassOf` is never a singleton.
Type::SubclassOf(..) => false,
Type::BoundSuper(..) => false,
Type::BooleanLiteral(_)
| Type::FunctionLiteral(..)
| Type::WrapperDescriptor(..)
@ -2015,6 +2048,11 @@ impl<'db> Type<'db> {
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::Never
| Type::Union(..)
@ -2139,6 +2177,12 @@ impl<'db> Type<'db> {
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).
// 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`.
@ -2282,6 +2326,13 @@ impl<'db> Type<'db> {
.to_instance(db)
.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
// 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`,
@ -2676,10 +2727,6 @@ impl<'db> Type<'db> {
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" => {
Symbol::bound(property.getter(db).unwrap_or(Type::none(db))).into()
}
@ -2804,6 +2851,19 @@ impl<'db> Type<'db> {
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::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()),
Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()),
Type::BoundSuper(_) => Truthiness::AlwaysTrue,
};
Ok(truthiness)
@ -3552,6 +3613,44 @@ impl<'db> Type<'db> {
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) => {
let getter_signature = Signature::new(
Parameters::new([
@ -4057,6 +4156,7 @@ impl<'db> Type<'db> {
| Type::Tuple(_)
| Type::TypeVar(_)
| Type::LiteralString
| Type::BoundSuper(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy => None,
}
@ -4118,6 +4218,7 @@ impl<'db> Type<'db> {
| Type::DataclassDecorator(_)
| Type::Never
| Type::FunctionLiteral(_)
| Type::BoundSuper(_)
| Type::PropertyInstance(_) => Err(InvalidTypeExpressionError {
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)],
fallback_type: Type::unknown(),
@ -4360,6 +4461,7 @@ impl<'db> Type<'db> {
.expect("Type::Todo should be a valid ClassBase"),
),
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::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::BoundSuper(_)
// Instance contains a ClassType, which has already been specialized if needed, like
// above with BoundMethod's self_instance.
| Type::Instance(_)
@ -4571,6 +4674,7 @@ impl<'db> Type<'db> {
| Self::WrapperDescriptor(_)
| Self::DataclassDecorator(_)
| Self::PropertyInstance(_)
| Self::BoundSuper(_)
| Self::Tuple(_) => self.to_meta_type(db).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.
#[cfg(not(debug_assertions))]
#[cfg(target_pointer_width = "64")]

View File

@ -707,6 +707,16 @@ impl<'db> ClassLiteralType<'db> {
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
// in this variable. After we've traversed the MRO, we'll either:
// (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> =
Err(LookupError::Unbound(TypeQualifiers::empty()));
for superclass in self.iter_mro(db, specialization) {
for superclass in mro_iter {
match superclass {
ClassBase::Dynamic(DynamicType::TodoProtocol) => {
// TODO: We currently skip `Protocol` when looking up class members, in order to
@ -1399,6 +1409,7 @@ impl<'db> KnownClass {
| Self::ParamSpecArgs
| Self::ParamSpecKwargs
| Self::TypeVarTuple
| Self::Super
| Self::WrapperDescriptorType
| Self::UnionType
| Self::MethodWrapperType => Truthiness::AlwaysTrue,
@ -1437,7 +1448,6 @@ impl<'db> KnownClass {
| Self::Float
| Self::Sized
| Self::Enum
| Self::Super
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
// and raises a `TypeError` in Python >=3.14
// (see https://docs.python.org/3/library/constants.html#NotImplemented)

View File

@ -11,7 +11,7 @@ use itertools::Either;
/// non-specialized generic class in any type expression (including the list of base classes), we
/// automatically construct the default specialization for that class.
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, salsa::Update)]
pub(crate) enum ClassBase<'db> {
pub enum ClassBase<'db> {
Dynamic(DynamicType),
Class(ClassType<'db>),
}
@ -96,6 +96,7 @@ impl<'db> ClassBase<'db> {
| Type::ModuleLiteral(_)
| Type::SubclassOf(_)
| Type::TypeVar(_)
| Type::BoundSuper(_)
| Type::AlwaysFalsy
| Type::AlwaysTruthy => None,
Type::KnownInstance(known_instance) => match known_instance {

View File

@ -37,6 +37,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&INVALID_METACLASS);
registry.register_lint(&INVALID_PARAMETER_DEFAULT);
registry.register_lint(&INVALID_RAISE);
registry.register_lint(&INVALID_SUPER_ARGUMENT);
registry.register_lint(&INVALID_TYPE_CHECKING_CONSTANT);
registry.register_lint(&INVALID_TYPE_FORM);
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(&TYPE_ASSERTION_FAILURE);
registry.register_lint(&TOO_MANY_POSITIONAL_ARGUMENTS);
registry.register_lint(&UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS);
registry.register_lint(&UNDEFINED_REVEAL);
registry.register_lint(&UNKNOWN_ARGUMENT);
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! {
/// ## What it does
/// 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! {
/// ## What it does
/// Checks for calls to `reveal_type` without importing it.

View File

@ -217,6 +217,14 @@ impl Display for DisplayRepresentation<'_> {
}
Type::AlwaysTruthy => f.write_str("AlwaysTruthy"),
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)
)
}
}
}
}

View File

@ -107,6 +107,7 @@ use super::slots::check_class_slots;
use super::string_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.
/// 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::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> {
let ast::ExprCall {
range: _,
@ -4144,6 +4208,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| KnownClass::Type
| KnownClass::Object
| KnownClass::Property
| KnownClass::Super
)
})
}) {
@ -4165,148 +4230,229 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_argument_types(arguments, call_arguments, &bindings.argument_forms);
match bindings.check_types(self.db(), &mut call_argument_types) {
Ok(bindings) => {
for binding in &bindings {
let Some(known_function) = binding
.callable_type
.into_function_literal()
.and_then(|function_type| function_type.known(self.db()))
else {
Ok(mut bindings) => {
for binding in &mut bindings {
let binding_type = binding.callable_type;
let Some((_, overload)) = binding.matching_overload_mut() else {
continue;
};
let Some((_, overload)) = binding.matching_overload() else {
continue;
};
match binding_type {
Type::FunctionLiteral(function_literal) => {
let Some(known_function) = function_literal.known(self.db()) else {
continue;
};
match known_function {
KnownFunction::RevealType => {
if let [Some(revealed_type)] = overload.parameter_types() {
if let Some(builder) = self
.context
.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
{
let mut diag = builder.into_diagnostic("Revealed type");
let span = self.context.span(call_expression);
diag.annotate(Annotation::primary(span).message(format_args!(
"`{}`",
revealed_type.display(self.db())
)));
}
}
}
KnownFunction::AssertType => {
if let [Some(actual_ty), Some(asserted_ty)] = overload.parameter_types()
{
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
self.context.report_lint_old(
&TYPE_ASSERTION_FAILURE,
call_expression,
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(self.db()),
asserted_ty.display(self.db()),
),
);
}
}
}
KnownFunction::AssertNever => {
if let [Some(actual_ty)] = overload.parameter_types() {
if !actual_ty.is_equivalent_to(self.db(), Type::Never) {
self.context.report_lint_old(
&TYPE_ASSERTION_FAILURE,
call_expression,
format_args!(
"Expected type `Never`, got `{}` instead",
actual_ty.display(self.db()),
),
);
}
}
}
KnownFunction::StaticAssert => {
if let [Some(parameter_ty), message] = overload.parameter_types() {
let truthiness = match parameter_ty.try_bool(self.db()) {
Ok(truthiness) => truthiness,
Err(err) => {
let condition = arguments
.find_argument("condition", 0)
.map(|argument| match argument {
ruff_python_ast::ArgOrKeyword::Arg(expr) => {
ast::AnyNodeRef::from(expr)
}
ruff_python_ast::ArgOrKeyword::Keyword(keyword) => {
ast::AnyNodeRef::from(keyword)
}
})
.unwrap_or(ast::AnyNodeRef::from(call_expression));
err.report_diagnostic(&self.context, condition);
continue;
match known_function {
KnownFunction::RevealType => {
if let [Some(revealed_type)] = overload.parameter_types() {
if let Some(builder) = self.context.report_diagnostic(
DiagnosticId::RevealedType,
Severity::Info,
) {
let mut diag = builder.into_diagnostic("Revealed type");
let span = self.context.span(call_expression);
diag.annotate(Annotation::primary(span).message(
format_args!(
"`{}`",
revealed_type.display(self.db())
),
));
}
}
};
if !truthiness.is_always_true() {
if let Some(message) = message
.and_then(Type::into_string_literal)
.map(|s| &**s.value(self.db()))
}
KnownFunction::AssertType => {
if let [Some(actual_ty), Some(asserted_ty)] =
overload.parameter_types()
{
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!("Static assertion error: {message}"),
);
} else if *parameter_ty == Type::BooleanLiteral(false) {
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!("Static assertion error: argument evaluates to `False`"),
);
} else if truthiness.is_always_false() {
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(self.db())
),
);
} else {
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(self.db())
),
);
if !actual_ty
.is_gradual_equivalent_to(self.db(), *asserted_ty)
{
self.context.report_lint_old(
&TYPE_ASSERTION_FAILURE,
call_expression,
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(self.db()),
asserted_ty.display(self.db()),
),
);
}
}
}
}
}
KnownFunction::Cast => {
if let [Some(casted_type), Some(source_type)] =
overload.parameter_types()
{
let db = self.db();
if (source_type.is_equivalent_to(db, *casted_type)
|| source_type.normalized(db) == casted_type.normalized(db))
&& !source_type.contains_todo(db)
{
self.context.report_lint_old(
&REDUNDANT_CAST,
call_expression,
format_args!(
"Value is already of type `{}`",
casted_type.display(db),
),
);
KnownFunction::AssertNever => {
if let [Some(actual_ty)] = overload.parameter_types() {
if !actual_ty.is_equivalent_to(self.db(), Type::Never) {
self.context.report_lint_old(
&TYPE_ASSERTION_FAILURE,
call_expression,
format_args!(
"Expected type `Never`, got `{}` instead",
actual_ty.display(self.db()),
),
);
}
}
}
KnownFunction::StaticAssert => {
if let [Some(parameter_ty), message] =
overload.parameter_types()
{
let truthiness = match parameter_ty.try_bool(self.db()) {
Ok(truthiness) => truthiness,
Err(err) => {
let condition = arguments
.find_argument("condition", 0)
.map(|argument| match argument {
ruff_python_ast::ArgOrKeyword::Arg(
expr,
) => ast::AnyNodeRef::from(expr),
ruff_python_ast::ArgOrKeyword::Keyword(
keyword,
) => ast::AnyNodeRef::from(keyword),
})
.unwrap_or(ast::AnyNodeRef::from(
call_expression,
));
err.report_diagnostic(&self.context, condition);
continue;
}
};
if !truthiness.is_always_true() {
if let Some(message) = message
.and_then(Type::into_string_literal)
.map(|s| &**s.value(self.db()))
{
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!(
"Static assertion error: {message}"
),
);
} else if *parameter_ty == Type::BooleanLiteral(false) {
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!("Static assertion error: argument evaluates to `False`"),
);
} else if truthiness.is_always_false() {
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(self.db())
),
);
} else {
self.context.report_lint_old(
&STATIC_ASSERT_ERROR,
call_expression,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(self.db())
),
);
}
}
}
}
KnownFunction::Cast => {
if let [Some(casted_type), Some(source_type)] =
overload.parameter_types()
{
let db = self.db();
if (source_type.is_equivalent_to(db, *casted_type)
|| source_type.normalized(db)
== casted_type.normalized(db))
&& !source_type.contains_todo(db)
{
self.context.report_lint_old(
&REDUNDANT_CAST,
call_expression,
format_args!(
"Value is already of type `{}`",
casted_type.display(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())
@ -4711,6 +4857,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_),
) => {
let unary_dunder_method = match op {
@ -4989,6 +5136,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_),
Type::FunctionLiteral(_)
| Type::Callable(..)
@ -5012,6 +5160,7 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_)
| Type::BoundSuper(_)
| Type::TypeVar(_),
op,
) => {

View File

@ -2,7 +2,10 @@ use std::cmp::Ordering;
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
/// 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::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)) => {
match (left_instance, right_instance) {
(KnownInstanceType::Any, _) => Ordering::Less,

View File

@ -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": {
"title": "detects invalid syntax in forward annotations",
"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": {
"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",