[ty] supress some trivial expr inlay hints (#21367)

I'm not 100% sold on this implementation, but it's a strict improvement
and it adds a ton of snapshot tests for future iteration.

Part of https://github.com/astral-sh/ty/issues/494
This commit is contained in:
Aria Desires 2025-11-10 14:51:14 -05:00 committed by GitHub
parent deeda56906
commit d258302b08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 405 additions and 32 deletions

View file

@ -231,7 +231,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
match stmt { match stmt {
Stmt::Assign(assign) => { Stmt::Assign(assign) => {
self.in_assignment = true; self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
for target in &assign.targets { for target in &assign.targets {
self.visit_expr(target); self.visit_expr(target);
} }
@ -324,6 +324,32 @@ fn arg_matches_name(arg_or_keyword: &ArgOrKeyword, name: &str) -> bool {
} }
} }
/// Given an expression that's the RHS of an assignment, would it be excessive to
/// emit an inlay type hint for the variable assigned to it?
///
/// This is used to suppress inlay hints for things like `x = 1`, `x, y = (1, 2)`, etc.
fn type_hint_is_excessive_for_expr(expr: &Expr) -> bool {
match expr {
// A tuple of all literals is excessive to typehint
Expr::Tuple(expr_tuple) => expr_tuple.elts.iter().all(type_hint_is_excessive_for_expr),
// Various Literal[...] types which are always excessive to hint
| Expr::BytesLiteral(_)
| Expr::NumberLiteral(_)
| Expr::BooleanLiteral(_)
| Expr::StringLiteral(_)
// `None` isn't terribly verbose, but still redundant
| Expr::NoneLiteral(_)
// This one expands to `str` which isn't verbose but is redundant
| Expr::FString(_)
// This one expands to `Template` which isn't verbose but is redundant
| Expr::TString(_)=> true,
// Everything else is reasonable
_ => false,
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -415,47 +441,183 @@ mod tests {
#[test] #[test]
fn test_assign_statement() { fn test_assign_statement() {
let test = inlay_hint_test("x = 1"); let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
x = 1
y = x
z = i(1)
w = z
",
);
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
x[: Literal[1]] = 1 def i(x: int, /) -> int:
return x
x = 1
y[: Literal[1]] = x
z[: int] = i(1)
w[: int] = z
"); ");
} }
#[test] #[test]
fn test_tuple_assignment() { fn test_unpacked_tuple_assignment() {
let test = inlay_hint_test("x, y = (1, 'abc')"); let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x1, y1 = (1, 'abc')
x2, y2 = (x1, y1)
x3, y3 = (i(1), s('abc'))
x4, y4 = (x3, y3)
",
);
assert_snapshot!(test.inlay_hints(), @r#" assert_snapshot!(test.inlay_hints(), @r#"
x[: Literal[1]], y[: Literal["abc"]] = (1, 'abc') def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x1, y1 = (1, 'abc')
x2[: Literal[1]], y2[: Literal["abc"]] = (x1, y1)
x3[: int], y3[: str] = (i(1), s('abc'))
x4[: int], y4[: str] = (x3, y3)
"#);
}
#[test]
fn test_multiple_assignment() {
let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x1, y1 = 1, 'abc'
x2, y2 = x1, y1
x3, y3 = i(1), s('abc')
x4, y4 = x3, y3
",
);
assert_snapshot!(test.inlay_hints(), @r#"
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x1, y1 = 1, 'abc'
x2[: Literal[1]], y2[: Literal["abc"]] = x1, y1
x3[: int], y3[: str] = i(1), s('abc')
x4[: int], y4[: str] = x3, y3
"#);
}
#[test]
fn test_tuple_assignment() {
let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x = (1, 'abc')
y = x
z = (i(1), s('abc'))
w = z
",
);
assert_snapshot!(test.inlay_hints(), @r#"
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x = (1, 'abc')
y[: tuple[Literal[1], Literal["abc"]]] = x
z[: tuple[int, str]] = (i(1), s('abc'))
w[: tuple[int, str]] = z
"#); "#);
} }
#[test] #[test]
fn test_nested_tuple_assignment() { fn test_nested_tuple_assignment() {
let test = inlay_hint_test("x, (y, z) = (1, ('abc', 2))"); let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x1, (y1, z1) = (1, ('abc', 2))
x2, (y2, z2) = (x1, (y1, z1))
x3, (y3, z3) = (i(1), (s('abc'), i(2)))
x4, (y4, z4) = (x3, (y3, z3))",
);
assert_snapshot!(test.inlay_hints(), @r#" assert_snapshot!(test.inlay_hints(), @r#"
x[: Literal[1]], (y[: Literal["abc"]], z[: Literal[2]]) = (1, ('abc', 2)) def i(x: int, /) -> int:
return x
def s(x: str, /) -> str:
return x
x1, (y1, z1) = (1, ('abc', 2))
x2[: Literal[1]], (y2[: Literal["abc"]], z2[: Literal[2]]) = (x1, (y1, z1))
x3[: int], (y3[: str], z3[: int]) = (i(1), (s('abc'), i(2)))
x4[: int], (y4[: str], z4[: int]) = (x3, (y3, z3))
"#); "#);
} }
#[test] #[test]
fn test_assign_statement_with_type_annotation() { fn test_assign_statement_with_type_annotation() {
let test = inlay_hint_test("x: int = 1"); let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
x: int = 1
y = x
z: int = i(1)
w = z",
);
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
def i(x: int, /) -> int:
return x
x: int = 1 x: int = 1
y[: Literal[1]] = x
z: int = i(1)
w[: int] = z
"); ");
} }
#[test] #[test]
fn test_assign_statement_out_of_range() { fn test_assign_statement_out_of_range() {
let test = inlay_hint_test("<START>x = 1<END>\ny = 2"); let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
<START>x = i(1)<END>
z = x",
);
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
x[: Literal[1]] = 1 def i(x: int, /) -> int:
y = 2 return x
x[: int] = i(1)
z = x
"); ");
} }
@ -465,28 +627,236 @@ mod tests {
" "
class A: class A:
def __init__(self, y): def __init__(self, y):
self.x = 1 self.x = int(1)
self.y = y self.y = y
a = A(2) a = A(2)
a.y = 3 a.y = int(3)
", ",
); );
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
class A: class A:
def __init__(self, y): def __init__(self, y):
self.x[: Literal[1]] = 1 self.x[: int] = int(1)
self.y[: Unknown] = y self.y[: Unknown] = y
a[: A] = A([y=]2) a[: A] = A([y=]2)
a.y[: Literal[3]] = 3 a.y[: int] = int(3)
"); ");
} }
#[test]
fn test_many_literals() {
let test = inlay_hint_test(
r#"
a = 1
b = 1.0
c = True
d = None
e = "hello"
f = 'there'
g = f"{e} {f}"
h = t"wow %d"
i = b'\x00'
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
a = 1
b = 1.0
c = True
d = None
e = "hello"
f = 'there'
g = f"{e} {f}"
h = t"wow %d"
i = b'\x00'
"#);
}
#[test]
fn test_many_literals_tuple() {
let test = inlay_hint_test(
r#"
a = (1, 2)
b = (1.0, 2.0)
c = (True, False)
d = (None, None)
e = ("hel", "lo")
f = ('the', 're')
g = (f"{ft}", f"{ft}")
h = (t"wow %d", t"wow %d")
i = (b'\x01', b'\x02')
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
a = (1, 2)
b = (1.0, 2.0)
c = (True, False)
d = (None, None)
e = ("hel", "lo")
f = ('the', 're')
g = (f"{ft}", f"{ft}")
h = (t"wow %d", t"wow %d")
i = (b'\x01', b'\x02')
"#);
}
#[test]
fn test_many_literals_unpacked_tuple() {
let test = inlay_hint_test(
r#"
a1, a2 = (1, 2)
b1, b2 = (1.0, 2.0)
c1, c2 = (True, False)
d1, d2 = (None, None)
e1, e2 = ("hel", "lo")
f1, f2 = ('the', 're')
g1, g2 = (f"{ft}", f"{ft}")
h1, h2 = (t"wow %d", t"wow %d")
i1, i2 = (b'\x01', b'\x02')
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
a1, a2 = (1, 2)
b1, b2 = (1.0, 2.0)
c1, c2 = (True, False)
d1, d2 = (None, None)
e1, e2 = ("hel", "lo")
f1, f2 = ('the', 're')
g1, g2 = (f"{ft}", f"{ft}")
h1, h2 = (t"wow %d", t"wow %d")
i1, i2 = (b'\x01', b'\x02')
"#);
}
#[test]
fn test_many_literals_multiple() {
let test = inlay_hint_test(
r#"
a1, a2 = 1, 2
b1, b2 = 1.0, 2.0
c1, c2 = True, False
d1, d2 = None, None
e1, e2 = "hel", "lo"
f1, f2 = 'the', 're'
g1, g2 = f"{ft}", f"{ft}"
h1, h2 = t"wow %d", t"wow %d"
i1, i2 = b'\x01', b'\x02'
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
a1, a2 = 1, 2
b1, b2 = 1.0, 2.0
c1, c2 = True, False
d1, d2 = None, None
e1, e2 = "hel", "lo"
f1, f2 = 'the', 're'
g1, g2 = f"{ft}", f"{ft}"
h1, h2 = t"wow %d", t"wow %d"
i1, i2 = b'\x01', b'\x02'
"#);
}
#[test]
fn test_many_literals_list() {
let test = inlay_hint_test(
r#"
a = [1, 2]
b = [1.0, 2.0]
c = [True, False]
d = [None, None]
e = ["hel", "lo"]
f = ['the', 're']
g = [f"{ft}", f"{ft}"]
h = [t"wow %d", t"wow %d"]
i = [b'\x01', b'\x02']
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
a[: list[Unknown | int]] = [1, 2]
b[: list[Unknown | float]] = [1.0, 2.0]
c[: list[Unknown | bool]] = [True, False]
d[: list[Unknown | None]] = [None, None]
e[: list[Unknown | str]] = ["hel", "lo"]
f[: list[Unknown | str]] = ['the', 're']
g[: list[Unknown | str]] = [f"{ft}", f"{ft}"]
h[: list[Unknown | Template]] = [t"wow %d", t"wow %d"]
i[: list[Unknown | bytes]] = [b'\x01', b'\x02']
"#);
}
#[test]
fn test_simple_init_call() {
let test = inlay_hint_test(
r#"
class MyClass:
def __init__(self):
self.x: int = 1
x = MyClass()
y = (MyClass(), MyClass())
a, b = MyClass(), MyClass()
c, d = (MyClass(), MyClass())
"#,
);
assert_snapshot!(test.inlay_hints(), @r"
class MyClass:
def __init__(self):
self.x: int = 1
x[: MyClass] = MyClass()
y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
a[: MyClass], b[: MyClass] = MyClass(), MyClass()
c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
");
}
#[test]
fn test_generic_init_call() {
let test = inlay_hint_test(
r#"
class MyClass[T, U]:
def __init__(self, x: list[T], y: tuple[U, U]):
self.x = x
self.y = y
x = MyClass([42], ("a", "b"))
y = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
a, b = MyClass([42], ("a", "b")), MyClass([42], ("a", "b"))
c, d = (MyClass([42], ("a", "b")), MyClass([42], ("a", "b")))
"#,
);
assert_snapshot!(test.inlay_hints(), @r#"
class MyClass[T, U]:
def __init__(self, x: list[T], y: tuple[U, U]):
self.x[: list[T@MyClass]] = x
self.y[: tuple[U@MyClass, U@MyClass]] = y
x[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b"))
y[: tuple[MyClass[Unknown | int, str], MyClass[Unknown | int, str]]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")))
a[: MyClass[Unknown | int, str]], b[: MyClass[Unknown | int, str]] = MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b"))
c[: MyClass[Unknown | int, str]], d[: MyClass[Unknown | int, str]] = (MyClass([x=][42], [y=]("a", "b")), MyClass([x=][42], [y=]("a", "b")))
"#);
}
#[test] #[test]
fn test_disabled_variable_types() { fn test_disabled_variable_types() {
let test = inlay_hint_test("x = 1"); let test = inlay_hint_test(
"
def i(x: int, /) -> int:
return x
x = i(1)
",
);
assert_snapshot!( assert_snapshot!(
test.inlay_hints_with_settings(&InlayHintSettings { test.inlay_hints_with_settings(&InlayHintSettings {
@ -494,7 +864,10 @@ mod tests {
..Default::default() ..Default::default()
}), }),
@r" @r"
x = 1 def i(x: int, /) -> int:
return x
x = i(1)
" "
); );
} }
@ -526,8 +899,8 @@ mod tests {
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
def foo(x: int): pass def foo(x: int): pass
x[: Literal[1]] = 1 x = 1
y[: Literal[2]] = 2 y = 2
foo(x) foo(x)
foo([x=]y) foo([x=]y)
"); ");
@ -539,7 +912,7 @@ mod tests {
" "
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
self.x: int = 1 self.x: int = 1
self.y: int = 2 self.y: int = 2
val = MyClass() val = MyClass()
@ -551,7 +924,7 @@ mod tests {
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
self.x: int = 1 self.x: int = 1
self.y: int = 2 self.y: int = 2
val[: MyClass] = MyClass() val[: MyClass] = MyClass()
@ -568,7 +941,7 @@ mod tests {
" "
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
self.x: int = 1 self.x: int = 1
self.y: int = 2 self.y: int = 2
x = MyClass() x = MyClass()
@ -580,7 +953,7 @@ mod tests {
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
self.x: int = 1 self.x: int = 1
self.y: int = 2 self.y: int = 2
x[: MyClass] = MyClass() x[: MyClass] = MyClass()
@ -596,7 +969,7 @@ mod tests {
" "
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
def x() -> int: def x() -> int:
return 1 return 1
def y() -> int: def y() -> int:
@ -610,7 +983,7 @@ mod tests {
assert_snapshot!(test.inlay_hints(), @r" assert_snapshot!(test.inlay_hints(), @r"
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
def x() -> int: def x() -> int:
return 1 return 1
def y() -> int: def y() -> int:
@ -630,7 +1003,7 @@ mod tests {
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
def x() -> List[int]: def x() -> List[int]:
return 1 return 1
def y() -> List[int]: def y() -> List[int]:
@ -646,7 +1019,7 @@ mod tests {
def foo(x: int): pass def foo(x: int): pass
class MyClass: class MyClass:
def __init__(): def __init__(self):
def x() -> List[int]: def x() -> List[int]:
return 1 return 1
def y() -> List[int]: def y() -> List[int]:

View file

@ -17,7 +17,7 @@ x = 1
def foo(a: int) -> int: def foo(a: int) -> int:
return a + 1 return a + 1
foo(1) y = foo(1)
"; ";
let mut server = TestServerBuilder::new()? let mut server = TestServerBuilder::new()?
@ -39,7 +39,7 @@ foo(1)
[ [
{ {
"position": { "position": {
"line": 0, "line": 5,
"character": 1 "character": 1
}, },
"label": [ "label": [
@ -47,7 +47,7 @@ foo(1)
"value": ": " "value": ": "
}, },
{ {
"value": "Literal[1]" "value": "int"
} }
], ],
"kind": 1 "kind": 1
@ -55,7 +55,7 @@ foo(1)
{ {
"position": { "position": {
"line": 5, "line": 5,
"character": 4 "character": 8
}, },
"label": [ "label": [
{ {