diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 790ba064aa..5bacaa04cb 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -231,7 +231,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { match stmt { Stmt::Assign(assign) => { - self.in_assignment = true; + self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value); for target in &assign.targets { 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)] mod tests { use super::*; @@ -415,47 +441,183 @@ mod tests { #[test] 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" - 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] - fn test_tuple_assignment() { - let test = inlay_hint_test("x, y = (1, 'abc')"); + fn test_unpacked_tuple_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#" - 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] 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#" - 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] 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" + def i(x: int, /) -> int: + return x + x: int = 1 + y[: Literal[1]] = x + z: int = i(1) + w[: int] = z "); } #[test] fn test_assign_statement_out_of_range() { - let test = inlay_hint_test("x = 1\ny = 2"); + let test = inlay_hint_test( + " + def i(x: int, /) -> int: + return x + x = i(1) + z = x", + ); assert_snapshot!(test.inlay_hints(), @r" - x[: Literal[1]] = 1 - y = 2 + def i(x: int, /) -> int: + return x + x[: int] = i(1) + z = x "); } @@ -465,28 +627,236 @@ mod tests { " class A: def __init__(self, y): - self.x = 1 + self.x = int(1) self.y = y a = A(2) - a.y = 3 + a.y = int(3) ", ); assert_snapshot!(test.inlay_hints(), @r" class A: def __init__(self, y): - self.x[: Literal[1]] = 1 + self.x[: int] = int(1) self.y[: Unknown] = y 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] 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!( test.inlay_hints_with_settings(&InlayHintSettings { @@ -494,7 +864,10 @@ mod tests { ..Default::default() }), @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" def foo(x: int): pass - x[: Literal[1]] = 1 - y[: Literal[2]] = 2 + x = 1 + y = 2 foo(x) foo([x=]y) "); @@ -539,7 +912,7 @@ mod tests { " def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): self.x: int = 1 self.y: int = 2 val = MyClass() @@ -551,7 +924,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): self.x: int = 1 self.y: int = 2 val[: MyClass] = MyClass() @@ -568,7 +941,7 @@ mod tests { " def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): self.x: int = 1 self.y: int = 2 x = MyClass() @@ -580,7 +953,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): self.x: int = 1 self.y: int = 2 x[: MyClass] = MyClass() @@ -596,7 +969,7 @@ mod tests { " def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): def x() -> int: return 1 def y() -> int: @@ -610,7 +983,7 @@ mod tests { assert_snapshot!(test.inlay_hints(), @r" def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): def x() -> int: return 1 def y() -> int: @@ -630,7 +1003,7 @@ mod tests { def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): def x() -> List[int]: return 1 def y() -> List[int]: @@ -646,7 +1019,7 @@ mod tests { def foo(x: int): pass class MyClass: - def __init__(): + def __init__(self): def x() -> List[int]: return 1 def y() -> List[int]: diff --git a/crates/ty_server/tests/e2e/inlay_hints.rs b/crates/ty_server/tests/e2e/inlay_hints.rs index 3dbbbee994..ea7c833b5f 100644 --- a/crates/ty_server/tests/e2e/inlay_hints.rs +++ b/crates/ty_server/tests/e2e/inlay_hints.rs @@ -17,7 +17,7 @@ x = 1 def foo(a: int) -> int: return a + 1 -foo(1) +y = foo(1) "; let mut server = TestServerBuilder::new()? @@ -39,7 +39,7 @@ foo(1) [ { "position": { - "line": 0, + "line": 5, "character": 1 }, "label": [ @@ -47,7 +47,7 @@ foo(1) "value": ": " }, { - "value": "Literal[1]" + "value": "int" } ], "kind": 1 @@ -55,7 +55,7 @@ foo(1) { "position": { "line": 5, - "character": 4 + "character": 8 }, "label": [ {