From d83d7a0dcdbeb8873f5b42bdf26b6fe46d2ba8ba Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 13 Oct 2025 11:57:46 +0100 Subject: [PATCH] [ty] Fix false-positive diagnostics on `super()` calls (#20814) --- .../resources/mdtest/class/super.md | 221 +++++++++- ...licit_Super_Objec…_(b753048091f275c0).snap | 217 ++++++++++ ...licit_Super_Objec…_(f9e5e48e3a4a4c12).snap | 214 ++++++++++ crates/ty_python_semantic/src/types.rs | 384 ++++++++++++++---- .../ty_python_semantic/src/types/display.rs | 4 +- .../ty_python_semantic/src/types/instance.rs | 58 +-- 6 files changed, 975 insertions(+), 123 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap diff --git a/crates/ty_python_semantic/resources/mdtest/class/super.md b/crates/ty_python_semantic/resources/mdtest/class/super.md index b4befc0e44..b943e4f21b 100644 --- a/crates/ty_python_semantic/resources/mdtest/class/super.md +++ b/crates/ty_python_semantic/resources/mdtest/class/super.md @@ -14,9 +14,16 @@ common usage. ### Explicit Super Object + + `super(pivot_class, owner)` performs attribute lookup along the MRO, starting immediately after the specified pivot class. +```toml +[environment] +python-version = "3.12" +``` + ```py class A: def a(self): ... @@ -34,21 +41,15 @@ reveal_type(C.__mro__) # revealed: tuple[, , , super(C, C()).a super(C, C()).b -# error: [unresolved-attribute] "Type `, C>` has no attribute `c`" -super(C, C()).c +super(C, C()).c # error: [unresolved-attribute] super(B, C()).a -# error: [unresolved-attribute] "Type `, C>` has no attribute `b`" -super(B, C()).b -# error: [unresolved-attribute] "Type `, C>` has no attribute `c`" -super(B, C()).c +super(B, C()).b # error: [unresolved-attribute] +super(B, C()).c # error: [unresolved-attribute] -# error: [unresolved-attribute] "Type `, C>` has no attribute `a`" -super(A, C()).a -# error: [unresolved-attribute] "Type `, C>` has no attribute `b`" -super(A, C()).b -# error: [unresolved-attribute] "Type `, C>` has no attribute `c`" -super(A, C()).c +super(A, C()).a # error: [unresolved-attribute] +super(A, C()).b # error: [unresolved-attribute] +super(A, C()).c # error: [unresolved-attribute] reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown reveal_type(super(C, C()).b) # revealed: bound method C.b() -> Unknown @@ -56,12 +57,80 @@ reveal_type(super(C, C()).aa) # revealed: int reveal_type(super(C, C()).bb) # revealed: int ``` +Examples of explicit `super()` with unusual types. We allow almost any type to be passed as the +second argument to `super()` -- the only exceptions are "pure abstract" types such as `Callable` and +synthesized `Protocol`s that cannot be upcast to, or interpreted as, a non-`object` nominal type. + +```py +import types +from typing_extensions import Callable, TypeIs, Literal, TypedDict + +def f(): ... + +class Foo[T]: + def method(self): ... + @property + def some_property(self): ... + +type Alias = int + +class SomeTypedDict(TypedDict): + x: int + y: bytes + +# revealed: , FunctionType> +reveal_type(super(object, f)) +# revealed: , WrapperDescriptorType> +reveal_type(super(object, types.FunctionType.__get__)) +# revealed: , GenericAlias> +reveal_type(super(object, Foo[int])) +# revealed: , _SpecialForm> +reveal_type(super(object, Literal)) +# revealed: , TypeAliasType> +reveal_type(super(object, Alias)) +# revealed: , MethodType> +reveal_type(super(object, Foo().method)) +# revealed: , property> +reveal_type(super(object, Foo.some_property)) + +def g(x: object) -> TypeIs[list[object]]: + return isinstance(x, list) + +def _(x: object, y: SomeTypedDict, z: Callable[[int, str], bool]): + if hasattr(x, "bar"): + # revealed: + reveal_type(x) + # error: [invalid-super-argument] + # revealed: Unknown + reveal_type(super(object, x)) + + # error: [invalid-super-argument] + # revealed: Unknown + reveal_type(super(object, z)) + + is_list = g(x) + # revealed: TypeIs[list[object] @ x] + reveal_type(is_list) + # revealed: , bool> + reveal_type(super(object, is_list)) + + # revealed: , dict[Literal["x", "y"], int | bytes]> + reveal_type(super(object, y)) +``` + ### 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`). +```toml +[environment] +python-version = "3.12" +``` + ```py from __future__ import annotations @@ -74,6 +143,7 @@ class B(A): def __init__(self, a: int): # TODO: Once `Self` is supported, this should be `, B>` reveal_type(super()) # revealed: , Unknown> + reveal_type(super(object, super())) # revealed: , super> super().__init__(a) @classmethod @@ -86,6 +156,123 @@ super(B, B(42)).__init__(42) super(B, B).f() ``` +Some examples with unusual annotations for `self` or `cls`: + +```py +import enum +from typing import Any, Self, Never, Protocol, Callable +from ty_extensions import Intersection + +class BuilderMeta(type): + def __new__( + cls: type[Any], + name: str, + bases: tuple[type, ...], + dct: dict[str, Any], + ) -> BuilderMeta: + # revealed: , Any> + s = reveal_type(super()) + # revealed: Any + return reveal_type(s.__new__(cls, name, bases, dct)) + +class BuilderMeta2(type): + def __new__( + cls: type[BuilderMeta2], + name: str, + bases: tuple[type, ...], + dct: dict[str, Any], + ) -> BuilderMeta2: + # revealed: , > + s = reveal_type(super()) + # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501) + # revealed: Unknown + return reveal_type(s.__new__(cls, name, bases, dct)) + +class Foo[T]: + x: T + + def method(self: Any): + reveal_type(super()) # revealed: , Any> + + if isinstance(self, Foo): + reveal_type(super()) # revealed: , Any> + + def method2(self: Foo[T]): + # revealed: , Foo[T@Foo]> + reveal_type(super()) + + def method3(self: Foo): + # revealed: , Foo[Unknown]> + reveal_type(super()) + + def method4(self: Self): + # revealed: , Foo[T@Foo]> + reveal_type(super()) + + def method5[S: Foo[int]](self: S, other: S) -> S: + # revealed: , Foo[int]> + reveal_type(super()) + return self + + def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: + # revealed: , Foo[int]> | , Foo[str]> + reveal_type(super()) + return self + + def method7[S](self: S, other: S) -> S: + # error: [invalid-super-argument] + # revealed: Unknown + reveal_type(super()) + return self + + def method8[S: int](self: S, other: S) -> S: + # error: [invalid-super-argument] + # revealed: Unknown + reveal_type(super()) + return self + + def method9[S: (int, str)](self: S, other: S) -> S: + # error: [invalid-super-argument] + # revealed: Unknown + reveal_type(super()) + return self + + def method10[S: Callable[..., str]](self: S, other: S) -> S: + # error: [invalid-super-argument] + # revealed: Unknown + reveal_type(super()) + return self + +type Alias = Bar + +class Bar: + def method(self: Alias): + # revealed: , Bar> + reveal_type(super()) + + def pls_dont_call_me(self: Never): + # revealed: , Unknown> + reveal_type(super()) + + def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): + # revealed: , Bar> + reveal_type(super()) + +class P(Protocol): + def method(self: P): + # revealed: , P> + reveal_type(super()) + +class E(enum.Enum): + X = 1 + + def method(self: E): + match self: + case E.X: + # revealed: , E> + reveal_type(super()) +``` + ### Unbound Super Object Calling `super(cls)` without a second argument returns an _unbound super object_. This is treated as @@ -167,11 +354,19 @@ class A: ## Built-ins and Literals ```py +from enum import Enum + reveal_type(super(bool, True)) # revealed: , bool> reveal_type(super(bool, bool())) # revealed: , bool> reveal_type(super(int, bool())) # revealed: , bool> reveal_type(super(int, 3)) # revealed: , int> reveal_type(super(str, "")) # revealed: , str> +reveal_type(super(bytes, b"")) # revealed: , bytes> + +class E(Enum): + X = 42 + +reveal_type(super(E, E.X)) # revealed: , E> ``` ## Descriptor Behavior with Super @@ -342,7 +537,7 @@ def f(x: 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 `` in `super(, Literal[""])` call" +# error: [invalid-super-argument] "`str` is not an instance or subclass of `` in `super(, str)` call" # revealed: Unknown reveal_type(super(int, str())) 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 new file mode 100644 index 0000000000..339c9a59a7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Explicit_Super_Objec…_(b753048091f275c0).snap @@ -0,0 +1,217 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: super.md - Super - Basic Usage - Explicit Super Object +mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md +--- + +# Python source files + +## 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)) +``` + +# Diagnostics + +``` +error[unresolved-attribute]: Type `, C>` has no attribute `c` + --> src/mdtest_snippet.py:17:1 + | +15 | super(C, C()).a +16 | super(C, C()).b +17 | super(C, C()).c # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^^ +18 | +19 | super(B, C()).a + | +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Type `, C>` has no attribute `b` + --> src/mdtest_snippet.py:20:1 + | +19 | super(B, C()).a +20 | super(B, C()).b # error: [unresolved-attribute] + | ^^^^^^^^^^^^^^^ +21 | super(B, C()).c # error: [unresolved-attribute] + | +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Type `, C>` has no attribute `c` + --> src/mdtest_snippet.py:21:1 + | +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] + | +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Type `, C>` has no attribute `a` + --> src/mdtest_snippet.py:23:1 + | +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] + | +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Type `, C>` has no attribute `b` + --> src/mdtest_snippet.py:24: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] + | +info: rule `unresolved-attribute` is enabled by default + +``` + +``` +error[unresolved-attribute]: Type `, C>` has no attribute `c` + --> src/mdtest_snippet.py:25: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] + | ^^^^^^^^^^^^^^^ +26 | +27 | reveal_type(super(C, C()).a) # revealed: bound method C.a() -> Unknown + | +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 + | +69 | # error: [invalid-super-argument] +70 | # revealed: Unknown +71 | reveal_type(super(object, x)) + | ^^^^^^^^^^^^^^^^ +72 | +73 | # error: [invalid-super-argument] + | +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 + | +73 | # error: [invalid-super-argument] +74 | # revealed: Unknown +75 | reveal_type(super(object, z)) + | ^^^^^^^^^^^^^^^^ +76 | +77 | is_list = g(x) + | +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 new file mode 100644 index 0000000000..846e4bdbb7 --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/super.md_-_Super_-_Basic_Usage_-_Implicit_Super_Objec…_(f9e5e48e3a4a4c12).snap @@ -0,0 +1,214 @@ +--- +source: crates/ty_test/src/lib.rs +expression: snapshot +--- +--- +mdtest name: super.md - Super - Basic Usage - Implicit Super Object +mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from __future__ import annotations + 2 | + 3 | class A: + 4 | def __init__(self, a: int): ... + 5 | @classmethod + 6 | def f(cls): ... + 7 | + 8 | class B(A): + 9 | def __init__(self, a: int): + 10 | # TODO: Once `Self` is supported, this should be `, B>` + 11 | reveal_type(super()) # revealed: , Unknown> + 12 | reveal_type(super(object, super())) # revealed: , super> + 13 | super().__init__(a) + 14 | + 15 | @classmethod + 16 | def f(cls): + 17 | # TODO: Once `Self` is supported, this should be `, >` + 18 | reveal_type(super()) # revealed: , Unknown> + 19 | super().f() + 20 | + 21 | super(B, B(42)).__init__(42) + 22 | super(B, B).f() + 23 | import enum + 24 | from typing import Any, Self, Never, Protocol, Callable + 25 | from ty_extensions import Intersection + 26 | + 27 | class BuilderMeta(type): + 28 | def __new__( + 29 | cls: type[Any], + 30 | name: str, + 31 | bases: tuple[type, ...], + 32 | dct: dict[str, Any], + 33 | ) -> BuilderMeta: + 34 | # revealed: , Any> + 35 | s = reveal_type(super()) + 36 | # revealed: Any + 37 | return reveal_type(s.__new__(cls, name, bases, dct)) + 38 | + 39 | class BuilderMeta2(type): + 40 | def __new__( + 41 | cls: type[BuilderMeta2], + 42 | name: str, + 43 | bases: tuple[type, ...], + 44 | dct: dict[str, Any], + 45 | ) -> BuilderMeta2: + 46 | # revealed: , > + 47 | s = reveal_type(super()) + 48 | # TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501) + 49 | # revealed: Unknown + 50 | return reveal_type(s.__new__(cls, name, bases, dct)) + 51 | + 52 | class Foo[T]: + 53 | x: T + 54 | + 55 | def method(self: Any): + 56 | reveal_type(super()) # revealed: , Any> + 57 | + 58 | if isinstance(self, Foo): + 59 | reveal_type(super()) # revealed: , Any> + 60 | + 61 | def method2(self: Foo[T]): + 62 | # revealed: , Foo[T@Foo]> + 63 | reveal_type(super()) + 64 | + 65 | def method3(self: Foo): + 66 | # revealed: , Foo[Unknown]> + 67 | reveal_type(super()) + 68 | + 69 | def method4(self: Self): + 70 | # revealed: , Foo[T@Foo]> + 71 | reveal_type(super()) + 72 | + 73 | def method5[S: Foo[int]](self: S, other: S) -> S: + 74 | # revealed: , Foo[int]> + 75 | reveal_type(super()) + 76 | return self + 77 | + 78 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S: + 79 | # revealed: , Foo[int]> | , Foo[str]> + 80 | reveal_type(super()) + 81 | return self + 82 | + 83 | def method7[S](self: S, other: S) -> S: + 84 | # error: [invalid-super-argument] + 85 | # revealed: Unknown + 86 | reveal_type(super()) + 87 | return self + 88 | + 89 | def method8[S: int](self: S, other: S) -> S: + 90 | # error: [invalid-super-argument] + 91 | # revealed: Unknown + 92 | reveal_type(super()) + 93 | return self + 94 | + 95 | def method9[S: (int, str)](self: S, other: S) -> S: + 96 | # error: [invalid-super-argument] + 97 | # revealed: Unknown + 98 | reveal_type(super()) + 99 | return self +100 | +101 | def method10[S: Callable[..., str]](self: S, other: S) -> S: +102 | # error: [invalid-super-argument] +103 | # revealed: Unknown +104 | reveal_type(super()) +105 | return self +106 | +107 | type Alias = Bar +108 | +109 | class Bar: +110 | def method(self: Alias): +111 | # revealed: , Bar> +112 | reveal_type(super()) +113 | +114 | def pls_dont_call_me(self: Never): +115 | # revealed: , Unknown> +116 | reveal_type(super()) +117 | +118 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]): +119 | # revealed: , Bar> +120 | reveal_type(super()) +121 | +122 | class P(Protocol): +123 | def method(self: P): +124 | # revealed: , P> +125 | reveal_type(super()) +126 | +127 | class E(enum.Enum): +128 | X = 1 +129 | +130 | def method(self: E): +131 | match self: +132 | case E.X: +133 | # revealed: , E> +134 | reveal_type(super()) +``` + +# Diagnostics + +``` +error[invalid-super-argument]: `S@method7` is not an instance or subclass of `` in `super(, S@method7)` call + --> src/mdtest_snippet.py:86:21 + | +84 | # error: [invalid-super-argument] +85 | # revealed: Unknown +86 | reveal_type(super()) + | ^^^^^^^ +87 | return self + | +info: Type variable `S` has `object` as its implicit upper bound +info: `object` is not an instance or subclass of `` +info: rule `invalid-super-argument` is enabled by default + +``` + +``` +error[invalid-super-argument]: `S@method8` is not an instance or subclass of `` in `super(, S@method8)` call + --> src/mdtest_snippet.py:92:21 + | +90 | # error: [invalid-super-argument] +91 | # revealed: Unknown +92 | reveal_type(super()) + | ^^^^^^^ +93 | return self + | +info: Type variable `S` has upper bound `int` +info: `int` is not an instance or subclass of `` +info: rule `invalid-super-argument` is enabled by default + +``` + +``` +error[invalid-super-argument]: `S@method9` is not an instance or subclass of `` in `super(, S@method9)` call + --> src/mdtest_snippet.py:98:21 + | +96 | # error: [invalid-super-argument] +97 | # revealed: Unknown +98 | reveal_type(super()) + | ^^^^^^^ +99 | return self + | +info: Type variable `S` has constraints `int, str` +info: `int | str` is not an instance or subclass of `` +info: rule `invalid-super-argument` is enabled by default + +``` + +``` +error[invalid-super-argument]: `S@method10` is a type variable with an abstract/structural type as its bounds or constraints, in `super(, S@method10)` call + --> src/mdtest_snippet.py:104:21 + | +102 | # error: [invalid-super-argument] +103 | # revealed: Unknown +104 | reveal_type(super()) + | ^^^^^^^ +105 | return self + | +info: Type variable `S` has upper bound `(...) -> str` +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 1c3aef00c5..5dd17d692d 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -1020,6 +1020,13 @@ impl<'db> Type<'db> { .expect("Expected a Type::Dynamic variant") } + pub(crate) const fn into_protocol_instance(self) -> Option> { + match self { + Type::ProtocolInstance(instance) => Some(instance), + _ => None, + } + } + #[track_caller] pub(crate) fn expect_class_literal(self) -> ClassLiteral<'db> { self.into_class_literal() @@ -11248,21 +11255,62 @@ impl<'db> EnumLiteralType<'db> { } } +/// Enumeration of ways in which a `super()` call can cause us to emit a diagnostic. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum BoundSuperError<'db> { - InvalidPivotClassType { + /// The second argument to `super()` (which may have been implicitly provided by + /// the Python interpreter) has an abstract or structural type. + /// It's impossible to determine whether a `Callable` type or a synthesized protocol + /// type is an instance or subclass of the pivot class, so these are rejected. + AbstractOwnerType { + owner_type: Type<'db>, pivot_class: Type<'db>, + /// If `owner_type` is a type variable, this contains the type variable instance + typevar_context: Option>, }, + /// The first argument to `super()` (which may have been implicitly provided by + /// the Python interpreter) is not a valid class type. + InvalidPivotClassType { pivot_class: Type<'db> }, + /// The second argument to `super()` was not a subclass or instance of the first argument. + /// (Note that both arguments may have been implicitly provided by the Python interpreter.) FailingConditionCheck { pivot_class: Type<'db>, owner: Type<'db>, + /// If `owner_type` is a type variable, this contains the type variable instance + typevar_context: Option>, }, + /// It was a single-argument `super()` call, but we were unable to determine + /// the implicit arguments provided by the Python interpreter. UnavailableImplicitArguments, } -impl BoundSuperError<'_> { - pub(super) fn report_diagnostic(&self, context: &InferContext, node: AnyNodeRef) { +impl<'db> BoundSuperError<'db> { + pub(super) fn report_diagnostic(&self, context: &'db InferContext<'db, '_>, node: AnyNodeRef) { match self { + BoundSuperError::AbstractOwnerType { + owner_type, + pivot_class, + typevar_context, + } => { + if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { + if let Some(typevar_context) = typevar_context { + let mut diagnostic = builder.into_diagnostic(format_args!( + "`{owner}` is a type variable with an abstract/structural type as \ + its bounds or constraints, in `super({pivot_class}, {owner})` call", + pivot_class = pivot_class.display(context.db()), + owner = owner_type.display(context.db()), + )); + Self::describe_typevar(context.db(), &mut diagnostic, *typevar_context); + } else { + builder.into_diagnostic(format_args!( + "`{owner}` is an abstract/structural type in \ + `super({pivot_class}, {owner})` call", + pivot_class = pivot_class.display(context.db()), + owner = owner_type.display(context.db()), + )); + } + } + } BoundSuperError::InvalidPivotClassType { pivot_class } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { builder.into_diagnostic(format_args!( @@ -11271,14 +11319,28 @@ impl BoundSuperError<'_> { )); } } - BoundSuperError::FailingConditionCheck { pivot_class, owner } => { + BoundSuperError::FailingConditionCheck { + pivot_class, + owner, + typevar_context, + } => { if let Some(builder) = context.report_lint(&INVALID_SUPER_ARGUMENT, node) { - builder.into_diagnostic(format_args!( + let mut diagnostic = builder.into_diagnostic(format_args!( "`{owner}` is not an instance or subclass of \ - `{pivot_class}` in `super({pivot_class}, {owner})` call", + `{pivot_class}` in `super({pivot_class}, {owner})` call", pivot_class = pivot_class.display(context.db()), owner = owner.display(context.db()), )); + if let Some(typevar_context) = typevar_context { + let bound_or_constraints_union = + Self::describe_typevar(context.db(), &mut diagnostic, *typevar_context); + diagnostic.info(format_args!( + "`{bounds_or_constraints}` is not an instance or subclass of `{pivot_class}`", + bounds_or_constraints = + bound_or_constraints_union.display(context.db()), + pivot_class = pivot_class.display(context.db()), + )); + } } } BoundSuperError::UnavailableImplicitArguments => { @@ -11292,6 +11354,44 @@ impl BoundSuperError<'_> { } } } + + /// Add an `info`-level diagnostic describing the bounds or constraints, + /// and return the type variable's upper bound or the union of its constraints. + fn describe_typevar( + db: &'db dyn Db, + diagnostic: &mut Diagnostic, + type_var: TypeVarInstance<'db>, + ) -> Type<'db> { + match type_var.bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + diagnostic.info(format_args!( + "Type variable `{}` has upper bound `{}`", + type_var.name(db), + bound.display(db) + )); + bound + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + diagnostic.info(format_args!( + "Type variable `{}` has constraints `{}`", + type_var.name(db), + constraints + .elements(db) + .iter() + .map(|c| c.display(db)) + .join(", ") + )); + Type::Union(constraints) + } + None => { + diagnostic.info(format_args!( + "Type variable `{}` has `object` as its implicit upper bound", + type_var.name(db) + )); + Type::object() + } + } + } } #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, get_size2::GetSize)] @@ -11326,14 +11426,6 @@ impl<'db> SuperOwnerKind<'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, db: &'db dyn Db) -> Option> { match self { SuperOwnerKind::Dynamic(_) => None, @@ -11341,35 +11433,6 @@ impl<'db> SuperOwnerKind<'db> { SuperOwnerKind::Instance(instance) => Some(instance.class(db)), } } - - 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::NominalInstance(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::SpecialForm(special_form) => { - SuperOwnerKind::try_from_type(db, special_form.instance_fallback(db)) - } - _ => None, - } - } } impl<'db> From> for Type<'db> { @@ -11397,8 +11460,8 @@ fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( bound_super: BoundSuperType<'db>, visitor: &V, ) { - visitor.visit_type(db, bound_super.pivot_class(db).into()); - visitor.visit_type(db, bound_super.owner(db).into_type()); + visitor.visit_type(db, Type::from(bound_super.pivot_class(db))); + visitor.visit_type(db, Type::from(bound_super.owner(db))); } impl<'db> BoundSuperType<'db> { @@ -11414,16 +11477,171 @@ impl<'db> BoundSuperType<'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 + let delegate_to = + |type_to_delegate_to| BoundSuperType::build(db, pivot_class_type, type_to_delegate_to); + + let delegate_with_error_mapped = + |type_to_delegate_to, error_context: Option>| { + delegate_to(type_to_delegate_to).map_err(|err| match err { + BoundSuperError::AbstractOwnerType { + owner_type: _, + pivot_class: _, + typevar_context: _, + } => BoundSuperError::AbstractOwnerType { + owner_type, + pivot_class: pivot_class_type, + typevar_context: error_context, + }, + BoundSuperError::FailingConditionCheck { + pivot_class, + owner: _, + typevar_context: _, + } => BoundSuperError::FailingConditionCheck { + pivot_class, + owner: owner_type, + typevar_context: error_context, + }, + BoundSuperError::InvalidPivotClassType { pivot_class } => { + BoundSuperError::InvalidPivotClassType { pivot_class } + } + BoundSuperError::UnavailableImplicitArguments => { + BoundSuperError::UnavailableImplicitArguments + } + }) + }; + + let owner = match owner_type { + Type::Never => SuperOwnerKind::Dynamic(DynamicType::Unknown), + Type::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), + Type::ClassLiteral(class) => SuperOwnerKind::Class(ClassType::NonGeneric(class)), + Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() { + SubclassOfInner::Class(class) => SuperOwnerKind::Class(class), + SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic), + }, + Type::NominalInstance(instance) => SuperOwnerKind::Instance(instance), + + Type::ProtocolInstance(protocol) => { + if let Some(nominal_instance) = protocol.as_nominal_type() { + SuperOwnerKind::Instance(nominal_instance) + } else { + return Err(BoundSuperError::AbstractOwnerType { + owner_type, + pivot_class: pivot_class_type, + typevar_context: None, + }); + } + } + + Type::Union(union) => { + return Ok(union .elements(db) .iter() - .map(|ty| BoundSuperType::build(db, pivot_class_type, *ty)) - .collect::, _>>()?, - )); - } + .try_fold(UnionBuilder::new(db), |builder, element| { + delegate_to(*element).map(|ty| builder.add(ty)) + })? + .build()); + } + Type::Intersection(intersection) => { + let mut builder = IntersectionBuilder::new(db); + let mut one_good_element_found = false; + for positive in intersection.positive(db) { + if let Ok(good_element) = delegate_to(*positive) { + one_good_element_found = true; + builder = builder.add_positive(good_element); + } + } + if !one_good_element_found { + return Err(BoundSuperError::AbstractOwnerType { + owner_type, + pivot_class: pivot_class_type, + typevar_context: None, + }); + } + for negative in intersection.negative(db) { + if let Ok(good_element) = delegate_to(*negative) { + builder = builder.add_negative(good_element); + } + } + return Ok(builder.build()); + } + Type::TypeAlias(alias) => { + return delegate_with_error_mapped(alias.value_type(db), None); + } + Type::TypeVar(type_var) | Type::NonInferableTypeVar(type_var) => { + let type_var = type_var.typevar(db); + return match type_var.bound_or_constraints(db) { + Some(TypeVarBoundOrConstraints::UpperBound(bound)) => { + delegate_with_error_mapped(bound, Some(type_var)) + } + Some(TypeVarBoundOrConstraints::Constraints(constraints)) => { + delegate_with_error_mapped(Type::Union(constraints), Some(type_var)) + } + None => delegate_with_error_mapped(Type::object(), Some(type_var)), + }; + } + Type::BooleanLiteral(_) | Type::TypeIs(_) => { + return delegate_to(KnownClass::Bool.to_instance(db)); + } + Type::IntLiteral(_) => return delegate_to(KnownClass::Int.to_instance(db)), + Type::StringLiteral(_) | Type::LiteralString => { + return delegate_to(KnownClass::Str.to_instance(db)); + } + Type::BytesLiteral(_) => { + return delegate_to(KnownClass::Bytes.to_instance(db)); + } + Type::SpecialForm(special_form) => { + return delegate_to(special_form.instance_fallback(db)); + } + Type::KnownInstance(instance) => { + return delegate_to(instance.instance_fallback(db)); + } + Type::FunctionLiteral(_) | Type::DataclassDecorator(_) => { + return delegate_to(KnownClass::FunctionType.to_instance(db)); + } + Type::WrapperDescriptor(_) => { + return delegate_to(KnownClass::WrapperDescriptorType.to_instance(db)); + } + Type::KnownBoundMethod(method) => { + return delegate_to(method.class().to_instance(db)); + } + Type::BoundMethod(_) => return delegate_to(KnownClass::MethodType.to_instance(db)), + Type::ModuleLiteral(_) => { + return delegate_to(KnownClass::ModuleType.to_instance(db)); + } + Type::GenericAlias(_) => return delegate_to(KnownClass::GenericAlias.to_instance(db)), + Type::PropertyInstance(_) => return delegate_to(KnownClass::Property.to_instance(db)), + Type::EnumLiteral(enum_literal_type) => { + return delegate_to(enum_literal_type.enum_class_instance(db)); + } + Type::BoundSuper(_) => return delegate_to(KnownClass::Super.to_instance(db)), + Type::TypedDict(td) => { + // In general it isn't sound to upcast a `TypedDict` to a `dict`, + // but here it seems like it's probably sound? + let mut key_builder = UnionBuilder::new(db); + let mut value_builder = UnionBuilder::new(db); + for (name, field) in td.items(db) { + key_builder = key_builder.add(Type::string_literal(db, &name)); + value_builder = value_builder.add(field.declared_ty); + } + return delegate_to( + KnownClass::Dict + .to_specialized_instance(db, [key_builder.build(), value_builder.build()]), + ); + } + Type::Callable(callable) if callable.is_function_like(db) => { + return delegate_to(KnownClass::FunctionType.to_instance(db)); + } + Type::AlwaysFalsy + | Type::AlwaysTruthy + | Type::Callable(_) + | Type::DataclassTransformer(_) => { + return Err(BoundSuperError::AbstractOwnerType { + owner_type, + pivot_class: pivot_class_type, + typevar_context: None, + }); + } + }; // We don't use `Classbase::try_from_type` here because: // - There are objects that may validly be present in a class's bases list @@ -11452,24 +11670,22 @@ impl<'db> BoundSuperType<'db> { } }; - 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(db) 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, - })?; + if let Some(pivot_class) = pivot_class.into_class() + && let Some(owner_class) = owner.into_class(db) + { + let pivot_class = pivot_class.class_literal(db).0; + if !owner_class.iter_mro(db).any(|superclass| match superclass { + ClassBase::Dynamic(_) => true, + ClassBase::Generic | ClassBase::Protocol | ClassBase::TypedDict => false, + ClassBase::Class(superclass) => superclass.class_literal(db).0 == pivot_class, + }) { + return Err(BoundSuperError::FailingConditionCheck { + pivot_class: pivot_class_type, + owner: owner_type, + typevar_context: None, + }); + } + } Ok(Type::BoundSuper(BoundSuperType::new( db, @@ -11525,19 +11741,22 @@ impl<'db> BoundSuperType<'db> { db, attribute, Type::none(db), - owner.into_type(), + Type::from(owner), ) .0, ), - SuperOwnerKind::Instance(_) => Some( - Type::try_call_dunder_get_on_attribute( - db, - attribute, - owner.into_type(), - owner.into_type().to_meta_type(db), + SuperOwnerKind::Instance(_) => { + let owner = Type::from(owner); + Some( + Type::try_call_dunder_get_on_attribute( + db, + attribute, + owner, + owner.to_meta_type(db), + ) + .0, ) - .0, - ), + } } } @@ -11551,9 +11770,8 @@ impl<'db> BoundSuperType<'db> { ) -> PlaceAndQualifiers<'db> { let owner = self.owner(db); let class = match owner { - SuperOwnerKind::Dynamic(_) => { - return owner - .into_type() + SuperOwnerKind::Dynamic(dynamic) => { + return Type::Dynamic(dynamic) .find_name_in_mro_with_policy(db, name, policy) .expect("Calling `find_name_in_mro` on dynamic type should return `Some`"); } diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 1bee509e26..0a25401fe9 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -571,9 +571,7 @@ impl Display for DisplayRepresentation<'_> { "", pivot = Type::from(bound_super.pivot_class(self.db)) .display_with(self.db, self.settings.singleline()), - owner = bound_super - .owner(self.db) - .into_type() + owner = Type::from(bound_super.owner(self.db)) .display_with(self.db, self.settings.singleline()) ) } diff --git a/crates/ty_python_semantic/src/types/instance.rs b/crates/ty_python_semantic/src/types/instance.rs index 7d7998530e..a7515898d1 100644 --- a/crates/ty_python_semantic/src/types/instance.rs +++ b/crates/ty_python_semantic/src/types/instance.rs @@ -156,31 +156,27 @@ impl<'db> Type<'db> { // This matches the behaviour of other type checkers, and is required for us to // recognise `str` as a subtype of `Container[str]`. structurally_satisfied.or(db, || { - if let Protocol::FromClass(class) = protocol.inner { - // if `self` and `other` are *both* protocols, we also need to treat `self` as if it - // were a nominal type, or we won't consider a protocol `P` that explicitly inherits - // from a protocol `Q` to be a subtype of `Q` to be a subtype of `Q` if it overrides - // `Q`'s members in a Liskov-incompatible way. - let type_to_test = if let Type::ProtocolInstance(ProtocolInstanceType { - inner: Protocol::FromClass(class), - .. - }) = self - { - Type::non_tuple_instance(db, class) - } else { - self - }; + let Some(nominal_instance) = protocol.as_nominal_type() else { + return ConstraintSet::from(false); + }; - type_to_test.has_relation_to_impl( - db, - Type::non_tuple_instance(db, class), - relation, - relation_visitor, - disjointness_visitor, - ) - } else { - ConstraintSet::from(false) - } + // if `self` and `other` are *both* protocols, we also need to treat `self` as if it + // were a nominal type, or we won't consider a protocol `P` that explicitly inherits + // from a protocol `Q` to be a subtype of `Q` to be a subtype of `Q` if it overrides + // `Q`'s members in a Liskov-incompatible way. + let type_to_test = self + .into_protocol_instance() + .and_then(ProtocolInstanceType::as_nominal_type) + .map(Type::NominalInstance) + .unwrap_or(self); + + type_to_test.has_relation_to_impl( + db, + Type::NominalInstance(nominal_instance), + relation, + relation_visitor, + disjointness_visitor, + ) }) } } @@ -605,6 +601,20 @@ impl<'db> ProtocolInstanceType<'db> { } } + /// If this is a class-based protocol, convert the protocol-instance into a nominal instance. + /// + /// If this is a synthesized protocol that does not correspond to a class definition + /// in source code, return `None`. These are "pure" abstract types, that cannot be + /// treated in a nominal way. + pub(super) fn as_nominal_type(self) -> Option> { + match self.inner { + Protocol::FromClass(class) => { + Some(NominalInstanceType(NominalInstanceInner::NonTuple(class))) + } + Protocol::Synthesized(_) => None, + } + } + /// Return the meta-type of this protocol-instance type. pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { match self.inner {