From 9090aead0f963be9bd6d14c388a6cafc3cc17893 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 14 Oct 2025 13:48:47 +0100 Subject: [PATCH] [ty] Fix further issues in `super()` inference logic (#20843) --- .../resources/mdtest/class/super.md | 22 ++ ...licit_Super_Objec…_(b753048091f275c0).snap | 288 ++++++++++-------- ...licit_Super_Objec…_(f9e5e48e3a4a4c12).snap | 1 + crates/ty_python_semantic/src/types.rs | 6 + .../src/types/bound_super.rs | 37 ++- 5 files changed, 212 insertions(+), 142 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index b943e4f21b..933e43bbd1 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -25,6 +25,8 @@ python-version = "3.12" ``` ```py +from __future__ import annotations + class A: def a(self): ... aa: int = 1 @@ -116,6 +118,26 @@ def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): # revealed: , dict[Literal["x", "y"], int | bytes]> reveal_type(super(object, y)) + +# The first argument to `super()` must be an actual class object; +# instances of `GenericAlias` are not accepted at runtime: +# +# error: [invalid-super-argument] +# revealed: Unknown +reveal_type(super(list[int], [])) +``` + +`super(pivot_class, owner)` can be called from inside methods, just like single-argument `super()`: + +```py +class Super: + def method(self) -> int: + return 42 + +class Sub(Super): + def method(self: Sub) -> int: + # revealed: , Sub> + return reveal_type(super(self.__class__, self)).method() ``` ### Implicit Super Object diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap index 339c9a59a7..014d05eff4 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap @@ -12,104 +12,121 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md ## mdtest_snippet.py ``` - 1 | class A: - 2 | def a(self): ... - 3 | aa: int = 1 - 4 | - 5 | class B(A): - 6 | def b(self): ... - 7 | bb: int = 2 - 8 | - 9 | class C(B): -10 | def c(self): ... -11 | cc: int = 3 -12 | -13 | reveal_type(C.__mro__) # revealed: tuple[, , , ] -14 | -15 | super(C, C()).a -16 | super(C, C()).b -17 | super(C, C()).c # error: [unresolved-attribute] -18 | -19 | super(B, C()).a -20 | super(B, C()).b # error: [unresolved-attribute] -21 | super(B, C()).c # error: [unresolved-attribute] -22 | -23 | super(A, C()).a # error: [unresolved-attribute] -24 | super(A, C()).b # error: [unresolved-attribute] -25 | super(A, C()).c # error: [unresolved-attribute] -26 | -27 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown -28 | reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown -29 | reveal_type(super(C, C()).aa) # revealed: int -30 | reveal_type(super(C, C()).bb) # revealed: int -31 | import types -32 | from typing_extensions import Callable, TypeIs, Literal, TypedDict -33 | -34 | def f(): ... -35 | -36 | class Foo[T]: -37 | def method(self): ... -38 | @property -39 | def some_property(self): ... -40 | -41 | type Alias = int -42 | -43 | class SomeTypedDict(TypedDict): -44 | x: int -45 | y: bytes -46 | -47 | # revealed: , FunctionType> -48 | reveal_type(super(object, f)) -49 | # revealed: , WrapperDescriptorType> -50 | reveal_type(super(object, types.FunctionType.__get__)) -51 | # revealed: , GenericAlias> -52 | reveal_type(super(object, Foo[int])) -53 | # revealed: , _SpecialForm> -54 | reveal_type(super(object, Literal)) -55 | # revealed: , TypeAliasType> -56 | reveal_type(super(object, Alias)) -57 | # revealed: , MethodType> -58 | reveal_type(super(object, Foo().method)) -59 | # revealed: , property> -60 | reveal_type(super(object, Foo.some_property)) -61 | -62 | def g(x: object) -> TypeIs[list[object]]: -63 | return isinstance(x, list) -64 | -65 | def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): -66 | if hasattr(x, "bar"): -67 | # revealed: -68 | reveal_type(x) -69 | # error: [invalid-super-argument] -70 | # revealed: Unknown -71 | reveal_type(super(object, x)) -72 | -73 | # error: [invalid-super-argument] -74 | # revealed: Unknown -75 | reveal_type(super(object, z)) -76 | -77 | is_list = g(x) -78 | # revealed: TypeIs[list[object] @ x] -79 | reveal_type(is_list) -80 | # revealed: , bool> -81 | reveal_type(super(object, is_list)) -82 | -83 | # revealed: , dict[Literal["x", "y"], int | bytes]> -84 | reveal_type(super(object, y)) + 1 | from __future__ import annotations + 2 | + 3 | class A: + 4 | def a(self): ... + 5 | aa: int = 1 + 6 | + 7 | class B(A): + 8 | def b(self): ... + 9 | bb: int = 2 + 10 | + 11 | class C(B): + 12 | def c(self): ... + 13 | cc: int = 3 + 14 | + 15 | reveal_type(C.__mro__) # revealed: tuple[, , , ] + 16 | + 17 | super(C, C()).a + 18 | super(C, C()).b + 19 | super(C, C()).c # error: [unresolved-attribute] + 20 | + 21 | super(B, C()).a + 22 | super(B, C()).b # error: [unresolved-attribute] + 23 | super(B, C()).c # error: [unresolved-attribute] + 24 | + 25 | super(A, C()).a # error: [unresolved-attribute] + 26 | super(A, C()).b # error: [unresolved-attribute] + 27 | super(A, C()).c # error: [unresolved-attribute] + 28 | + 29 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown + 30 | reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown + 31 | reveal_type(super(C, C()).aa) # revealed: int + 32 | reveal_type(super(C, C()).bb) # revealed: int + 33 | import types + 34 | from typing_extensions import Callable, TypeIs, Literal, TypedDict + 35 | + 36 | def f(): ... + 37 | + 38 | class Foo[T]: + 39 | def method(self): ... + 40 | @property + 41 | def some_property(self): ... + 42 | + 43 | type Alias = int + 44 | + 45 | class SomeTypedDict(TypedDict): + 46 | x: int + 47 | y: bytes + 48 | + 49 | # revealed: , FunctionType> + 50 | reveal_type(super(object, f)) + 51 | # revealed: , WrapperDescriptorType> + 52 | reveal_type(super(object, types.FunctionType.__get__)) + 53 | # revealed: , GenericAlias> + 54 | reveal_type(super(object, Foo[int])) + 55 | # revealed: , _SpecialForm> + 56 | reveal_type(super(object, Literal)) + 57 | # revealed: , TypeAliasType> + 58 | reveal_type(super(object, Alias)) + 59 | # revealed: , MethodType> + 60 | reveal_type(super(object, Foo().method)) + 61 | # revealed: , property> + 62 | reveal_type(super(object, Foo.some_property)) + 63 | + 64 | def g(x: object) -> TypeIs[list[object]]: + 65 | return isinstance(x, list) + 66 | + 67 | def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): + 68 | if hasattr(x, "bar"): + 69 | # revealed: + 70 | reveal_type(x) + 71 | # error: [invalid-super-argument] + 72 | # revealed: Unknown + 73 | reveal_type(super(object, x)) + 74 | + 75 | # error: [invalid-super-argument] + 76 | # revealed: Unknown + 77 | reveal_type(super(object, z)) + 78 | + 79 | is_list = g(x) + 80 | # revealed: TypeIs[list[object] @ x] + 81 | reveal_type(is_list) + 82 | # revealed: , bool> + 83 | reveal_type(super(object, is_list)) + 84 | + 85 | # revealed: , dict[Literal["x", "y"], int | bytes]> + 86 | reveal_type(super(object, y)) + 87 | + 88 | # The first argument to `super()` must be an actual class object; + 89 | # instances of `GenericAlias` are not accepted at runtime: + 90 | # + 91 | # error: [invalid-super-argument] + 92 | # revealed: Unknown + 93 | reveal_type(super(list[int], [])) + 94 | class Super: + 95 | def method(self) -> int: + 96 | return 42 + 97 | + 98 | class Sub(Super): + 99 | def method(self: Sub) -> int: +100 | # revealed: , Sub> +101 | return reveal_type(super(self.__class__, self)).method() ``` # Diagnostics ``` error[unresolved-attribute]: Type `, C>` has no attribute `c` - --> src/mdtest_snippet.py:17:1 + --> src/mdtest_snippet.py:19:1 | -15 | super(C, C()).a -16 | super(C, C()).b -17 | super(C, C()).c # error: [unresolved-attribute] +17 | super(C, C()).a +18 | super(C, C()).b +19 | super(C, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -18 | -19 | super(B, C()).a +20 | +21 | super(B, C()).a | info: rule `unresolved-attribute` is enabled by default @@ -117,12 +134,12 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Type `, C>` has no attribute `b` - --> src/mdtest_snippet.py:20:1 + --> src/mdtest_snippet.py:22:1 | -19 | super(B, C()).a -20 | super(B, C()).b # error: [unresolved-attribute] +21 | super(B, C()).a +22 | super(B, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -21 | super(B, C()).c # error: [unresolved-attribute] +23 | super(B, C()).c # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -130,14 +147,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Type `, C>` has no attribute `c` - --> src/mdtest_snippet.py:21:1 + --> src/mdtest_snippet.py:23:1 | -19 | super(B, C()).a -20 | super(B, C()).b # error: [unresolved-attribute] -21 | super(B, C()).c # error: [unresolved-attribute] +21 | super(B, C()).a +22 | super(B, C()).b # error: [unresolved-attribute] +23 | super(B, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -22 | -23 | super(A, C()).a # error: [unresolved-attribute] +24 | +25 | super(A, C()).a # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -145,14 +162,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Type `, C>` has no attribute `a` - --> src/mdtest_snippet.py:23:1 + --> src/mdtest_snippet.py:25:1 | -21 | super(B, C()).c # error: [unresolved-attribute] -22 | -23 | super(A, C()).a # error: [unresolved-attribute] +23 | super(B, C()).c # error: [unresolved-attribute] +24 | +25 | super(A, C()).a # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -24 | super(A, C()).b # error: [unresolved-attribute] -25 | super(A, C()).c # error: [unresolved-attribute] +26 | super(A, C()).b # error: [unresolved-attribute] +27 | super(A, C()).c # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -160,12 +177,12 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Type `, C>` has no attribute `b` - --> src/mdtest_snippet.py:24:1 + --> src/mdtest_snippet.py:26:1 | -23 | super(A, C()).a # error: [unresolved-attribute] -24 | super(A, C()).b # error: [unresolved-attribute] +25 | super(A, C()).a # error: [unresolved-attribute] +26 | super(A, C()).b # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -25 | super(A, C()).c # error: [unresolved-attribute] +27 | super(A, C()).c # error: [unresolved-attribute] | info: rule `unresolved-attribute` is enabled by default @@ -173,14 +190,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[unresolved-attribute]: Type `, C>` has no attribute `c` - --> src/mdtest_snippet.py:25:1 + --> src/mdtest_snippet.py:27:1 | -23 | super(A, C()).a # error: [unresolved-attribute] -24 | super(A, C()).b # error: [unresolved-attribute] -25 | super(A, C()).c # error: [unresolved-attribute] +25 | super(A, C()).a # error: [unresolved-attribute] +26 | super(A, C()).b # error: [unresolved-attribute] +27 | super(A, C()).c # error: [unresolved-attribute] | ^^^^^^^^^^^^^^^ -26 | -27 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown +28 | +29 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown | info: rule `unresolved-attribute` is enabled by default @@ -188,14 +205,14 @@ info: rule `unresolved-attribute` is enabled by default ``` error[invalid-super-argument]: `` is an abstract/structural type in `super(, )` call - --> src/mdtest_snippet.py:71:21 + --> src/mdtest_snippet.py:73:21 | -69 | # error: [invalid-super-argument] -70 | # revealed: Unknown -71 | reveal_type(super(object, x)) +71 | # error: [invalid-super-argument] +72 | # revealed: Unknown +73 | reveal_type(super(object, x)) | ^^^^^^^^^^^^^^^^ -72 | -73 | # error: [invalid-super-argument] +74 | +75 | # error: [invalid-super-argument] | info: rule `invalid-super-argument` is enabled by default @@ -203,14 +220,29 @@ info: rule `invalid-super-argument` is enabled by default ``` error[invalid-super-argument]: `(int, str, /) -> bool` is an abstract/structural type in `super(, (int, str, /) -> bool)` call - --> src/mdtest_snippet.py:75:17 + --> src/mdtest_snippet.py:77:17 | -73 | # error: [invalid-super-argument] -74 | # revealed: Unknown -75 | reveal_type(super(object, z)) +75 | # error: [invalid-super-argument] +76 | # revealed: Unknown +77 | reveal_type(super(object, z)) | ^^^^^^^^^^^^^^^^ -76 | -77 | is_list = g(x) +78 | +79 | is_list = g(x) + | +info: rule `invalid-super-argument` is enabled by default + +``` + +``` +error[invalid-super-argument]: `types.GenericAlias` instance `list[int]` is not a valid class + --> src/mdtest_snippet.py:93:13 + | +91 | # error: [invalid-super-argument] +92 | # revealed: Unknown +93 | reveal_type(super(list[int], [])) + | ^^^^^^^^^^^^^^^^^^^^ +94 | class Super: +95 | def method(self) -> int: | info: rule `invalid-super-argument` is enabled by default diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap index 846e4bdbb7..b13937e6e5 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap @@ -162,6 +162,7 @@ error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` +help: Consider adding an upper bound to type variable `S` info: rule `invalid-super-argument` is enabled by default ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 410192de9b..7f9d182c69 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7790,6 +7790,12 @@ pub enum TypeVarKind { TypingSelf, } +impl TypeVarKind { + const fn is_self(self) -> bool { + matches!(self, Self::TypingSelf) + } +} + /// The identity of a type variable. /// /// This represents the core identity of a typevar, independent of its bounds or constraints. Two diff --git a/crates/ty_python_semantic/src/types/bound_super.rs b/crates/ty_python_semantic/src/types/bound_super.rs index 07ce3fbc46..9bacb1454c 100644 --- a/crates/ty_python_semantic/src/types/bound_super.rs +++ b/crates/ty_python_semantic/src/types/bound_super.rs @@ -5,7 +5,7 @@ use ruff_db::diagnostic::Diagnostic; use ruff_python_ast::AnyNodeRef; use crate::{ - Db, + Db, DisplaySettings, place::{Place, PlaceAndQualifiers}, types::{ ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy, @@ -75,10 +75,16 @@ impl<'db> BoundSuperError<'db> { } BoundSuperError::InvalidPivotClassType { pivot_class } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { - builder.into_diagnostic(format_args!( - "`{pivot_class}` is not a valid class", - pivot_class = pivot_class.display(context.db()), - )); + match pivot_class { + Type::GenericAlias(alias) => builder.into_diagnostic(format_args!( + "`types.GenericAlias` instance `{}` is not a valid class", + alias.display_with(context.db(), DisplaySettings::default()), + )), + _ => builder.into_diagnostic(format_args!( + "`{pivot_class}` is not a valid class", + pivot_class = pivot_class.display(context.db()), + )), + }; } } BoundSuperError::FailingConditionCheck { @@ -102,6 +108,14 @@ impl<'db> BoundSuperError<'db> { bound_or_constraints_union.display(context.db()), pivot_class = pivot_class.display(context.db()), )); + if typevar_context.bound_or_constraints(context.db()).is_none() + && !typevar_context.kind(context.db()).is_self() + { + diagnostic.help(format_args!( + "Consider adding an upper bound to type variable `{}`", + typevar_context.name(context.db()) + )); + } } } } @@ -412,15 +426,10 @@ impl<'db> BoundSuperType<'db> { // but are valid as pivot classes, e.g. unsubscripted `typing.Generic` let pivot_class = match pivot_class_type { Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)), - Type::GenericAlias(class) => ClassBase::Class(ClassType::Generic(class)), - Type::SubclassOf(subclass_of) if subclass_of.subclass_of().is_dynamic() => { - ClassBase::Dynamic( - subclass_of - .subclass_of() - .into_dynamic() - .expect("Checked in branch arm"), - ) - } + Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() { + SubclassOfInner::Class(class) => ClassBase::Class(class), + SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic), + }, Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol, Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic, Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict,