From fc3b341529fcc7aaac328a605fb3b1baffa588ff Mon Sep 17 00:00:00 2001 From: "Mark Z. Ding" Date: Fri, 17 Oct 2025 07:50:58 -0400 Subject: [PATCH] [ty] Truncate Literal type display in some situations (#20928) --- .../mdtest/diagnostics/union_call.md | 24 ++++ ..._Truncation_for_long_…_(ec94b5e857284ef3).snap | 56 +++++++++ .../ty_python_semantic/src/types/display.rs | 115 ++++++++++++------ 3 files changed, 158 insertions(+), 37 deletions(-) create mode 100644 crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Truncation_for_long_…_(ec94b5e857284ef3).snap diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md index 31cafa14bf..a5e9b9370e 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md @@ -138,3 +138,27 @@ def _(n: int): # error: [unknown-argument] y = f("foo", name="bar", unknown="quux") ``` + +### Truncation for long unions and literals + +This test demonstrates a call where the expected type is a large mixed union. The diagnostic must +therefore truncate the long expected union type to avoid overwhelming output. + +```py +from typing import Literal, Union + +class A: ... +class B: ... +class C: ... +class D: ... +class E: ... +class F: ... + +def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int: + return 0 + +def _(n: int): + x = n + # error: [invalid-argument-type] + f1(x) +``` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Truncation_for_long_…_(ec94b5e857284ef3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Truncation_for_long_…_(ec94b5e857284ef3).snap new file mode 100644 index 0000000000..77582d963c --- /dev/null +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/union_call.md_-_Calling_a_union_of_f…_-_Try_to_cover_all_pos…_-_Truncation_for_long_…_(ec94b5e857284ef3).snap @@ -0,0 +1,56 @@ +--- +source: crates/ty_test/src/lib.rs +assertion_line: 427 +expression: snapshot +--- +--- +mdtest name: union_call.md - Calling a union of function types - Try to cover all possible reasons - Truncation for long unions and literals +mdtest path: crates/ty_python_semantic/resources/mdtest/diagnostics/union_call.md +--- + +# Python source files + +## mdtest_snippet.py + +``` + 1 | from typing import Literal, Union + 2 | + 3 | class A: ... + 4 | class B: ... + 5 | class C: ... + 6 | class D: ... + 7 | class E: ... + 8 | class F: ... + 9 | +10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int: +11 | return 0 +12 | +13 | def _(n: int): +14 | x = n +15 | # error: [invalid-argument-type] +16 | f1(x) +``` + +# Diagnostics + +``` +error[invalid-argument-type]: Argument to function `f1` is incorrect + --> src/mdtest_snippet.py:16:8 + | +14 | x = n +15 | # error: [invalid-argument-type] +16 | f1(x) + | ^ Expected `Literal[1, 2, 3, 4, 5, ... omitted 3 literals] | A | B | ... omitted 4 union elements`, found `int` + | +info: Function defined here + --> src/mdtest_snippet.py:10:5 + | + 8 | class F: ... + 9 | +10 | def f1(x: Union[Literal[1, 2, 3, 4, 5, 6, 7, 8], A, B, C, D, E, F]) -> int: + | ^^ ----------------------------------------------------------- Parameter declared here +11 | return 0 + | +info: rule `invalid-argument-type` is enabled by default + +``` diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index 73957453c9..5b2f69e811 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -38,7 +38,7 @@ pub struct DisplaySettings<'db> { /// Class names that should be displayed fully qualified /// (e.g., `module.ClassName` instead of just `ClassName`) pub qualified: Rc>, - /// Whether long unions are displayed in full + /// Whether long unions and literals are displayed in full pub preserve_full_unions: bool, } @@ -1328,6 +1328,44 @@ impl Display for DisplayParameter<'_> { } } +#[derive(Debug, Copy, Clone)] +struct TruncationPolicy { + max: usize, + max_when_elided: usize, +} + +impl TruncationPolicy { + fn display_limit(self, total: usize, preserve_full: bool) -> usize { + if preserve_full { + return total; + } + let limit = if total > self.max { + self.max_when_elided + } else { + self.max + }; + limit.min(total) + } +} + +#[derive(Debug)] +struct DisplayOmitted { + count: usize, + singular: &'static str, + plural: &'static str, +} + +impl Display for DisplayOmitted { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let noun = if self.count == 1 { + self.singular + } else { + self.plural + }; + write!(f, "... omitted {} {}", self.count, noun) + } +} + impl<'db> UnionType<'db> { fn display_with( &'db self, @@ -1348,8 +1386,10 @@ struct DisplayUnionType<'db> { settings: DisplaySettings<'db>, } -const MAX_DISPLAYED_UNION_ITEMS: usize = 5; -const MAX_DISPLAYED_UNION_ITEMS_WHEN_ELIDED: usize = 3; +const UNION_POLICY: TruncationPolicy = TruncationPolicy { + max: 5, + max_when_elided: 3, +}; impl Display for DisplayUnionType<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -1379,16 +1419,8 @@ impl Display for DisplayUnionType<'_> { 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 display_limit = + UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); let mut condensed_types = Some(condensed_types); let mut displayed_entries = 0usize; @@ -1420,8 +1452,10 @@ 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 { + join.entry(&DisplayOmitted { count: omitted_entries, + singular: "union element", + plural: "union elements", }); } } @@ -1437,38 +1471,45 @@ impl fmt::Debug for DisplayUnionType<'_> { Display::fmt(self, f) } } - -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>, db: &'db dyn Db, settings: DisplaySettings<'db>, } +const LITERAL_POLICY: TruncationPolicy = TruncationPolicy { + max: 7, + max_when_elided: 5, +}; + impl Display for DisplayLiteralGroup<'_> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.write_str("Literal[")?; - f.join(", ") - .entries( - self.literals - .iter() - .map(|ty| ty.representation(self.db, self.settings.singleline())), - ) - .finish()?; + + let total_entries = self.literals.len(); + + let display_limit = + LITERAL_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); + + let mut join = f.join(", "); + + for lit in self.literals.iter().take(display_limit) { + let rep = lit.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: "literal", + plural: "literals", + }); + } + } + + join.finish()?; f.write_str("]") } }