[ty] Add more random TypeDetails and tests (#21546)

This commit is contained in:
Aria Desires 2025-11-20 14:46:17 -05:00 committed by GitHub
parent 290a5720cb
commit 1b28fc1f14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 428 additions and 74 deletions

View File

@ -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[: <class 'Foo[int]'>] = 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[: <class 'Foo[int]'>] = 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[: <class 'Foo[int]'>] = 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(

View File

@ -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 '{}'>",
class.display_with(self.db, self.settings.clone())
),
Type::GenericAlias(generic) => write!(
f.with_detail(TypeDetail::Type(self.ty)),
"<class '{}'>",
generic.display_with(self.db, self.settings.singleline())
),
Type::ClassLiteral(class) => {
let mut f = f.with_detail(TypeDetail::Type(self.ty));
f.write_str("<class '")?;
class
.display_with(self.db, self.settings.clone())
.fmt_detailed(&mut f)?;
f.write_str("'>")
}
Type::GenericAlias(generic) => {
let mut f = f.with_detail(TypeDetail::Type(self.ty));
f.write_str("<class '")?;
generic
.display_with(self.db, self.settings.clone())
.fmt_detailed(&mut f)?;
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,