From f497d341a5d5bc9e8309d1f3c5b676ff3bb96f85 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 10 Nov 2025 10:46:06 -0500 Subject: [PATCH 1/2] elide redundant inlay hints for function args --- crates/ty_ide/src/inlay_hints.rs | 163 ++++++++++++++++++++++++++++++- 1 file changed, 161 insertions(+), 2 deletions(-) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 47859c39e3881..8af7bec30e38e 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -4,7 +4,7 @@ use crate::Db; use ruff_db::files::File; use ruff_db::parsed::parsed_module; use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal}; -use ruff_python_ast::{AnyNodeRef, Expr, Stmt}; +use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, Stmt}; use ruff_text_size::{Ranged, TextRange, TextSize}; use ty_python_semantic::types::Type; use ty_python_semantic::types::ide_support::inlay_hint_function_argument_details; @@ -283,7 +283,9 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { self.visit_expr(&call.func); for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() { - if let Some(name) = argument_names.get(&index) { + if let Some(name) = argument_names.get(&index) + && !arg_matches_name(&arg_or_keyword, name) + { self.add_call_argument_name(arg_or_keyword.range().start(), name); } self.visit_expr(arg_or_keyword.value()); @@ -296,6 +298,32 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> { } } +/// Given a positional argument, check if the expression is the "same name" +/// as the function argument itself. +/// +/// This allows us to filter out reptitive inlay hints like `x=x`, `x=y.x`, etc. +fn arg_matches_name(arg_or_keyword: &ArgOrKeyword, name: &str) -> bool { + // Only care about positional args + let ArgOrKeyword::Arg(arg) = arg_or_keyword else { + return false; + }; + + let mut expr = *arg; + loop { + match expr { + // `x=x(1, 2)` counts as a match, recurse for it + Expr::Call(expr_call) => expr = &expr_call.func, + // `x=x[0]` is a match, recurse for it + Expr::Subscript(expr_subscript) => expr = &expr_subscript.value, + // `x=x` is a match + Expr::Name(expr_name) => return expr_name.id.as_str() == name, + // `x=y.x` is a match + Expr::Attribute(expr_attribute) => return expr_attribute.attr.as_str() == name, + _ => return false, + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -485,6 +513,137 @@ mod tests { "); } + #[test] + fn test_function_call_with_positional_or_keyword_parameter_redundant_name() { + let test = inlay_hint_test( + " + def foo(x: int): pass + x = 1 + y = 2 + foo(x) + foo(y)", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo(x: int): pass + x[: Literal[1]] = 1 + y[: Literal[2]] = 2 + foo(x) + foo([x=]y) + "); + } + + #[test] + fn test_function_call_with_positional_or_keyword_parameter_redundant_attribute() { + let test = inlay_hint_test( + " + def foo(x: int): pass + class MyClass: + def __init__(): + self.x: int = 1 + self.y: int = 2 + val = MyClass() + + foo(val.x) + foo(val.y)", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo(x: int): pass + class MyClass: + def __init__(): + self.x: int = 1 + self.y: int = 2 + val[: MyClass] = MyClass() + + foo(val.x) + foo([x=]val.y) + "); + } + + #[test] + fn test_function_call_with_positional_or_keyword_parameter_redundant_attribute_not() { + // This one checks that we don't allow elide `x=` for `x.y` + let test = inlay_hint_test( + " + def foo(x: int): pass + class MyClass: + def __init__(): + self.x: int = 1 + self.y: int = 2 + x = MyClass() + + foo(x.x) + foo(x.y)", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo(x: int): pass + class MyClass: + def __init__(): + self.x: int = 1 + self.y: int = 2 + x[: MyClass] = MyClass() + + foo(x.x) + foo([x=]x.y) + "); + } + + #[test] + fn test_function_call_with_positional_or_keyword_parameter_redundant_call() { + let test = inlay_hint_test( + " + def foo(x: int): pass + class MyClass: + def __init__(): + def x() -> int: + return 1 + def y() -> int: + return 2 + val = MyClass() + + foo(val.x()) + foo(val.y())", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo(x: int): pass + class MyClass: + def __init__(): + def x() -> int: + return 1 + def y() -> int: + return 2 + val[: MyClass] = MyClass() + + foo(val.x()) + foo([x=]val.y()) + "); + } + + #[test] + fn test_function_call_with_positional_or_keyword_parameter_redundant_subscript() { + let test = inlay_hint_test( + " + def foo(x: int): pass + x = [1] + y = [2] + + foo(x[0]) + foo(y[0])", + ); + + assert_snapshot!(test.inlay_hints(), @r" + def foo(x: int): pass + x[: list[Unknown | int]] = [1] + y[: list[Unknown | int]] = [2] + + foo(x[0]) + foo([x=]y[0]) + "); + } + #[test] fn test_function_call_with_positional_only_parameter() { let test = inlay_hint_test( From a2ac0ae47db75d8f55da66234deb19d53c4adbe2 Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Mon, 10 Nov 2025 10:52:07 -0500 Subject: [PATCH 2/2] add complex test --- crates/ty_ide/src/inlay_hints.rs | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/crates/ty_ide/src/inlay_hints.rs b/crates/ty_ide/src/inlay_hints.rs index 8af7bec30e38e..790ba064aab6c 100644 --- a/crates/ty_ide/src/inlay_hints.rs +++ b/crates/ty_ide/src/inlay_hints.rs @@ -622,6 +622,42 @@ mod tests { "); } + #[test] + fn test_function_call_with_positional_or_keyword_parameter_redundant_complex() { + let test = inlay_hint_test( + " + from typing import List + + def foo(x: int): pass + class MyClass: + def __init__(): + def x() -> List[int]: + return 1 + def y() -> List[int]: + return 2 + val = MyClass() + + foo(val.x()[0]) + foo(val.y()[1])", + ); + + assert_snapshot!(test.inlay_hints(), @r" + from typing import List + + def foo(x: int): pass + class MyClass: + def __init__(): + def x() -> List[int]: + return 1 + def y() -> List[int]: + return 2 + val[: MyClass] = MyClass() + + foo(val.x()[0]) + foo([x=]val.y()[1]) + "); + } + #[test] fn test_function_call_with_positional_or_keyword_parameter_redundant_subscript() { let test = inlay_hint_test(