[ty]: consolidate type[] types in a union when displaying them in diagnostics (#22592)

This commit is contained in:
Bhuminjay Soni
2026-01-15 20:50:58 +05:30
committed by GitHub
parent de954b6fa4
commit b2d57ddaa5
10 changed files with 116 additions and 45 deletions

View File

@@ -235,8 +235,8 @@ Foo(3.14)
Foo(42)
Foo("hello") # error: [invalid-argument-type] "Argument is incorrect: Expected `int | float`, found `Literal["hello"]`"
reveal_type(Foo(3.14).__class__) # revealed: type[int] | type[float]
reveal_type(Foo(42).__class__) # revealed: type[int] | type[float]
reveal_type(Foo(3.14).__class__) # revealed: type[int | float]
reveal_type(Foo(42).__class__) # revealed: type[int | float]
static_assert(is_assignable_to(Foo, float))
static_assert(is_assignable_to(Foo, int | float))
static_assert(is_assignable_to(Foo, int | float | None))
@@ -253,9 +253,9 @@ Bar(3.14)
Bar(42)
Bar("goodbye") # error: [invalid-argument-type]
reveal_type(Bar(1 + 2j).__class__) # revealed: type[int] | type[float] | type[complex]
reveal_type(Bar(3.14).__class__) # revealed: type[int] | type[float] | type[complex]
reveal_type(Bar(42).__class__) # revealed: type[int] | type[float] | type[complex]
reveal_type(Bar(1 + 2j).__class__) # revealed: type[int | float | complex]
reveal_type(Bar(3.14).__class__) # revealed: type[int | float | complex]
reveal_type(Bar(42).__class__) # revealed: type[int | float | complex]
static_assert(is_assignable_to(Bar, complex))
static_assert(is_assignable_to(Bar, int | float | complex))
static_assert(is_assignable_to(Bar, int | float | complex | None))

View File

@@ -2094,8 +2094,8 @@ def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]):
reveal_type(b.__class__) # revealed: <class 'str'>
reveal_type(type(b)) # revealed: <class 'str'>
reveal_type(c.__class__) # revealed: type[int] | type[str]
reveal_type(type(c)) # revealed: type[int] | type[str]
reveal_type(c.__class__) # revealed: type[int | str]
reveal_type(type(c)) # revealed: type[int | str]
# `type[type]`, a.k.a., either the class `type` or some subclass of `type`.
# It would be incorrect to infer `Literal[type]` here,

View File

@@ -408,7 +408,7 @@ def f(x: type[B]) -> B: ...
from overloaded import A, B, f
def _(x: type[A | B]):
reveal_type(x) # revealed: type[A] | type[B]
reveal_type(x) # revealed: type[A | B]
reveal_type(f(x)) # revealed: A | B
reveal_type(f(*(x,))) # revealed: A | B
```

View File

@@ -1139,8 +1139,8 @@ SubclassOfP = type[P]
reveal_type(SubclassOfA) # revealed: <special-form 'type[A]'>
reveal_type(SubclassOfAny) # revealed: <special-form 'type[Any]'>
reveal_type(SubclassOfAOrB1) # revealed: <special-form 'type[A | B]'>
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special-form 'type[A | B]'>
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special-form 'type[A | B]'>
reveal_type(SubclassOfG) # revealed: <special-form 'type[G[Unknown]]'>
reveal_type(SubclassOfGInt) # revealed: <special-form 'type[G[int]]'>
reveal_type(SubclassOfP) # revealed: <special-form 'type[P]'>
@@ -1161,13 +1161,13 @@ def _(
reveal_type(subclass_of_any) # revealed: type[Any]
reveal_type(subclass_of_any()) # revealed: Any
reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b1) # revealed: type[A | B]
reveal_type(subclass_of_a_or_b1()) # revealed: A | B
reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b2) # revealed: type[A | B]
reveal_type(subclass_of_a_or_b2()) # revealed: A | B
reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b3) # revealed: type[A | B]
reveal_type(subclass_of_a_or_b3()) # revealed: A | B
reveal_type(subclass_of_g) # revealed: type[G[Unknown]]
@@ -1200,10 +1200,10 @@ def _(
subclass_of_union_alias1: SubclassOfUnionAlias1,
subclass_of_union_alias2: SubclassOfUnionAlias2,
):
reveal_type(subclass_of_union_alias1) # revealed: type[C] | type[D]
reveal_type(subclass_of_union_alias1) # revealed: type[C | D]
reveal_type(subclass_of_union_alias1()) # revealed: C | D
reveal_type(subclass_of_union_alias2) # revealed: type[C] | type[D]
reveal_type(subclass_of_union_alias2) # revealed: type[C | D]
reveal_type(subclass_of_union_alias2()) # revealed: C | D
```
@@ -1255,8 +1255,8 @@ SubclassOfP = Type[P]
reveal_type(SubclassOfA) # revealed: <special-form 'type[A]'>
reveal_type(SubclassOfAny) # revealed: <special-form 'type[Any]'>
reveal_type(SubclassOfAOrB1) # revealed: <special-form 'type[A | B]'>
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special-form 'type[A] | type[B]'>
reveal_type(SubclassOfAOrB2) # revealed: <types.UnionType special-form 'type[A | B]'>
reveal_type(SubclassOfAOrB3) # revealed: <types.UnionType special-form 'type[A | B]'>
reveal_type(SubclassOfG) # revealed: <special-form 'type[G[Unknown]]'>
reveal_type(SubclassOfGInt) # revealed: <special-form 'type[G[int]]'>
reveal_type(SubclassOfP) # revealed: <special-form 'type[P]'>
@@ -1277,13 +1277,13 @@ def _(
reveal_type(subclass_of_any) # revealed: type[Any]
reveal_type(subclass_of_any()) # revealed: Any
reveal_type(subclass_of_a_or_b1) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b1) # revealed: type[A | B]
reveal_type(subclass_of_a_or_b1()) # revealed: A | B
reveal_type(subclass_of_a_or_b2) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b2) # revealed: type[A | B]
reveal_type(subclass_of_a_or_b2()) # revealed: A | B
reveal_type(subclass_of_a_or_b3) # revealed: type[A] | type[B]
reveal_type(subclass_of_a_or_b3) # revealed: type[A | B]
reveal_type(subclass_of_a_or_b3()) # revealed: A | B
reveal_type(subclass_of_g) # revealed: type[G[Unknown]]

View File

@@ -141,7 +141,7 @@ python-version = "3.10"
```py
def f(x: type[int | str | bytes | range]):
if issubclass(x, int | str):
reveal_type(x) # revealed: type[int] | type[str]
reveal_type(x) # revealed: type[int | str]
elif issubclass(x, bytes | memoryview):
reveal_type(x) # revealed: type[bytes]
else:
@@ -154,7 +154,7 @@ runtime a special exception is made for `None` so that `issubclass(x, int | None
```py
def _(x: type):
if issubclass(x, int | str | None):
reveal_type(x) # revealed: type[int] | type[str] | <class 'NoneType'>
reveal_type(x) # revealed: type[int | str] | <class 'NoneType'>
else:
reveal_type(x) # revealed: type & ~type[int] & ~type[str] & ~<class 'NoneType'>
```
@@ -176,9 +176,9 @@ python-version = "3.10"
def _(x: type[int | list | bytes]):
# error: [invalid-argument-type]
if issubclass(x, int | list[int]):
reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes]
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
else:
reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes]
reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```
## PEP-604 unions on Python \<3.10
@@ -213,7 +213,7 @@ reveal_type(IntOrStr) # revealed: <types.UnionType special-form 'int | str'>
def f(x: type[int | str | bytes | range]):
if issubclass(x, IntOrStr):
reveal_type(x) # revealed: type[int] | type[str]
reveal_type(x) # revealed: type[int | str]
elif issubclass(x, Union[bytes, memoryview]):
reveal_type(x) # revealed: type[bytes]
else:

View File

@@ -262,11 +262,11 @@ def _(
af: type[AmbiguousClass] | type[FalsyClass],
flag: bool,
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
reveal_type(ta) # revealed: type[TruthyClass | AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | (type[AmbiguousClass] & ~AlwaysFalsy)
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
reveal_type(af) # revealed: type[AmbiguousClass | FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy

View File

@@ -16,9 +16,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/narrow/issubclass.md
1 | def _(x: type[int | list | bytes]):
2 | # error: [invalid-argument-type]
3 | if issubclass(x, int | list[int]):
4 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes]
4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
5 | else:
6 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes]
6 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
```
# Diagnostics
@@ -33,7 +33,7 @@ error[invalid-argument-type]: Invalid second argument to `issubclass`
| ^^^^^^^^^^^^^^---------------^
| |
| This `UnionType` instance contains non-class elements
4 | reveal_type(x) # revealed: type[int] | type[list[Unknown]] | type[bytes]
4 | reveal_type(x) # revealed: type[int | list[Unknown] | bytes]
5 | else:
|
info: A `UnionType` instance can only be used as the second argument to `issubclass` if all elements are class objects

View File

@@ -93,7 +93,7 @@ class A:
class C: ...
def _(u: type[BasicUser | ProUser | A.B.C]):
# revealed: type[BasicUser] | type[ProUser] | type[C]
# revealed: type[BasicUser | ProUser | C]
reveal_type(u)
```
@@ -110,9 +110,9 @@ class A:
class C: ...
def f(a: type[Union[BasicUser, ProUser, A.B.C]], b: type[Union[str]], c: type[Union[BasicUser, Union[ProUser, A.B.C]]]):
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(a) # revealed: type[BasicUser | ProUser | C]
reveal_type(b) # revealed: type[str]
reveal_type(c) # revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(c) # revealed: type[BasicUser | ProUser | C]
```
## New-style and old-style unions in combination
@@ -128,8 +128,8 @@ class A:
class C: ...
def f(a: type[BasicUser | Union[ProUser, A.B.C]], b: type[Union[BasicUser | Union[ProUser, A.B.C | str]]]):
reveal_type(a) # revealed: type[BasicUser] | type[ProUser] | type[C]
reveal_type(b) # revealed: type[BasicUser] | type[ProUser] | type[C] | type[str]
reveal_type(a) # revealed: type[BasicUser | ProUser | C]
reveal_type(b) # revealed: type[BasicUser | ProUser | C | str]
```
## Illegal parameters

View File

@@ -7,9 +7,10 @@ This test suite covers certain basic properties and simplification strategies fo
```py
from typing import Literal
def _(u1: int | str, u2: Literal[0] | Literal[1]) -> None:
def _(u1: int | str, u2: Literal[0] | Literal[1], u3: type[int] | type[str]) -> None:
reveal_type(u1) # revealed: int | str
reveal_type(u2) # revealed: Literal[0, 1]
reveal_type(u3) # revealed: type[int | str]
```
## Duplicate elements are collapsed

View File

@@ -28,8 +28,8 @@ use crate::types::visitor::TypeVisitor;
use crate::types::{
BindingContext, BoundTypeVarIdentity, CallableType, CallableTypeKind, IntersectionType,
KnownBoundMethodType, KnownClass, KnownInstanceType, MaterializationKind, Protocol,
ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, Type, TypeGuardLike,
TypedDictType, UnionType, WrapperDescriptorKind, visitor,
ProtocolInstanceType, SpecialFormType, StringLiteralType, SubclassOfInner, SubclassOfType,
Type, TypeGuardLike, TypedDictType, UnionType, WrapperDescriptorKind, visitor,
};
/// Settings for displaying types and signatures
@@ -2151,15 +2151,20 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> {
}
let elements = self.ty.elements(self.db);
let mut condensed_types = vec![];
let mut subclass_of_types = vec![];
let condensed_types = elements
.iter()
.copied()
.filter(|element| is_condensable(*element))
.collect::<Vec<_>>();
for element in elements.iter().copied() {
if is_condensable(element) {
condensed_types.push(element);
} else if let Type::SubclassOf(subclass_of) = element {
subclass_of_types.push(subclass_of);
}
}
let total_entries =
usize::from(!condensed_types.is_empty()) + elements.len() - condensed_types.len();
let total_entries = elements.len() - condensed_types.len() - subclass_of_types.len()
+ usize::from(!condensed_types.is_empty())
+ usize::from(!subclass_of_types.is_empty());
assert_ne!(total_entries, 0);
@@ -2170,6 +2175,7 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> {
UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions);
let mut condensed_types = Some(condensed_types);
let mut subclass_of_types = Some(subclass_of_types);
let mut displayed_entries = 0usize;
for element in elements {
@@ -2186,6 +2192,15 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> {
settings: self.settings.singleline(),
});
}
} else if element.is_subclass_of() {
if let Some(subclass_of_types) = subclass_of_types.take() {
displayed_entries += 1;
join.entry(&DisplaySubclassOfGroup {
types: subclass_of_types,
db: self.db,
settings: self.settings.singleline(),
});
}
} else {
displayed_entries += 1;
join.entry(&DisplayMaybeParenthesizedType {
@@ -2221,6 +2236,61 @@ impl fmt::Debug for DisplayUnionType<'_, '_> {
Display::fmt(self, f)
}
}
struct DisplaySubclassOfGroup<'db> {
types: Vec<SubclassOfType<'db>>,
db: &'db dyn Db,
settings: DisplaySettings<'db>,
}
impl<'db> FmtDetailed<'db> for DisplaySubclassOfGroup<'db> {
fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result {
f.write_str("type[")?;
let total_entries = self.types.len();
let display_limit =
UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions);
let mut join = f.join(" | ");
for subclass_of in self.types.iter().take(display_limit) {
match subclass_of.subclass_of() {
SubclassOfInner::Class(ClassType::NonGeneric(class)) => {
join.entry(&class.display_with(self.db, self.settings.singleline()));
}
SubclassOfInner::Class(ClassType::Generic(alias)) => {
join.entry(&alias.display_with(self.db, self.settings.singleline()));
}
SubclassOfInner::Dynamic(dynamic) => {
let rep =
Type::Dynamic(dynamic).representation(self.db, self.settings.singleline());
join.entry(&rep);
}
SubclassOfInner::TypeVar(bound_typevar) => {
let rep = Type::TypeVar(bound_typevar)
.representation(self.db, self.settings.singleline());
join.entry(&rep);
}
}
}
if !self.settings.preserve_full_unions {
let omitted_entries = total_entries.saturating_sub(display_limit);
if omitted_entries > 0 {
join.entry(&DisplayOmitted {
count: omitted_entries,
singular: "type",
plural: "types",
});
}
}
join.finish()?;
f.write_str("]")
}
}
impl Display for DisplaySubclassOfGroup<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
self.fmt_detailed(&mut TypeWriter::Formatter(f))
}
}
struct DisplayLiteralGroup<'db> {
literals: Vec<Type<'db>>,
db: &'db dyn Db,