From 2f64ef9c724c554a39630b9bb8dfe3c46ebc9929 Mon Sep 17 00:00:00 2001 From: Dex Devlon <51504045+bxff@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:59:08 +0530 Subject: [PATCH] [ty] Include type parameters in generic callable display (#22435) --- crates/ty_ide/src/completion.rs | 44 ++--- .../resources/mdtest/call/methods.md | 10 +- .../mdtest/dataclasses/dataclasses.md | 2 +- .../mdtest/generics/legacy/classes.md | 2 +- .../mdtest/generics/pep695/classes.md | 2 +- .../mdtest/generics/pep695/functions.md | 4 +- .../resources/mdtest/generics/scoping.md | 25 ++- .../resources/mdtest/named_tuple.md | 2 +- .../resources/mdtest/overloads.md | 2 +- .../resources/mdtest/scopes/builtin.md | 2 +- ..._Cover_non-keyword_re…_(707b284610419a54).snap | 2 +- crates/ty_python_semantic/src/types.rs | 32 +++- .../ty_python_semantic/src/types/display.rs | 153 +++++++++++++++--- .../src/types/signatures.rs | 13 ++ 14 files changed, 230 insertions(+), 65 deletions(-) diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index 85f2ac819e..0132f79e92 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -3586,7 +3586,7 @@ quux. __init_subclass__ :: bound method type[Quux].__init_subclass__() -> None __module__ :: str __ne__ :: bound method Quux.__ne__(value: object, /) -> bool - __new__ :: def __new__(cls) -> Self@__new__ + __new__ :: def __new__[Self](cls) -> Self __reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...] __reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...] __repr__ :: bound method Quux.__repr__() -> str @@ -3667,19 +3667,19 @@ C. __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool - __new__ :: def __new__(cls) -> Self@__new__ - __or__ :: bound method .__or__[Self](value: Any, /) -> UnionType | Self@__or__ + __new__ :: def __new__[Self](cls) -> Self + __or__ :: bound method .__or__[Self](value: Any, /) -> UnionType | Self __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] __qualname__ :: str __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] __repr__ :: def __repr__(self) -> str - __ror__ :: bound method .__ror__[Self](value: Any, /) -> UnionType | Self@__ror__ + __ror__ :: bound method .__ror__[Self](value: Any, /) -> UnionType | Self __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None __sizeof__ :: def __sizeof__(self) -> int __str__ :: def __str__(self) -> str __subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool - __subclasses__ :: bound method .__subclasses__[Self]() -> list[Self@__subclasses__] + __subclasses__ :: bound method .__subclasses__[Self]() -> list[Self] __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool __text_signature__ :: str | None __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] @@ -3737,18 +3737,18 @@ Meta. __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool - __or__ :: def __or__[Self](self: Self@__or__, value: Any, /) -> UnionType | Self@__or__ + __or__ :: def __or__[Self](self: Self, value: Any, /) -> UnionType | Self __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] __qualname__ :: str __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] __repr__ :: def __repr__(self) -> str - __ror__ :: def __ror__[Self](self: Self@__ror__, value: Any, /) -> UnionType | Self@__ror__ + __ror__ :: def __ror__[Self](self: Self, value: Any, /) -> UnionType | Self __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None __sizeof__ :: def __sizeof__(self) -> int __str__ :: def __str__(self) -> str __subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool - __subclasses__ :: def __subclasses__[Self](self: Self@__subclasses__) -> list[Self@__subclasses__] + __subclasses__ :: def __subclasses__[Self](self: Self) -> list[Self] __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool __text_signature__ :: str | None __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] @@ -3866,19 +3866,19 @@ Quux. __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool - __new__ :: def __new__(cls) -> Self@__new__ - __or__ :: bound method .__or__[Self](value: Any, /) -> UnionType | Self@__or__ + __new__ :: def __new__[Self](cls) -> Self + __or__ :: bound method .__or__[Self](value: Any, /) -> UnionType | Self __prepare__ :: bound method .__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object] __qualname__ :: str __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] __reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...] __repr__ :: def __repr__(self) -> str - __ror__ :: bound method .__ror__[Self](value: Any, /) -> UnionType | Self@__ror__ + __ror__ :: bound method .__ror__[Self](value: Any, /) -> UnionType | Self __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None __sizeof__ :: def __sizeof__(self) -> int __str__ :: def __str__(self) -> str __subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool - __subclasses__ :: bound method .__subclasses__[Self]() -> list[Self@__subclasses__] + __subclasses__ :: bound method .__subclasses__[Self]() -> list[Self] __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool __text_signature__ :: str | None __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] @@ -3919,8 +3919,8 @@ Answer. __bool__ :: bound method .__bool__() -> Literal[True] __class__ :: __contains__ :: bound method .__contains__(value: object) -> bool - __copy__ :: def __copy__(self) -> Self@__copy__ - __deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__ + __copy__ :: def __copy__[Self](self) -> Self + __deepcopy__ :: def __deepcopy__[Self](self, memo: Any) -> Self __delattr__ :: def __delattr__(self, name: str, /) -> None __dict__ :: dict[str, Any] __dictoffset__ :: int @@ -3930,34 +3930,34 @@ Answer. __flags__ :: int __format__ :: def __format__(self, format_spec: str) -> str __getattribute__ :: def __getattribute__(self, name: str, /) -> Any - __getitem__ :: bound method .__getitem__[_EnumMemberT](name: str) -> _EnumMemberT@__getitem__ + __getitem__ :: bound method .__getitem__[_EnumMemberT](name: str) -> _EnumMemberT __getstate__ :: def __getstate__(self) -> object __hash__ :: def __hash__(self) -> int __init__ :: def __init__(self) -> None __init_subclass__ :: bound method .__init_subclass__() -> None __instancecheck__ :: bound method .__instancecheck__(instance: Any, /) -> bool __itemsize__ :: int - __iter__ :: bound method .__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__] + __iter__ :: bound method .__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT] __len__ :: bound method .__len__() -> int __members__ :: MappingProxyType[str, Answer] __module__ :: str __mro__ :: tuple[type, ...] __name__ :: str __ne__ :: def __ne__(self, value: object, /) -> bool - __new__ :: def __new__(cls, value: object) -> Self@__new__ - __or__ :: bound method .__or__[Self](value: Any, /) -> UnionType | Self@__or__ + __new__ :: def __new__[Self](cls, value: object) -> Self + __or__ :: bound method .__or__[Self](value: Any, /) -> UnionType | Self __order__ :: str __prepare__ :: bound method .__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict __qualname__ :: str __reduce__ :: def __reduce__(self) -> str | tuple[Any, ...] __repr__ :: def __repr__(self) -> str - __reversed__ :: bound method .__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__] - __ror__ :: bound method .__ror__[Self](value: Any, /) -> UnionType | Self@__ror__ + __reversed__ :: bound method .__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT] + __ror__ :: bound method .__ror__[Self](value: Any, /) -> UnionType | Self __setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None __sizeof__ :: def __sizeof__(self) -> int __str__ :: def __str__(self) -> str __subclasscheck__ :: bound method .__subclasscheck__(subclass: type, /) -> bool - __subclasses__ :: bound method .__subclasses__[Self]() -> list[Self@__subclasses__] + __subclasses__ :: bound method .__subclasses__[Self]() -> list[Self] __subclasshook__ :: bound method .__subclasshook__(subclass: type, /) -> bool __text_signature__ :: str | None __type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...] @@ -3999,7 +3999,7 @@ quux. index :: bound method Quux.index(value: Any, start: SupportsIndex = 0, stop: SupportsIndex = ..., /) -> int x :: int y :: str - __add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]] + __add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], [_T](value: tuple[_T, ...], /) -> tuple[int | str | _T, ...]] __annotations__ :: dict[str, Any] __class__ :: type[Quux] __class_getitem__ :: bound method type[Quux].__class_getitem__(item: Any, /) -> GenericAlias diff --git a/crates/ty_python_semantic/resources/mdtest/call/methods.md b/crates/ty_python_semantic/resources/mdtest/call/methods.md index 63341a6a09..fea7c85a57 100644 --- a/crates/ty_python_semantic/resources/mdtest/call/methods.md +++ b/crates/ty_python_semantic/resources/mdtest/call/methods.md @@ -786,17 +786,17 @@ argument: ```py from typing_extensions import Self -reveal_type(object.__new__) # revealed: def __new__(cls) -> Self@__new__ -reveal_type(object().__new__) # revealed: def __new__(cls) -> Self@__new__ -# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] +reveal_type(object.__new__) # revealed: def __new__[Self](cls) -> Self +reveal_type(object().__new__) # revealed: def __new__[Self](cls) -> Self +# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self] reveal_type(int.__new__) -# revealed: Overload[(cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self@__new__, (cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self@__new__] +# revealed: Overload[[Self](cls, x: str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc = 0, /) -> Self, [Self](cls, x: str | bytes | bytearray, /, base: SupportsIndex) -> Self] reveal_type((42).__new__) class X: def __init__(self, val: int): ... def make_another(self) -> Self: - reveal_type(self.__new__) # revealed: def __new__(cls) -> Self@__new__ + reveal_type(self.__new__) # revealed: def __new__[Self](cls) -> Self return self.__new__(type(self)) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index f8fe27ebdf..6c352b1b75 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -1208,7 +1208,7 @@ def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]: # revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None reveal_type(ParentDataclass.__init__) -# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None +# revealed: [T](self: ParentDataclass[T], value: T) -> None reveal_type(ChildOfParentDataclass.__init__) result_int = uses_dataclass(42) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md index 30d6a89ec0..464f7a3fc8 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md @@ -817,7 +817,7 @@ class WithOverloadedMethod(Generic[T]): def method(self, x: S | T) -> S | T: return x -# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int] +# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int] reveal_type(WithOverloadedMethod[int].method) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md index 06680d2168..c0e7db96b1 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md @@ -702,7 +702,7 @@ class WithOverloadedMethod[T]: def method[S](self, x: S | T) -> S | T: return x -# revealed: Overload[(self, x: int) -> int, (self, x: S@method) -> S@method | int] +# revealed: Overload[(self, x: int) -> int, [S](self, x: S) -> S | int] reveal_type(WithOverloadedMethod[int].method) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md index d979beeb12..180af1497a 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/pep695/functions.md @@ -867,7 +867,7 @@ class ClassWithOverloadedInit[T]: # overload. We would then also have to determine that R must be equal to the return type of **P's # solution. -# revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]] +# revealed: Overload[[T](x: int) -> ClassWithOverloadedInit[int], [T](x: str) -> ClassWithOverloadedInit[str]] reveal_type(into_callable(ClassWithOverloadedInit)) # TODO: revealed: Overload[(x: int) -> ClassWithOverloadedInit[int], (x: str) -> ClassWithOverloadedInit[str]] # revealed: Overload[(x: int) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str], (x: str) -> ClassWithOverloadedInit[int] | ClassWithOverloadedInit[str]] @@ -888,7 +888,7 @@ class GenericClass[T]: def _(x: list[str]): # TODO: This fails because we are not propagating GenericClass's generic context into the # Callable that we create for it. - # revealed: (x: list[T@GenericClass], y: list[T@GenericClass]) -> GenericClass[T@GenericClass] + # revealed: [T](x: list[T], y: list[T]) -> GenericClass[T] reveal_type(into_callable(GenericClass)) # revealed: ty_extensions.GenericContext[T@GenericClass] reveal_type(generic_context(into_callable(GenericClass))) diff --git a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md index bcc807e609..f4a8b3b889 100644 --- a/crates/ty_python_semantic/resources/mdtest/generics/scoping.md +++ b/crates/ty_python_semantic/resources/mdtest/generics/scoping.md @@ -153,7 +153,7 @@ already solved and specialized when the class was specialized: from ty_extensions import generic_context legacy.m("string", None) # error: [invalid-argument-type] -reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S@m) -> S@m +reveal_type(legacy.m) # revealed: bound method Legacy[int].m[S](x: int, y: S) -> S # revealed: ty_extensions.GenericContext[T@Legacy] reveal_type(generic_context(Legacy)) # revealed: ty_extensions.GenericContext[Self@m, S@m] @@ -344,4 +344,27 @@ class C[T]: ok2: Inner[T] ``` +## Mixed-scope type parameters + +Methods can have type parameters that are scoped to the method itself, while also referring to type +parameters from the enclosing class. + +```py +from typing import Generic, TypeVar + +from ty_extensions import into_callable + +T = TypeVar("T") +S = TypeVar("S") + +class Foo(Generic[T]): + def bar(self, x: T, y: S) -> tuple[T, S]: + raise NotImplementedError + +def f(x: type[Foo[T]]) -> T: + # revealed: [S](self, x: T@f, y: S) -> tuple[T@f, S] + reveal_type(into_callable(x.bar)) + raise NotImplementedError +``` + [scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables diff --git a/crates/ty_python_semantic/resources/mdtest/named_tuple.md b/crates/ty_python_semantic/resources/mdtest/named_tuple.md index 7034a74176..cb0f201989 100644 --- a/crates/ty_python_semantic/resources/mdtest/named_tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/named_tuple.md @@ -348,7 +348,7 @@ def expects_named_tuple(x: typing.NamedTuple): reveal_type(x) # revealed: tuple[object, ...] & NamedTupleLike reveal_type(x._make) # revealed: bound method type[NamedTupleLike]._make(iterable: Iterable[Any]) -> NamedTupleLike reveal_type(x._replace) # revealed: bound method NamedTupleLike._replace(...) -> NamedTupleLike - # revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], (value: tuple[_T@__add__, ...], /) -> tuple[object, ...]] + # revealed: Overload[(value: tuple[object, ...], /) -> tuple[object, ...], [_T](value: tuple[_T, ...], /) -> tuple[object, ...]] reveal_type(x.__add__) reveal_type(x.__iter__) # revealed: bound method tuple[object, ...].__iter__() -> Iterator[object] diff --git a/crates/ty_python_semantic/resources/mdtest/overloads.md b/crates/ty_python_semantic/resources/mdtest/overloads.md index f2fd9b7595..d688a2042c 100644 --- a/crates/ty_python_semantic/resources/mdtest/overloads.md +++ b/crates/ty_python_semantic/resources/mdtest/overloads.md @@ -311,7 +311,7 @@ def func[T](x: T) -> T: ... def func[T](x: T | None = None) -> T | None: return x -reveal_type(func) # revealed: Overload[() -> None, (x: T@func) -> T@func] +reveal_type(func) # revealed: Overload[() -> None, [T](x: T) -> T] reveal_type(func()) # revealed: None reveal_type(func(1)) # revealed: Literal[1] reveal_type(func("")) # revealed: Literal[""] diff --git a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md index 558bf0f117..169931352f 100644 --- a/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md +++ b/crates/ty_python_semantic/resources/mdtest/scopes/builtin.md @@ -11,7 +11,7 @@ def _(flag: bool) -> None: abs = 1 chr: int = 1 - reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T@abs], /) -> _T@abs) + reveal_type(abs) # revealed: Literal[1] | (def abs[_T](x: SupportsAbs[_T], /) -> _T) reveal_type(chr) # revealed: Literal[1] | (def chr(i: SupportsIndex, /) -> str) ``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap index cffbc5d79b..26a2a0ff76 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Cover_non-keyword_re…_(707b284610419a54).snap @@ -200,7 +200,7 @@ info: Type variable defined here | ^^^^^^ 14 | return 0 | -info: Union variant `def f4[T](x: T@f4) -> int` is incompatible with this call site +info: Union variant `def f4[T](x: T) -> int` is incompatible with this call site info: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | ... omitted 5 union elements` info: rule `invalid-argument-type` is enabled by default diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 4c37f17ba1..315a8c2dd6 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -899,6 +899,16 @@ impl<'db> Type<'db> { matches!(self, Type::Never) } + /// Returns `true` if this type contains a `Self` type variable. + pub(crate) fn contains_self(&self, db: &'db dyn Db) -> bool { + any_over_type( + db, + *self, + &|ty| matches!(ty, Type::TypeVar(tv) if tv.typevar(db).is_self(db)), + false, + ) + } + /// Returns `true` if `self` is [`Type::Callable`]. pub(crate) const fn is_callable_type(&self) -> bool { matches!(self, Type::Callable(..)) @@ -6900,8 +6910,26 @@ impl<'db> TypeMapping<'_, 'db> { context: GenericContext<'db>, ) -> GenericContext<'db> { match self { - TypeMapping::ApplySpecialization(_) - | TypeMapping::UniqueSpecialization { .. } + TypeMapping::ApplySpecialization(specialization) => { + // Filter out type variables that are already specialized + // (i.e., mapped to a non-TypeVar type) + GenericContext::from_typevar_instances( + db, + context.variables(db).filter(|bound_typevar| { + // Keep the type variable if it's not in the specialization + // or if it's mapped to itself (still a TypeVar) + match specialization.get(db, *bound_typevar) { + None => true, + Some(Type::TypeVar(mapped_typevar)) => { + // Still a TypeVar, keep it if it's mapping to itself + mapped_typevar.identity(db) == bound_typevar.identity(db) + } + Some(_) => false, // Specialized to a concrete type, filter out + } + }), + ) + } + TypeMapping::UniqueSpecialization { .. } | TypeMapping::PromoteLiterals(_) | TypeMapping::BindLegacyTypevars(_) | TypeMapping::Materialize(_) diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 1cae0b11d7..4c6d1ba297 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -26,10 +26,10 @@ use crate::types::signatures::{ use crate::types::tuple::TupleSpec; use crate::types::visitor::TypeVisitor; use crate::types::{ - BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, KnownBoundMethodType, - KnownClass, KnownInstanceType, MaterializationKind, Protocol, ProtocolInstanceType, - SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypeGuardLike, TypedDictType, - UnionType, WrapperDescriptorKind, visitor, + BindingContext, BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType, + KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, Protocol, + ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypeGuardLike, + TypedDictType, UnionType, WrapperDescriptorKind, visitor, }; /// Settings for displaying types and signatures @@ -45,6 +45,10 @@ pub struct DisplaySettings<'db> { /// Disallow Signature printing to introduce a name /// (presumably because we rendered one already) pub disallow_signature_name: bool, + /// Scopes that are currently active in the display context (e.g. function scopes + /// whose type parameters are currently being displayed). + /// Used to suppress redundant `@{scope}` suffixes for type variables. + pub active_scopes: Rc>>, } impl<'db> DisplaySettings<'db> { @@ -88,6 +92,16 @@ impl<'db> DisplaySettings<'db> { } } + #[must_use] + pub fn with_active_scopes(&self, scopes: impl IntoIterator>) -> Self { + let mut active_scopes = (*self.active_scopes).clone(); + active_scopes.extend(scopes); + Self { + active_scopes: Rc::new(active_scopes), + ..self.clone() + } + } + #[must_use] pub fn from_possibly_ambiguous_types(db: &'db dyn Db, types: I) -> Self where @@ -753,7 +767,9 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { write!( f.with_type(Type::TypeVar(bound_typevar)), "{}", - bound_typevar.identity(self.db).display(self.db) + bound_typevar + .identity(self.db) + .display_with(self.db, self.settings.clone()) )?; f.write_char(']') } @@ -778,10 +794,14 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { match function.signature(self.db).overloads.as_slice() { [signature] => { + let bound_signature = signature.bind_self(self.db, Some(typing_self_ty)); + let hide_unused_self = + bound_signature.should_hide_self_from_display(self.db); let type_parameters = DisplayOptionalGenericContext { - generic_context: signature.generic_context.as_ref(), + generic_context: bound_signature.generic_context.as_ref(), db: self.db, settings: self.settings.clone(), + hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("bound method ")?; @@ -791,8 +811,7 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { f.write_char('.')?; f.with_type(self.ty).write_str(function.name(self.db))?; type_parameters.fmt_detailed(f)?; - signature - .bind_self(self.db, Some(typing_self_ty)) + bound_signature .display_with(self.db, self.settings.disallow_signature_name()) .fmt_detailed(f) } @@ -984,7 +1003,13 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { } Type::TypeVar(bound_typevar) => { f.set_invalid_type_annotation(); - write!(f, "{}", bound_typevar.identity(self.db).display(self.db)) + write!( + f, + "{}", + bound_typevar + .identity(self.db) + .display_with(self.db, self.settings.clone()) + ) } Type::AlwaysTruthy => f.with_type(self.ty).write_str("AlwaysTruthy"), Type::AlwaysFalsy => f.with_type(self.ty).write_str("AlwaysFalsy"), @@ -1049,6 +1074,19 @@ impl<'db> BoundTypeVarIdentity<'db> { DisplayBoundTypeVarIdentity { bound_typevar_identity: self, db, + settings: DisplaySettings::default(), + } + } + + pub(crate) fn display_with( + self, + db: &'db dyn Db, + settings: DisplaySettings<'db>, + ) -> impl Display { + DisplayBoundTypeVarIdentity { + bound_typevar_identity: self, + db, + settings, } } } @@ -1056,13 +1094,18 @@ impl<'db> BoundTypeVarIdentity<'db> { struct DisplayBoundTypeVarIdentity<'db> { bound_typevar_identity: BoundTypeVarIdentity<'db>, db: &'db dyn Db, + settings: DisplaySettings<'db>, } impl Display for DisplayBoundTypeVarIdentity<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str(self.bound_typevar_identity.identity.name(self.db))?; - if let Some(binding_context) = self.bound_typevar_identity.binding_context.name(self.db) { - write!(f, "@{binding_context}")?; + let binding_context = self.bound_typevar_identity.binding_context; + if let Some(binding_context_name) = binding_context.name(self.db) + && let Some(definition) = binding_context.definition() + && !self.settings.active_scopes.contains(&definition) + { + write!(f, "@{binding_context_name}")?; } if let Some(paramspec_attr) = self.bound_typevar_identity.paramspec_attr { write!(f, ".{paramspec_attr}")?; @@ -1193,10 +1236,12 @@ pub(crate) struct DisplayOverloadLiteral<'db> { impl<'db> FmtDetailed<'db> for DisplayOverloadLiteral<'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { let signature = self.literal.signature(self.db); + let hide_unused_self = signature.should_hide_self_from_display(self.db); let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, settings: self.settings.clone(), + hide_unused_self, }; f.set_invalid_type_annotation(); @@ -1241,10 +1286,13 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { match signature.overloads.as_slice() { [signature] => { + let hide_unused_self = signature.should_hide_self_from_display(self.db); + let type_parameters = DisplayOptionalGenericContext { generic_context: signature.generic_context.as_ref(), db: self.db, settings: self.settings.clone(), + hide_unused_self, }; f.set_invalid_type_annotation(); f.write_str("def ")?; @@ -1361,6 +1409,7 @@ impl<'db> GenericContext<'db> { db, settings: DisplaySettings::default(), full: true, + hide_unused_self: false, } } @@ -1374,6 +1423,7 @@ impl<'db> GenericContext<'db> { db, settings, full: false, + hide_unused_self: false, } } } @@ -1382,14 +1432,22 @@ struct DisplayOptionalGenericContext<'a, 'db> { generic_context: Option<&'a GenericContext<'db>>, db: &'db dyn Db, settings: DisplaySettings<'db>, + /// If true, hide `Self` type variables from the generic context prefix + /// when they are not displayed in the signature body. + hide_unused_self: bool, } impl<'db> FmtDetailed<'db> for DisplayOptionalGenericContext<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { if let Some(generic_context) = self.generic_context { - generic_context - .display_with(self.db, self.settings.clone()) - .fmt_detailed(f) + DisplayGenericContext { + generic_context, + db: self.db, + settings: self.settings.clone(), + full: false, + hide_unused_self: self.hide_unused_self, + } + .fmt_detailed(f) } else { Ok(()) } @@ -1408,22 +1466,27 @@ pub struct DisplayGenericContext<'a, 'db> { #[expect(dead_code)] settings: DisplaySettings<'db>, full: bool, + /// If true, hide `Self` type variables from the generic context prefix. + hide_unused_self: bool, } impl<'db> DisplayGenericContext<'_, 'db> { fn fmt_normal(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - let variables = self.generic_context.variables(self.db); + let mut variables = self + .generic_context + .variables(self.db) + .filter(|bound_typevar| { + // If hide_unused_self is true and this is a Self typevar, skip it + !self.hide_unused_self || !bound_typevar.typevar(self.db).is_self(self.db) + }) + .peekable(); - let non_implicit_variables: Vec<_> = variables - .filter(|bound_typevar| !bound_typevar.typevar(self.db).is_self(self.db)) - .collect(); - - if non_implicit_variables.is_empty() { + if variables.peek().is_none() { return Ok(()); } f.write_char('[')?; - for (idx, bound_typevar) in non_implicit_variables.iter().enumerate() { + for (idx, bound_typevar) in variables.enumerate() { if idx > 0 { f.write_str(", ")?; } @@ -1433,12 +1496,14 @@ impl<'db> DisplayGenericContext<'_, 'db> { f.write_str("**")?; } write!( - f.with_type(Type::TypeVar(*bound_typevar)), + f.with_type(Type::TypeVar(bound_typevar)), "{}", typevar.name(self.db) )?; } - f.write_char(']') + f.write_char(']')?; + + Ok(()) } fn fmt_full(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { @@ -1678,6 +1743,7 @@ impl<'db> Signature<'db> { ) -> DisplaySignature<'a, 'db> { DisplaySignature { definition: self.definition(), + generic_context: self.generic_context.as_ref(), parameters: self.parameters(), return_ty: self.return_ty, db, @@ -1688,13 +1754,14 @@ impl<'db> Signature<'db> { pub(crate) struct DisplaySignature<'a, 'db> { definition: Option>, + generic_context: Option<&'a GenericContext<'db>>, parameters: &'a Parameters<'db>, return_ty: Type<'db>, db: &'db dyn Db, settings: DisplaySettings<'db>, } -impl DisplaySignature<'_, '_> { +impl<'db> DisplaySignature<'_, 'db> { /// Get detailed display information including component ranges pub(crate) fn to_string_parts(&self) -> SignatureDisplayDetails { let mut f = TypeWriter::Details(TypeDetailsWriter::new()); @@ -1705,6 +1772,14 @@ impl DisplaySignature<'_, '_> { TypeWriter::Formatter(_) => unreachable!("Expected Details variant"), } } + + pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { + !self.return_ty.contains_self(db) + && !self + .parameters + .iter() + .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) + } } impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { @@ -1730,15 +1805,41 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> { f.write_str(&name)?; } + let settings = if let Some(generic_context) = self.generic_context { + self.settings + .with_active_scopes(generic_context.variables(self.db).filter_map(|bound| { + match bound.binding_context(self.db) { + BindingContext::Definition(def) => Some(def), + BindingContext::Synthetic => None, + } + })) + } else { + self.settings.clone() + }; + + // Display type parameters if present, but only when the caller hasn't + // already displayed them (indicated by disallow_signature_name being false) + if !self.settings.disallow_signature_name { + let hide_unused_self = self.should_hide_self_from_display(self.db); + + DisplayOptionalGenericContext { + generic_context: self.generic_context, + db: self.db, + settings: settings.clone(), + hide_unused_self, + } + .fmt_detailed(&mut f)?; + } + // Parameters self.parameters - .display_with(self.db, self.settings.clone()) + .display_with(self.db, settings.clone()) .fmt_detailed(&mut f)?; // Return type f.write_str(" -> ")?; self.return_ty - .display_with(self.db, self.settings.singleline()) + .display_with(self.db, settings.singleline()) .fmt_detailed(&mut f)?; if self.parameters.is_top() { diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index ce53f143ff..c45ecc2522 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -713,6 +713,19 @@ impl<'db> Signature<'db> { Self::new(Parameters::bottom(), Type::Never) } + /// Returns `true` if `Self` should be hidden from the generic context display. + /// + /// `Self` is hidden if it does not appear in: + /// 1. The return type + /// 2. Any explicitly annotated parameter (not inferred) + pub(crate) fn should_hide_self_from_display(&self, db: &'db dyn Db) -> bool { + !self.return_ty.contains_self(db) + && !self + .parameters() + .iter() + .any(|p| p.should_annotation_be_displayed() && p.annotated_type().contains_self(db)) + } + pub(crate) fn with_inherited_generic_context( mut self, db: &'db dyn Db,