[ty] Truncate type display for long unions in some situations (#20730)

## Summary

Fixes [astral-sh/ty#1307](https://github.com/astral-sh/ty/issues/1307)

Unions with length <= 5 are unaffected to minimize test churn
Unions with length > 5 will only display the first 3 elements + "...
omitted x union elements"
Here "length" is defined as the number of elements after condensation to
literals

Edit: we no longer truncate in revel case. 
Before:

> info: Attempted to call union type `(def f1() -> int) | (def f2(name:
str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) ->
int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) |
(Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`

After:

> 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`

The below comparisons are outdated, but left here as a reference.

Before:
```reveal_type(x)  # revealed: Literal[1, 2] | A | B | C | D | E | F | G```
```reveal_type(x) # revealed: Result1A | Result1B | Result2A | Result2B
| Result3 | Result4```
After:
```reveal_type(x)  # revealed: Literal[1, 2] | A | B | ... omitted 5 union elements```
```reveal_type(x) # revealed: Result1A | Result1B | Result2A | ...
omitted 3 union elements```

This formatting is consistent with
`crates/ty_python_semantic/src/types/call/bind.rs` line 2992

## Test Plan

Cosmetic only, covered and verified by changes in mdtest
This commit is contained in:
Mark Z. Ding 2025-10-08 06:21:26 -04:00 committed by GitHub
parent 1f1542db51
commit f95eb90951
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 83 additions and 12 deletions

View file

@ -86,7 +86,7 @@ error[call-non-callable]: Object of type `Literal[5]` is not callable
| ^^^^
|
info: Union variant `Literal[5]` 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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 `call-non-callable` is enabled by default
```
@ -101,7 +101,7 @@ error[call-non-callable]: Object of type `PossiblyNotCallable` is not callable (
| ^^^^
|
info: Union variant `PossiblyNotCallable` 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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 `call-non-callable` is enabled by default
```
@ -116,7 +116,7 @@ error[missing-argument]: No argument provided for required parameter `b` of func
| ^^^^
|
info: Union variant `def f3(a: int, b: int) -> 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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 `missing-argument` is enabled by default
```
@ -152,7 +152,7 @@ info: Overload implementation defined here
28 | return x + y if x and y else None
|
info: Union variant `Overload[() -> None, (x: str, y: str) -> str]` 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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 `no-matching-overload` is enabled by default
```
@ -176,7 +176,7 @@ info: Function defined here
8 | return 0
|
info: Union variant `def f2(name: str) -> 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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
```
@ -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: Attempted to call union type `(def f1() -> int) | (def f2(name: str) -> int) | (def f3(a: int, b: int) -> int) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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
```
@ -227,7 +227,7 @@ info: Matching overload defined here
info: Non-matching overloads for function `f5`:
info: () -> None
info: Union variant `Overload[() -> None, (x: str) -> str]` 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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
```
@ -242,7 +242,7 @@ error[too-many-positional-arguments]: Too many positional arguments to function
| ^
|
info: Union variant `def f1() -> 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) | (def f4[T](x: T@f4) -> int) | Literal[5] | (Overload[() -> None, (x: str) -> str]) | (Overload[() -> None, (x: str, y: str) -> str]) | PossiblyNotCallable`
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 `too-many-positional-arguments` is enabled by default
```

View file

@ -35,6 +35,8 @@ pub struct DisplaySettings<'db> {
/// Class names that should be displayed fully qualified
/// (e.g., `module.ClassName` instead of just `ClassName`)
pub qualified: Rc<FxHashSet<&'db str>>,
/// Whether long unions are displayed in full
pub preserve_full_unions: bool,
}
impl<'db> DisplaySettings<'db> {
@ -54,6 +56,22 @@ impl<'db> DisplaySettings<'db> {
}
}
#[must_use]
pub fn truncate_long_unions(self) -> Self {
Self {
preserve_full_unions: false,
..self
}
}
#[must_use]
pub fn preserve_long_unions(self) -> Self {
Self {
preserve_full_unions: true,
..self
}
}
#[must_use]
pub fn from_possibly_ambiguous_type_pair(
db: &'db dyn Db,
@ -1265,6 +1283,9 @@ struct DisplayUnionType<'db> {
settings: DisplaySettings<'db>,
}
const MAX_DISPLAYED_UNION_ITEMS: usize = 5;
const MAX_DISPLAYED_UNION_ITEMS_WHEN_ELIDED: usize = 3;
impl Display for DisplayUnionType<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
fn is_condensable(ty: Type<'_>) -> bool {
@ -1286,12 +1307,35 @@ impl Display for DisplayUnionType<'_> {
.filter(|element| is_condensable(*element))
.collect::<Vec<_>>();
let total_entries =
usize::from(!condensed_types.is_empty()) + elements.len() - condensed_types.len();
assert_ne!(total_entries, 0);
let mut join = f.join(" | ");
let display_limit = if self.settings.preserve_full_unions {
total_entries
} else {
let limit = if total_entries > MAX_DISPLAYED_UNION_ITEMS {
MAX_DISPLAYED_UNION_ITEMS_WHEN_ELIDED
} else {
MAX_DISPLAYED_UNION_ITEMS
};
limit.min(total_entries)
};
let mut condensed_types = Some(condensed_types);
let mut displayed_entries = 0usize;
for element in elements {
if displayed_entries >= display_limit {
break;
}
if is_condensable(*element) {
if let Some(condensed_types) = condensed_types.take() {
displayed_entries += 1;
join.entry(&DisplayLiteralGroup {
literals: condensed_types,
db: self.db,
@ -1299,6 +1343,7 @@ impl Display for DisplayUnionType<'_> {
});
}
} else {
displayed_entries += 1;
join.entry(&DisplayMaybeParenthesizedType {
ty: *element,
db: self.db,
@ -1307,6 +1352,15 @@ impl Display for DisplayUnionType<'_> {
}
}
if !self.settings.preserve_full_unions {
let omitted_entries = total_entries.saturating_sub(displayed_entries);
if omitted_entries > 0 {
join.entry(&DisplayUnionOmitted {
count: omitted_entries,
});
}
}
join.finish()?;
Ok(())
@ -1319,6 +1373,21 @@ impl fmt::Debug for DisplayUnionType<'_> {
}
}
struct DisplayUnionOmitted {
count: usize,
}
impl Display for DisplayUnionOmitted {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let plural = if self.count == 1 {
"element"
} else {
"elements"
};
write!(f, "... omitted {} union {}", self.count, plural)
}
}
struct DisplayLiteralGroup<'db> {
literals: Vec<Type<'db>>,
db: &'db dyn Db,

View file

@ -72,6 +72,7 @@ use crate::types::diagnostic::{
report_bad_argument_to_get_protocol_members, report_bad_argument_to_protocol_interface,
report_runtime_check_against_non_runtime_checkable_protocol,
};
use crate::types::display::DisplaySettings;
use crate::types::generics::GenericContext;
use crate::types::narrow::ClassInfoConstraintFunction;
use crate::types::signatures::{CallableSignature, Signature};
@ -1386,10 +1387,11 @@ impl KnownFunction {
{
let mut diag = builder.into_diagnostic("Revealed type");
let span = context.span(&call_expression.arguments.args[0]);
diag.annotate(
Annotation::primary(span)
.message(format_args!("`{}`", revealed_type.display(db))),
);
diag.annotate(Annotation::primary(span).message(format_args!(
"`{}`",
revealed_type
.display_with(db, DisplaySettings::default().preserve_long_unions())
)));
}
}