Skip to content

Commit 4821c05

Browse files
authored
[ty] elide redundant inlay hints for function args (#21365)
This elides the following inlay hints: ```py foo([x=]x) foo([x=]y.x) foo([x=]x[0]) foo([x=]x(...)) # composes to complex situations foo([x=]y.x(..)[0]) ``` Fixes astral-sh/ty#1514
1 parent 835e31b commit 4821c05

File tree

1 file changed

+197
-2
lines changed

1 file changed

+197
-2
lines changed

crates/ty_ide/src/inlay_hints.rs

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::Db;
44
use ruff_db::files::File;
55
use ruff_db::parsed::parsed_module;
66
use ruff_python_ast::visitor::source_order::{self, SourceOrderVisitor, TraversalSignal};
7-
use ruff_python_ast::{AnyNodeRef, Expr, Stmt};
7+
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword, Expr, Stmt};
88
use ruff_text_size::{Ranged, TextRange, TextSize};
99
use ty_python_semantic::types::Type;
1010
use ty_python_semantic::types::ide_support::inlay_hint_function_argument_details;
@@ -283,7 +283,9 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
283283
self.visit_expr(&call.func);
284284

285285
for (index, arg_or_keyword) in call.arguments.arguments_source_order().enumerate() {
286-
if let Some(name) = argument_names.get(&index) {
286+
if let Some(name) = argument_names.get(&index)
287+
&& !arg_matches_name(&arg_or_keyword, name)
288+
{
287289
self.add_call_argument_name(arg_or_keyword.range().start(), name);
288290
}
289291
self.visit_expr(arg_or_keyword.value());
@@ -296,6 +298,32 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
296298
}
297299
}
298300

301+
/// Given a positional argument, check if the expression is the "same name"
302+
/// as the function argument itself.
303+
///
304+
/// This allows us to filter out reptitive inlay hints like `x=x`, `x=y.x`, etc.
305+
fn arg_matches_name(arg_or_keyword: &ArgOrKeyword, name: &str) -> bool {
306+
// Only care about positional args
307+
let ArgOrKeyword::Arg(arg) = arg_or_keyword else {
308+
return false;
309+
};
310+
311+
let mut expr = *arg;
312+
loop {
313+
match expr {
314+
// `x=x(1, 2)` counts as a match, recurse for it
315+
Expr::Call(expr_call) => expr = &expr_call.func,
316+
// `x=x[0]` is a match, recurse for it
317+
Expr::Subscript(expr_subscript) => expr = &expr_subscript.value,
318+
// `x=x` is a match
319+
Expr::Name(expr_name) => return expr_name.id.as_str() == name,
320+
// `x=y.x` is a match
321+
Expr::Attribute(expr_attribute) => return expr_attribute.attr.as_str() == name,
322+
_ => return false,
323+
}
324+
}
325+
}
326+
299327
#[cfg(test)]
300328
mod tests {
301329
use super::*;
@@ -485,6 +513,173 @@ mod tests {
485513
");
486514
}
487515

516+
#[test]
517+
fn test_function_call_with_positional_or_keyword_parameter_redundant_name() {
518+
let test = inlay_hint_test(
519+
"
520+
def foo(x: int): pass
521+
x = 1
522+
y = 2
523+
foo(x)
524+
foo(y)",
525+
);
526+
527+
assert_snapshot!(test.inlay_hints(), @r"
528+
def foo(x: int): pass
529+
x[: Literal[1]] = 1
530+
y[: Literal[2]] = 2
531+
foo(x)
532+
foo([x=]y)
533+
");
534+
}
535+
536+
#[test]
537+
fn test_function_call_with_positional_or_keyword_parameter_redundant_attribute() {
538+
let test = inlay_hint_test(
539+
"
540+
def foo(x: int): pass
541+
class MyClass:
542+
def __init__():
543+
self.x: int = 1
544+
self.y: int = 2
545+
val = MyClass()
546+
547+
foo(val.x)
548+
foo(val.y)",
549+
);
550+
551+
assert_snapshot!(test.inlay_hints(), @r"
552+
def foo(x: int): pass
553+
class MyClass:
554+
def __init__():
555+
self.x: int = 1
556+
self.y: int = 2
557+
val[: MyClass] = MyClass()
558+
559+
foo(val.x)
560+
foo([x=]val.y)
561+
");
562+
}
563+
564+
#[test]
565+
fn test_function_call_with_positional_or_keyword_parameter_redundant_attribute_not() {
566+
// This one checks that we don't allow elide `x=` for `x.y`
567+
let test = inlay_hint_test(
568+
"
569+
def foo(x: int): pass
570+
class MyClass:
571+
def __init__():
572+
self.x: int = 1
573+
self.y: int = 2
574+
x = MyClass()
575+
576+
foo(x.x)
577+
foo(x.y)",
578+
);
579+
580+
assert_snapshot!(test.inlay_hints(), @r"
581+
def foo(x: int): pass
582+
class MyClass:
583+
def __init__():
584+
self.x: int = 1
585+
self.y: int = 2
586+
x[: MyClass] = MyClass()
587+
588+
foo(x.x)
589+
foo([x=]x.y)
590+
");
591+
}
592+
593+
#[test]
594+
fn test_function_call_with_positional_or_keyword_parameter_redundant_call() {
595+
let test = inlay_hint_test(
596+
"
597+
def foo(x: int): pass
598+
class MyClass:
599+
def __init__():
600+
def x() -> int:
601+
return 1
602+
def y() -> int:
603+
return 2
604+
val = MyClass()
605+
606+
foo(val.x())
607+
foo(val.y())",
608+
);
609+
610+
assert_snapshot!(test.inlay_hints(), @r"
611+
def foo(x: int): pass
612+
class MyClass:
613+
def __init__():
614+
def x() -> int:
615+
return 1
616+
def y() -> int:
617+
return 2
618+
val[: MyClass] = MyClass()
619+
620+
foo(val.x())
621+
foo([x=]val.y())
622+
");
623+
}
624+
625+
#[test]
626+
fn test_function_call_with_positional_or_keyword_parameter_redundant_complex() {
627+
let test = inlay_hint_test(
628+
"
629+
from typing import List
630+
631+
def foo(x: int): pass
632+
class MyClass:
633+
def __init__():
634+
def x() -> List[int]:
635+
return 1
636+
def y() -> List[int]:
637+
return 2
638+
val = MyClass()
639+
640+
foo(val.x()[0])
641+
foo(val.y()[1])",
642+
);
643+
644+
assert_snapshot!(test.inlay_hints(), @r"
645+
from typing import List
646+
647+
def foo(x: int): pass
648+
class MyClass:
649+
def __init__():
650+
def x() -> List[int]:
651+
return 1
652+
def y() -> List[int]:
653+
return 2
654+
val[: MyClass] = MyClass()
655+
656+
foo(val.x()[0])
657+
foo([x=]val.y()[1])
658+
");
659+
}
660+
661+
#[test]
662+
fn test_function_call_with_positional_or_keyword_parameter_redundant_subscript() {
663+
let test = inlay_hint_test(
664+
"
665+
def foo(x: int): pass
666+
x = [1]
667+
y = [2]
668+
669+
foo(x[0])
670+
foo(y[0])",
671+
);
672+
673+
assert_snapshot!(test.inlay_hints(), @r"
674+
def foo(x: int): pass
675+
x[: list[Unknown | int]] = [1]
676+
y[: list[Unknown | int]] = [2]
677+
678+
foo(x[0])
679+
foo([x=]y[0])
680+
");
681+
}
682+
488683
#[test]
489684
fn test_function_call_with_positional_only_parameter() {
490685
let test = inlay_hint_test(

0 commit comments

Comments
 (0)