diff --git a/crates/red_knot_python_semantic/resources/mdtest/attributes.md b/crates/red_knot_python_semantic/resources/mdtest/attributes.md index 6de5cc7fdb..15882300c1 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/attributes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/attributes.md @@ -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 diff --git a/crates/red_knot_python_semantic/resources/mdtest/class/super.md b/crates/red_knot_python_semantic/resources/mdtest/class/super.md new file mode 100644 index 0000000000..2fa7d0d767 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/class/super.md @@ -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 `` has no attribute `c`" +super(C, C()).c + +super(B, C()).a +# error: [unresolved-attribute] "Type `` has no attribute `b`" +super(B, C()).b +# error: [unresolved-attribute] "Type `` has no attribute `c`" +super(B, C()).c + +# error: [unresolved-attribute] "Type `` has no attribute `a`" +super(A, C()).a +# error: [unresolved-attribute] "Type `` has no attribute `b`" +super(A, C()).b +# error: [unresolved-attribute] "Type `` 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__, )`. 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 `` + reveal_type(super()) # revealed: + super().__init__(a) + + @classmethod + def f(cls): + # TODO: Once `Self` is supported, this should be `` + reveal_type(super()) # revealed: + 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(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: + reveal_type(super(A, x)) # revealed: + reveal_type(super(x, A())) # revealed: + + 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: + + class B: + def test(self): + reveal_type(super()) # revealed: + + class C(A.B): + def test(self): + reveal_type(super()) # revealed: + + def inner(t: C): + reveal_type(super()) # revealed: + lambda x: reveal_type(super()) # revealed: +``` + +## Built-ins and Literals + +```py +reveal_type(super(bool, True)) # revealed: +reveal_type(super(bool, bool())) # revealed: +reveal_type(super(int, bool())) # revealed: +reveal_type(super(int, 3)) # revealed: +reveal_type(super(str, "")) # revealed: +``` + +## 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: | + + # error: [possibly-unbound-attribute] "Attribute `b` on type ` | ` 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: + +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: | + + reveal_type(s.x) # revealed: Unknown | Literal[1, 2] + reveal_type(s.y) # revealed: int | str + + # error: [possibly-unbound-attribute] "Attribute `a` on type ` | ` 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 `` 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 `` with no `__getitem__` method" +super(B, B())[0] +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 2db902bb34..6e33f79847 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -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[]` 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> { + 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> { + 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 { + 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, 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::, _>>()?, + )); + } + + 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>, + ) -> impl Iterator> { + 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: + /// + fn try_call_dunder_get_on_attribute( + self, + db: &'db dyn Db, + attribute: SymbolAndQualifiers<'db>, + ) -> Option> { + 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")] diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index 289bb3d67d..f69149d55b 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -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>, + ) -> 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) diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index 0a173993f5..4b33c60b8c 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -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 { diff --git a/crates/red_knot_python_semantic/src/types/diagnostic.rs b/crates/red_knot_python_semantic/src/types/diagnostic.rs index ccfa6d8095..bca2c739a8 100644 --- a/crates/red_knot_python_semantic/src/types/diagnostic.rs +++ b/crates/red_knot_python_semantic/src/types/diagnostic.rs @@ -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. diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 1f922ad4db..1314fabb6f 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -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, + "", + pivot = Type::from(bound_super.pivot_class(self.db)).display(self.db), + owner = bound_super.owner(self.db).into_type().display(self.db) + ) + } } } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index f532e85985..b049ff57c0 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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> { + 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> { + 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, ) => { diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index 0fbe974bcb..e3b341fee6 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -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, diff --git a/knot.schema.json b/knot.schema.json index 2d3f00b962..5911ab01d3 100644 --- a/knot.schema.json +++ b/knot.schema.json @@ -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",