diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 26ae12a8449f66..df3ac6acd6f221 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -971,6 +971,24 @@ mod tests { w[: tuple[int, str]] = z --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2695:7 + | + 2694 | @disjoint_base + 2695 | class tuple(Sequence[_T_co]): + | ^^^^^ + 2696 | """Built-in immutable sequence. + | + info: Source + --> main2.py:8:5 + | + 7 | x = (1, 'abc') + 8 | y[: tuple[Literal[1], Literal["abc"]]] = x + | ^^^^^ + 9 | z[: tuple[int, str]] = (i(1), s('abc')) + 10 | w[: tuple[int, str]] = z + | + info[inlay-hint-location]: Inlay Hint Target --> stdlib/typing.pyi:351:1 | @@ -1009,6 +1027,24 @@ mod tests { 10 | w[: tuple[int, str]] = z | + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2695:7 + | + 2694 | @disjoint_base + 2695 | class tuple(Sequence[_T_co]): + | ^^^^^ + 2696 | """Built-in immutable sequence. + | + info: Source + --> main2.py:9:5 + | + 7 | x = (1, 'abc') + 8 | y[: tuple[Literal[1], Literal["abc"]]] = x + 9 | z[: tuple[int, str]] = (i(1), s('abc')) + | ^^^^^ + 10 | w[: tuple[int, str]] = z + | + info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:348:7 | @@ -1047,6 +1083,23 @@ mod tests { 10 | w[: tuple[int, str]] = z | + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2695:7 + | + 2694 | @disjoint_base + 2695 | class tuple(Sequence[_T_co]): + | ^^^^^ + 2696 | """Built-in immutable sequence. + | + info: Source + --> main2.py:10:5 + | + 8 | y[: tuple[Literal[1], Literal["abc"]]] = x + 9 | z[: tuple[int, str]] = (i(1), s('abc')) + 10 | w[: tuple[int, str]] = z + | ^^^^^ + | + info[inlay-hint-location]: Inlay Hint Target --> stdlib/builtins.pyi:348:7 | @@ -2343,7 +2396,7 @@ mod tests { "#, ); - assert_snapshot!(test.inlay_hints(), @r" + assert_snapshot!(test.inlay_hints(), @r#" class MyClass: def __init__(self): self.x: int = 1 @@ -2373,6 +2426,24 @@ mod tests { 8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass() | + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2695:7 + | + 2694 | @disjoint_base + 2695 | class tuple(Sequence[_T_co]): + | ^^^^^ + 2696 | """Built-in immutable sequence. + | + info: Source + --> main2.py:7:5 + | + 6 | x[: MyClass] = MyClass() + 7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass()) + | ^^^^^ + 8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass() + 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) + | + info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | @@ -2478,7 +2549,7 @@ mod tests { 9 | c[: MyClass], d[: MyClass] = (MyClass(), MyClass()) | ^^^^^^^ | - "); + "#); } #[test] @@ -2527,6 +2598,25 @@ mod tests { 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y | + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2695:7 + | + 2694 | @disjoint_base + 2695 | class tuple(Sequence[_T_co]): + | ^^^^^ + 2696 | """Built-in immutable sequence. + | + info: Source + --> main2.py:5:18 + | + 3 | def __init__(self, x: list[T], y: tuple[U, U]): + 4 | self.x[: list[T@MyClass]] = x + 5 | self.y[: tuple[U@MyClass, U@MyClass]] = y + | ^^^^^ + 6 | + 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) + | + info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | @@ -2646,6 +2736,24 @@ mod tests { 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… | + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2695:7 + | + 2694 | @disjoint_base + 2695 | class tuple(Sequence[_T_co]): + | ^^^^^ + 2696 | """Built-in immutable sequence. + | + info: Source + --> main2.py:8:5 + | + 7 | x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")) + 8 | y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a",… + | ^^^^^ + 9 | a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b… + 10 | c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "… + | + info[inlay-hint-location]: Inlay Hint Target --> main.py:2:7 | @@ -4830,6 +4938,191 @@ mod tests { "#); } + #[test] + fn test_literal_group() { + let mut test = inlay_hint_test( + r#" + def branch(cond: int): + if cond < 10: + x = 1 + elif cond < 20: + x = 2 + elif cond < 30: + x = 3 + elif cond < 40: + x = "hello" + else: + x = None + y = x"#, + ); + + assert_snapshot!(test.inlay_hints(), @r#" + def branch(cond: int): + if cond < 10: + x = 1 + elif cond < 20: + x = 2 + elif cond < 30: + x = 3 + elif cond < 40: + x = "hello" + else: + x = None + y[: Literal[1, 2, 3, "hello"] | None] = x + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/typing.pyi:351:1 + | + 349 | Final: _SpecialForm + 350 | + 351 | Literal: _SpecialForm + | ^^^^^^^ + 352 | TypedDict: _SpecialForm + | + info: Source + --> main2.py:13:9 + | + 11 | else: + 12 | x = None + 13 | y[: Literal[1, 2, 3, "hello"] | None] = x + | ^^^^^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/types.pyi:950:11 + | + 948 | if sys.version_info >= (3, 10): + 949 | @final + 950 | class NoneType: + | ^^^^^^^^ + 951 | """The type of the None singleton.""" + | + info: Source + --> main2.py:13:37 + | + 11 | else: + 12 | x = None + 13 | y[: Literal[1, 2, 3, "hello"] | None] = x + | ^^^^ + | + "#); + } + + #[test] + fn test_generic_alias() { + let mut test = inlay_hint_test( + r" + class Foo[T]: ... + + a = Foo[int]", + ); + + assert_snapshot!(test.inlay_hints(), @r#" + class Foo[T]: ... + + a[: ] = Foo[int] + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> main.py:2:7 + | + 2 | class Foo[T]: ... + | ^^^ + 3 | + 4 | a = Foo[int] + | + info: Source + --> main2.py:4:13 + | + 2 | class Foo[T]: ... + 3 | + 4 | a[: ] = Foo[int] + | ^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:348:7 + | + 347 | @disjoint_base + 348 | class int: + | ^^^ + 349 | """int([x]) -> integer + 350 | int(x, base=10) -> integer + | + info: Source + --> main2.py:4:17 + | + 2 | class Foo[T]: ... + 3 | + 4 | a[: ] = Foo[int] + | ^^^ + | + "#); + } + + #[test] + fn test_subclass_type() { + let mut test = inlay_hint_test( + r" + def f(x: list[str]): + y = type(x)", + ); + + assert_snapshot!(test.inlay_hints(), @r#" + def f(x: list[str]): + y[: type[list[str]]] = type(x) + --------------------------------------------- + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:247:7 + | + 246 | @disjoint_base + 247 | class type: + | ^^^^ + 248 | """type(object) -> the object's type + 249 | type(name, bases, dict, **kwds) -> a new type + | + info: Source + --> main2.py:3:9 + | + 2 | def f(x: list[str]): + 3 | y[: type[list[str]]] = type(x) + | ^^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:2802:7 + | + 2801 | @disjoint_base + 2802 | class list(MutableSequence[_T]): + | ^^^^ + 2803 | """Built-in mutable sequence. + | + info: Source + --> main2.py:3:14 + | + 2 | def f(x: list[str]): + 3 | y[: type[list[str]]] = type(x) + | ^^^^ + | + + info[inlay-hint-location]: Inlay Hint Target + --> stdlib/builtins.pyi:915:7 + | + 914 | @disjoint_base + 915 | class str(Sequence[str]): + | ^^^ + 916 | """str(object='') -> str + 917 | str(bytes_or_buffer[, encoding[, errors]]) -> str + | + info: Source + --> main2.py:3:19 + | + 2 | def f(x: list[str]): + 3 | y[: type[list[str]]] = type(x) + | ^^^ + | + "#); + } + #[test] fn test_complex_parameter_combinations() { let mut test = inlay_hint_test( diff --git a/crates/ty_python_semantic/src/types/display.rs b/crates/ty_python_semantic/src/types/display.rs index a396f13e9f79d9..52f6dd75727635 100644 --- a/crates/ty_python_semantic/src/types/display.rs +++ b/crates/ty_python_semantic/src/types/display.rs @@ -6,7 +6,6 @@ use std::collections::hash_map::Entry; use std::fmt::{self, Display, Formatter, Write}; use std::rc::Rc; -use ruff_db::display::FormatterJoinExtension; use ruff_db::files::FilePath; use ruff_db::source::line_index; use ruff_python_ast::str::{Quote, TripleQuotes}; @@ -612,19 +611,9 @@ impl Display for DisplayRepresentation<'_> { impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { match self.ty { - Type::Dynamic(dynamic) => match dynamic { - DynamicType::Any => write!( - f.with_detail(TypeDetail::Type(Type::SpecialForm(SpecialFormType::Any))), - "{dynamic}" - ), - DynamicType::Unknown => write!( - f.with_detail(TypeDetail::Type(Type::SpecialForm( - SpecialFormType::Unknown - ))), - "{dynamic}" - ), - _ => write!(f, "{dynamic}"), - }, + Type::Dynamic(dynamic) => dynamic + .display_with(self.db, self.settings.clone()) + .fmt_detailed(f), Type::Never => f.with_detail(TypeDetail::Type(self.ty)).write_str("Never"), Type::NominalInstance(instance) => { let class = instance.class(self.db); @@ -681,32 +670,50 @@ impl<'db> FmtDetailed<'db> for DisplayRepresentation<'db> { module.module(self.db).name(self.db) ) } - Type::ClassLiteral(class) => write!( - f.with_detail(TypeDetail::Type(self.ty)), - "", - class.display_with(self.db, self.settings.clone()) - ), - Type::GenericAlias(generic) => write!( - f.with_detail(TypeDetail::Type(self.ty)), - "", - generic.display_with(self.db, self.settings.singleline()) - ), + Type::ClassLiteral(class) => { + let mut f = f.with_detail(TypeDetail::Type(self.ty)); + f.write_str("") + } + Type::GenericAlias(generic) => { + let mut f = f.with_detail(TypeDetail::Type(self.ty)); + f.write_str("") + } Type::SubclassOf(subclass_of_ty) => match subclass_of_ty.subclass_of() { SubclassOfInner::Class(ClassType::NonGeneric(class)) => { - f.write_str("type[")?; + f.with_detail(TypeDetail::Type(KnownClass::Type.to_class_literal(self.db))) + .write_str("type")?; + f.write_char('[')?; class .display_with(self.db, self.settings.clone()) .fmt_detailed(f)?; - f.write_str("]") + f.write_char(']') } SubclassOfInner::Class(ClassType::Generic(alias)) => { - f.write_str("type[")?; + f.with_detail(TypeDetail::Type(KnownClass::Type.to_class_literal(self.db))) + .write_str("type")?; + f.write_char('[')?; alias .display_with(self.db, self.settings.clone()) .fmt_detailed(f)?; - f.write_str("]") + f.write_char(']') + } + SubclassOfInner::Dynamic(dynamic) => { + f.with_detail(TypeDetail::Type(KnownClass::Type.to_class_literal(self.db))) + .write_str("type")?; + f.write_char('[')?; + dynamic + .display_with(self.db, self.settings.clone()) + .fmt_detailed(f)?; + f.write_char(']') } - SubclassOfInner::Dynamic(dynamic) => write!(f, "type[{dynamic}]"), }, Type::SpecialForm(special_form) => { write!(f.with_detail(TypeDetail::Type(self.ty)), "{special_form}") @@ -962,7 +969,11 @@ pub(crate) struct DisplayTuple<'a, 'db> { impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { - f.write_str("tuple[")?; + f.with_detail(TypeDetail::Type( + KnownClass::Tuple.to_class_literal(self.db), + )) + .write_str("tuple")?; + f.write_char('[')?; match self.tuple { TupleSpec::Fixed(tuple) => { let elements = tuple.elements_slice(); @@ -998,7 +1009,13 @@ impl<'db> FmtDetailed<'db> for DisplayTuple<'_, 'db> { f.write_str(", ")?; } if !tuple.prefix.is_empty() || !tuple.suffix.is_empty() { - f.write_str("*tuple[")?; + f.write_char('*')?; + // Might as well link the type again here too + f.with_detail(TypeDetail::Type( + KnownClass::Tuple.to_class_literal(self.db), + )) + .write_str("tuple")?; + f.write_char('[')?; } tuple .variable @@ -1119,7 +1136,10 @@ impl<'db> FmtDetailed<'db> for DisplayFunctionType<'db> { signatures => { // TODO: How to display overloads? if !self.settings.multiline { - f.write_str("Overload[")?; + // TODO: This should ideally have a TypeDetail but we actually + // don't have a type for @overload (we just detect the decorator) + f.write_str("Overload")?; + f.write_char('[')?; } let separator = if self.settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); @@ -1142,6 +1162,48 @@ impl Display for DisplayFunctionType<'_> { } } +impl<'db> DynamicType<'db> { + fn display_with<'a>( + &'a self, + db: &'db dyn Db, + settings: DisplaySettings<'db>, + ) -> DisplayDynamicType<'a, 'db> { + DisplayDynamicType { + dynamic_type: self, + db, + settings, + } + } +} + +struct DisplayDynamicType<'a, 'db> { + dynamic_type: &'a DynamicType<'db>, + #[allow(dead_code)] + db: &'db dyn Db, + #[allow(dead_code)] + settings: DisplaySettings<'db>, +} + +impl<'db> FmtDetailed<'db> for DisplayDynamicType<'_, 'db> { + fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + match self.dynamic_type { + DynamicType::Any => write!( + f.with_detail(TypeDetail::Type(Type::SpecialForm(SpecialFormType::Any))), + "{}", + self.dynamic_type, + ), + DynamicType::Unknown => write!( + f.with_detail(TypeDetail::Type(Type::SpecialForm( + SpecialFormType::Unknown + ))), + "{}", + self.dynamic_type, + ), + _ => write!(f, "{}", self.dynamic_type), + } + } +} + impl<'db> GenericAlias<'db> { pub(crate) fn display(self, db: &'db dyn Db) -> DisplayGenericAlias<'db> { self.display_with(db, DisplaySettings::default()) @@ -1463,7 +1525,10 @@ impl<'db> FmtDetailed<'db> for DisplayCallableType<'_, 'db> { signatures => { // TODO: How to display overloads? if !self.settings.multiline { - f.write_str("Overload[")?; + // TODO: This should ideally have a TypeDetail but we actually + // don't have a type for @overload (we just detect the decorator) + f.write_str("Overload")?; + f.write_char('[')?; } let separator = if self.settings.multiline { "\n" } else { ", " }; let mut join = f.join(separator); @@ -1711,8 +1776,8 @@ struct DisplayOmitted { plural: &'static str, } -impl Display for DisplayOmitted { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { +impl<'db> FmtDetailed<'db> for DisplayOmitted { + fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { let noun = if self.count == 1 { self.singular } else { @@ -1722,6 +1787,12 @@ impl Display for DisplayOmitted { } } +impl Display for DisplayOmitted { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.fmt_detailed(&mut TypeWriter::Formatter(f)) + } +} + impl<'db> UnionType<'db> { fn display_with<'a>( &'a self, @@ -1774,15 +1845,7 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { assert_ne!(total_entries, 0); // Done manually because we have a mix of FmtDetailed and Display - let mut is_first = true; - let mut write_join = |f: &mut TypeWriter<'_, '_, 'db>| { - if !is_first { - f.write_str(" | ") - } else { - is_first = false; - Ok(()) - } - }; + let mut join = f.join(" | "); let display_limit = UNION_POLICY.display_limit(total_entries, self.settings.preserve_full_unions); @@ -1798,45 +1861,33 @@ impl<'db> FmtDetailed<'db> for DisplayUnionType<'_, 'db> { if is_condensable(*element) { if let Some(condensed_types) = condensed_types.take() { displayed_entries += 1; - write_join(f)?; - write!( - f, - "{}", - DisplayLiteralGroup { - literals: condensed_types, - db: self.db, - settings: self.settings.singleline(), - } - )?; + join.entry(&DisplayLiteralGroup { + literals: condensed_types, + db: self.db, + settings: self.settings.singleline(), + }); } } else { displayed_entries += 1; - write_join(f)?; - DisplayMaybeParenthesizedType { + join.entry(&DisplayMaybeParenthesizedType { ty: *element, db: self.db, settings: self.settings.singleline(), - } - .fmt_detailed(f)?; + }); } } if !self.settings.preserve_full_unions { let omitted_entries = total_entries.saturating_sub(displayed_entries); if omitted_entries > 0 { - write_join(f)?; - write!( - f, - "{}", - DisplayOmitted { - count: omitted_entries, - singular: "union element", - plural: "union elements", - } - )?; + join.entry(&DisplayOmitted { + count: omitted_entries, + singular: "union element", + plural: "union elements", + }); } } - Ok(()) + join.finish() } } @@ -1862,9 +1913,13 @@ const LITERAL_POLICY: TruncationPolicy = TruncationPolicy { max_when_elided: 5, }; -impl Display for DisplayLiteralGroup<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.write_str("Literal[")?; +impl<'db> FmtDetailed<'db> for DisplayLiteralGroup<'db> { + fn fmt_detailed(&self, f: &mut TypeWriter<'_, '_, 'db>) -> fmt::Result { + f.with_detail(TypeDetail::Type(Type::SpecialForm( + SpecialFormType::Literal, + ))) + .write_str("Literal")?; + f.write_char('[')?; let total_entries = self.literals.len(); @@ -1894,6 +1949,12 @@ impl Display for DisplayLiteralGroup<'_> { } } +impl Display for DisplayLiteralGroup<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + self.fmt_detailed(&mut TypeWriter::Formatter(f)) + } +} + impl<'db> IntersectionType<'db> { fn display_with<'a>( &'a self,