[ty] fix GotoTargets for keyword args in nested function calls (#20013)

While implementing similar logic for initializers I noticed that this
code appeared to be walking the ancestors in the wrong direction, and so
if you have nested function calls it would always grab the outermost one
instead of the closest-ancestor.

The four copies of the test are because there's something really evil in
our caching that can't seem to be demonstrated in our cursor testing
framework, which I'm filing a followup for.
This commit is contained in:
Aria Desires 2025-08-21 16:19:52 -04:00 committed by GitHub
parent c68ff8d90b
commit fc5321e000
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 158 additions and 5 deletions

View file

@ -116,10 +116,10 @@ impl<'a> CoveringNode<'a> {
Ok(self)
}
/// Returns an iterator over the ancestor nodes, starting from the root
/// and ending with the covering node.
pub(crate) fn ancestors(&self) -> impl Iterator<Item = AnyNodeRef<'a>> + '_ {
self.nodes.iter().copied()
/// Returns an iterator over the ancestor nodes, starting with the node itself
/// and walking towards the root.
pub(crate) fn ancestors(&self) -> impl DoubleEndedIterator<Item = AnyNodeRef<'a>> + '_ {
self.nodes.iter().copied().rev()
}
/// Finds the index of the node that fully covers the range and

View file

@ -630,6 +630,158 @@ class MyClass: ...
");
}
/// goto-definition on a nested call using a keyword arg where both funcs have that arg name
///
/// In this case they ultimately have different signatures.
#[test]
fn goto_definition_nested_keyword_arg1() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
def my_func(ab, y, z = None): ...
def my_other_func(ab, y): ...
my_other_func(my_func(a<CURSOR>b=5, y=2), 0)
my_func(my_other_func(ab=5, y=2), 0)
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> main.py:2:13
|
2 | def my_func(ab, y, z = None): ...
| ^^
3 | def my_other_func(ab, y): ...
|
info: Source
--> main.py:5:23
|
3 | def my_other_func(ab, y): ...
4 |
5 | my_other_func(my_func(ab=5, y=2), 0)
| ^^
6 | my_func(my_other_func(ab=5, y=2), 0)
|
");
}
/// goto-definition on a nested call using a keyword arg where both funcs have that arg name
///
/// In this case they ultimately have different signatures.
#[test]
fn goto_definition_nested_keyword_arg2() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
def my_func(ab, y, z = None): ...
def my_other_func(ab, y): ...
my_other_func(my_func(ab=5, y=2), 0)
my_func(my_other_func(a<CURSOR>b=5, y=2), 0)
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> main.py:3:19
|
2 | def my_func(ab, y, z = None): ...
3 | def my_other_func(ab, y): ...
| ^^
4 |
5 | my_other_func(my_func(ab=5, y=2), 0)
|
info: Source
--> main.py:6:23
|
5 | my_other_func(my_func(ab=5, y=2), 0)
6 | my_func(my_other_func(ab=5, y=2), 0)
| ^^
|
");
}
/// goto-definition on a nested call using a keyword arg where both funcs have that arg name
///
/// In this case they have identical signatures.
#[test]
fn goto_definition_nested_keyword_arg3() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
def my_func(ab, y): ...
def my_other_func(ab, y): ...
my_other_func(my_func(a<CURSOR>b=5, y=2), 0)
my_func(my_other_func(ab=5, y=2), 0)
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> main.py:2:13
|
2 | def my_func(ab, y): ...
| ^^
3 | def my_other_func(ab, y): ...
|
info: Source
--> main.py:5:23
|
3 | def my_other_func(ab, y): ...
4 |
5 | my_other_func(my_func(ab=5, y=2), 0)
| ^^
6 | my_func(my_other_func(ab=5, y=2), 0)
|
");
}
/// goto-definition on a nested call using a keyword arg where both funcs have that arg name
///
/// In this case they have identical signatures.
#[test]
fn goto_definition_nested_keyword_arg4() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
def my_func(ab, y): ...
def my_other_func(ab, y): ...
my_other_func(my_func(ab=5, y=2), 0)
my_func(my_other_func(a<CURSOR>b=5, y=2), 0)
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> main.py:3:19
|
2 | def my_func(ab, y): ...
3 | def my_other_func(ab, y): ...
| ^^
4 |
5 | my_other_func(my_func(ab=5, y=2), 0)
|
info: Source
--> main.py:6:23
|
5 | my_other_func(my_func(ab=5, y=2), 0)
6 | my_func(my_other_func(ab=5, y=2), 0)
| ^^
|
");
}
impl CursorTest {
fn goto_definition(&self) -> String {
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)

View file

@ -14,7 +14,8 @@ pub fn selection_range(db: &dyn Db, file: File, offset: TextSize) -> Vec<TextRan
let covering = covering_node(parsed.syntax().into(), range);
let mut ranges = Vec::new();
for node in covering.ancestors() {
// Start with the largest range (the root), so iterate ancestors backwards
for node in covering.ancestors().rev() {
if should_include_in_selection(node) {
let range = node.range();
// Eliminate duplicates when parent and child nodes have the same range