Skip to content

Commit a25a4df

Browse files
[ty] Disambiguate duplicate-looking overloaded callables in union display (#23907)
## Summary Adjust type display so unions that contain distinct overloaded callables with identical single-line renderings include callable names for any ambiguous elements... This avoids outputs like `A | A` when the union actually contains two different function values (like `str.upper | str.lower`). Closes astral-sh/ty#2919. --------- Co-authored-by: Alex Waygood <[email protected]>
1 parent ce2cb82 commit a25a4df

2 files changed

Lines changed: 94 additions & 11 deletions

File tree

crates/ty_python_semantic/resources/mdtest/type_display/callable.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ def _(flag: bool, c: CallableTypeOf[f]):
3434
reveal_type(x) # revealed: Overload[(x: int) -> bool, (x: str) -> str] | Literal[True]
3535
```
3636

37+
When a union would otherwise display two distinct overloaded callables identically, we include their
38+
names to avoid implying that the union contains duplicate elements:
39+
40+
```py
41+
def f(flag: bool):
42+
x = str.upper if flag else str.lower
43+
# revealed: (Overload[def upper(self: LiteralString) -> LiteralString, def upper(self) -> str]) | (Overload[def lower(self: LiteralString) -> LiteralString, def lower(self) -> str])
44+
reveal_type(x)
45+
```
46+
3747
### Top
3848

3949
And we don't parenthesize the top callable, since it is wrapped in `Top[...]`:

crates/ty_python_semantic/src/types/display.rs

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,35 @@ impl<'db> NamedItem<'db> {
6868
}
6969
}
7070

71+
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
72+
enum SignatureNameDisplay {
73+
#[default]
74+
Auto,
75+
Force,
76+
Disallow,
77+
}
78+
79+
impl SignatureNameDisplay {
80+
const fn should_display(self, multiline: bool) -> bool {
81+
match self {
82+
Self::Auto => multiline,
83+
Self::Force => true,
84+
Self::Disallow => false,
85+
}
86+
}
87+
88+
const fn allows_type_parameters(self) -> bool {
89+
!matches!(self, Self::Disallow)
90+
}
91+
}
92+
7193
/// Settings for displaying types and signatures
7294
#[derive(Debug, Clone, Default)]
7395
pub struct DisplaySettings<'db> {
7496
/// Whether rendering can be multiline
7597
pub multiline: bool,
98+
/// Whether callable signatures should include their definition name.
99+
signature_name_display: SignatureNameDisplay,
76100
/// Class names that should be displayed fully qualified
77101
/// (e.g., `module.ClassName` instead of just `ClassName`)
78102
pub qualified: Rc<FxHashMap<&'db str, QualificationLevel>>,
@@ -81,9 +105,6 @@ pub struct DisplaySettings<'db> {
81105
pub qualified_type_aliases: Rc<FxHashMap<&'db str, QualificationLevel>>,
82106
/// Whether long unions and literals are displayed in full
83107
pub preserve_full_unions: bool,
84-
/// Disallow Signature printing to introduce a name
85-
/// (presumably because we rendered one already)
86-
pub disallow_signature_name: bool,
87108
/// Scopes that are currently active in the display context (e.g. function scopes
88109
/// whose type parameters are currently being displayed).
89110
/// Used to suppress redundant `@{scope}` suffixes for type variables.
@@ -129,7 +150,15 @@ impl<'db> DisplaySettings<'db> {
129150
#[must_use]
130151
pub fn disallow_signature_name(&self) -> Self {
131152
Self {
132-
disallow_signature_name: true,
153+
signature_name_display: SignatureNameDisplay::Disallow,
154+
..self.clone()
155+
}
156+
}
157+
158+
#[must_use]
159+
pub fn force_signature_name(&self) -> Self {
160+
Self {
161+
signature_name_display: SignatureNameDisplay::Force,
133162
..self.clone()
134163
}
135164
}
@@ -1996,10 +2025,12 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> {
19962025
f.write_str("Top[")?;
19972026
}
19982027

1999-
// If we're multiline printing and a name hasn't been emitted, try to
2028+
// If the current display policy wants a signature name and a name hasn't been emitted,
20002029
// remember what the name was by checking if we have a definition
2001-
if self.settings.multiline
2002-
&& !self.settings.disallow_signature_name
2030+
if self
2031+
.settings
2032+
.signature_name_display
2033+
.should_display(self.settings.multiline)
20032034
&& let Some(definition) = self.definition
20042035
&& let Some(name) = definition.name(self.db)
20052036
{
@@ -2020,8 +2051,12 @@ impl<'db> FmtDetailed<'db> for DisplaySignature<'_, 'db> {
20202051
};
20212052

20222053
// Display type parameters if present, but only when the caller hasn't
2023-
// already displayed them (indicated by disallow_signature_name being false)
2024-
if !self.settings.disallow_signature_name {
2054+
// already displayed them.
2055+
if self
2056+
.settings
2057+
.signature_name_display
2058+
.allows_type_parameters()
2059+
{
20252060
let hide_unused_self = self.should_hide_self_from_display(self.db);
20262061

20272062
DisplayOptionalGenericContext {
@@ -2372,9 +2407,39 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> {
23722407
)
23732408
}
23742409

2410+
fn singleline_union_element_label<'db>(
2411+
db: &'db dyn Db,
2412+
element: Type<'db>,
2413+
settings: &DisplaySettings<'db>,
2414+
) -> String {
2415+
element.display_with(db, settings.singleline()).to_string()
2416+
}
2417+
2418+
fn duplicate_ambiguous_labels(element_labels: &[Option<String>]) -> FxHashSet<&str> {
2419+
let mut counts: FxHashMap<&str, usize> = FxHashMap::default();
2420+
2421+
for label in element_labels.iter().flatten() {
2422+
*counts.entry(&**label).or_default() += 1;
2423+
}
2424+
2425+
counts
2426+
.into_iter()
2427+
.filter_map(|(label, count)| (count > 1).then_some(label))
2428+
.collect()
2429+
}
2430+
23752431
let elements = self.ty.elements(self.db);
23762432
let mut condensed_types = vec![];
23772433
let mut subclass_of_types = vec![];
2434+
let element_labels: Vec<_> = elements
2435+
.iter()
2436+
.copied()
2437+
.map(|element| {
2438+
(!is_condensable(element) && !element.is_subclass_of())
2439+
.then(|| singleline_union_element_label(self.db, element, &self.settings))
2440+
})
2441+
.collect();
2442+
let duplicate_ambiguous_labels = duplicate_ambiguous_labels(&element_labels);
23782443

23792444
for element in elements.iter().copied() {
23802445
if is_condensable(element) {
@@ -2400,7 +2465,7 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> {
24002465
let mut subclass_of_types = Some(subclass_of_types);
24012466
let mut displayed_entries = 0usize;
24022467

2403-
for element in elements {
2468+
for (element, label) in elements.iter().zip(&element_labels) {
24042469
if displayed_entries >= display_limit {
24052470
break;
24062471
}
@@ -2425,10 +2490,18 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> {
24252490
}
24262491
} else {
24272492
displayed_entries += 1;
2493+
let settings = if label
2494+
.as_deref()
2495+
.is_some_and(|label| duplicate_ambiguous_labels.contains(label))
2496+
{
2497+
self.settings.singleline().force_signature_name()
2498+
} else {
2499+
self.settings.singleline()
2500+
};
24282501
join.entry(&DisplayMaybeParenthesizedType {
24292502
ty: *element,
24302503
db: self.db,
2431-
settings: self.settings.singleline(),
2504+
settings,
24322505
});
24332506
}
24342507
}

0 commit comments

Comments
 (0)