ruff/crates/ty_ide/src/goto_declaration.rs
Aria Desires ec5584219e
Some checks are pending
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / python package (push) Waiting to run
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks-instrumented (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
[ty] Make initializer calls GotoTargets (#20014)
This introduces `GotoTarget::Call` that represents the kind of
ambiguous/overloaded click of a callable-being-called:

```py
x = mymodule.MyClass(1, 2)
             ^^^^^^^
```

This is equivalent to `GotoTarget::Expression` for the same span but
enriched
with information about the actual callable implementation.

That is, if you click on `MyClass` in `MyClass()` it is *both* a
reference to the class and to the initializer of the class. Therefore
it would be ideal for goto-* and docstrings to be some intelligent
merging of both the class and the initializer.

In particular the callable-implementation (initializer) is prioritized
over the callable-itself (class) so when showing docstrings we will
preferentially show the docs of the initializer if it exists, and then
fallback to the docs of the class.

For goto-definition/goto-declaration we will yield both the class and
the initializer, requiring you to pick which you want (this is perhaps
needlessly pedantic but...).

Fixes https://github.com/astral-sh/ty/issues/898
Fixes https://github.com/astral-sh/ty/issues/1010
2025-09-02 14:49:14 -04:00

1885 lines
43 KiB
Rust

use crate::goto::find_goto_target;
use crate::{Db, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::parsed_module;
use ruff_text_size::{Ranged, TextSize};
use ty_python_semantic::ImportAliasResolution;
/// Navigate to the declaration of a symbol.
///
/// A "declaration" includes both formal declarations (class statements, def statements,
/// and variable annotations) but also variable assignments. This expansive definition
/// is needed because Python doesn't require formal declarations of variables like most languages do.
pub fn goto_declaration(
db: &dyn Db,
file: File,
offset: TextSize,
) -> Option<RangedValue<NavigationTargets>> {
let module = parsed_module(db, file).load(db);
let goto_target = find_goto_target(&module, offset)?;
let declaration_targets = goto_target
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)?
.declaration_targets(db)?;
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: declaration_targets,
})
}
#[cfg(test)]
mod tests {
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
use crate::{NavigationTarget, goto_declaration};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
SubDiagnosticSeverity,
};
use ruff_db::files::FileRange;
use ruff_text_size::Ranged;
#[test]
fn goto_declaration_function_call_to_definition() {
let test = cursor_test(
"
def my_function(x, y):
return x + y
result = my_func<CURSOR>tion(1, 2)
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:5
|
2 | def my_function(x, y):
| ^^^^^^^^^^^
3 | return x + y
|
info: Source
--> main.py:5:10
|
3 | return x + y
4 |
5 | result = my_function(1, 2)
| ^^^^^^^^^^^
|
");
}
#[test]
fn goto_declaration_variable_assignment() {
let test = cursor_test(
"
x = 42
y = x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:1
|
2 | x = 42
| ^
3 | y = x
|
info: Source
--> main.py:3:5
|
2 | x = 42
3 | y = x
| ^
|
");
}
#[test]
fn goto_declaration_class_instantiation() {
let test = cursor_test(
"
class MyClass:
def __init__(self):
pass
instance = My<CURSOR>Class()
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self):
4 | pass
|
info: Source
--> main.py:6:12
|
4 | pass
5 |
6 | instance = MyClass()
| ^^^^^^^
|
info[goto-declaration]: Declaration
--> main.py:3:9
|
2 | class MyClass:
3 | def __init__(self):
| ^^^^^^^^
4 | pass
|
info: Source
--> main.py:6:12
|
4 | pass
5 |
6 | instance = MyClass()
| ^^^^^^^
|
");
}
#[test]
fn goto_declaration_parameter_usage() {
let test = cursor_test(
"
def foo(param):
return pa<CURSOR>ram * 2
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:9
|
2 | def foo(param):
| ^^^^^
3 | return param * 2
|
info: Source
--> main.py:3:12
|
2 | def foo(param):
3 | return param * 2
| ^^^^^
|
");
}
#[test]
fn goto_declaration_type_parameter() {
let test = cursor_test(
"
def generic_func[T](value: T) -> T:
v: T<CURSOR> = value
return v
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:18
|
2 | def generic_func[T](value: T) -> T:
| ^
3 | v: T = value
4 | return v
|
info: Source
--> main.py:3:8
|
2 | def generic_func[T](value: T) -> T:
3 | v: T = value
| ^
4 | return v
|
");
}
#[test]
fn goto_declaration_type_parameter_class() {
let test = cursor_test(
"
class GenericClass[T]:
def __init__(self, value: T<CURSOR>):
self.value = value
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:20
|
2 | class GenericClass[T]:
| ^
3 | def __init__(self, value: T):
4 | self.value = value
|
info: Source
--> main.py:3:31
|
2 | class GenericClass[T]:
3 | def __init__(self, value: T):
| ^
4 | self.value = value
|
");
}
#[test]
fn goto_declaration_nested_scope_variable() {
let test = cursor_test(
"
x = \"outer\"
def outer_func():
def inner_func():
return x<CURSOR> # Should find outer x
return inner_func
",
);
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:2:1
|
2 | x = "outer"
| ^
3 | def outer_func():
4 | def inner_func():
|
info: Source
--> main.py:5:16
|
3 | def outer_func():
4 | def inner_func():
5 | return x # Should find outer x
| ^
6 | return inner_func
|
"#);
}
#[test]
fn goto_declaration_class_scope_skipped() {
let test = cursor_test(
r#"
class A:
x = 1
def method(self):
def inner():
return <CURSOR>x # Should NOT find class variable x
return inner
"#,
);
// Should not find the class variable 'x' due to Python's scoping rules
assert_snapshot!(test.goto_declaration(), @"No goto target found");
}
#[test]
fn goto_declaration_import_simple() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule
print(mymod<CURSOR>ule.function())
",
)
.source(
"mymodule.py",
r#"
def function():
return "hello from mymodule"
variable = 42
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mymodule.py:1:1
|
1 |
| ^
2 | def function():
3 | return "hello from mymodule"
|
info: Source
--> main.py:3:7
|
2 | import mymodule
3 | print(mymodule.function())
| ^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_import_from() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import my_function
print(my_func<CURSOR>tion())
",
)
.source(
"mymodule.py",
r#"
def my_function():
return "hello"
def other_function():
return "other"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mymodule.py:2:5
|
2 | def my_function():
| ^^^^^^^^^^^
3 | return "hello"
|
info: Source
--> main.py:3:7
|
2 | from mymodule import my_function
3 | print(my_function())
| ^^^^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_import_as() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule.submodule as sub
print(<CURSOR>sub.helper())
",
)
.source(
"mymodule/__init__.py",
"
# Main module init
",
)
.source(
"mymodule/submodule.py",
r#"
FOO = 0
"#,
)
.build();
// Should find the submodule file itself
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mymodule/submodule.py:1:1
|
1 |
| ^
2 | FOO = 0
|
info: Source
--> main.py:3:7
|
2 | import mymodule.submodule as sub
3 | print(sub.helper())
| ^^^
|
"#);
}
#[test]
fn goto_declaration_from_import_as() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from utils import func as h
print(<CURSOR>h("test"))
"#,
)
.source(
"utils.py",
r#"
def func(arg):
return f"Processed: {arg}"
"#,
)
.build();
// Should resolve to the actual function definition, not the import statement
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> utils.py:2:5
|
2 | def func(arg):
| ^^^^
3 | return f"Processed: {arg}"
|
info: Source
--> main.py:3:7
|
2 | from utils import func as h
3 | print(h("test"))
| ^
|
"#);
}
#[test]
fn goto_declaration_from_import_chain() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from intermediate import shared_function
print(shared_func<CURSOR>tion())
"#,
)
.source(
"intermediate.py",
r#"
# Re-export the function from the original module
from original import shared_function
"#,
)
.source(
"original.py",
r#"
def shared_function():
return "from original"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> original.py:2:5
|
2 | def shared_function():
| ^^^^^^^^^^^^^^^
3 | return "from original"
|
info: Source
--> main.py:3:7
|
2 | from intermediate import shared_function
3 | print(shared_function())
| ^^^^^^^^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_from_star_import() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from math_utils import *
result = add_n<CURSOR>umbers(5, 3)
"#,
)
.source(
"math_utils.py",
r#"
def add_numbers(a, b):
"""Add two numbers together."""
return a + b
def multiply_numbers(a, b):
"""Multiply two numbers together."""
return a * b
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> math_utils.py:2:5
|
2 | def add_numbers(a, b):
| ^^^^^^^^^^^
3 | """Add two numbers together."""
4 | return a + b
|
info: Source
--> main.py:3:10
|
2 | from math_utils import *
3 | result = add_numbers(5, 3)
| ^^^^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_relative_import() {
let test = CursorTest::builder()
.source(
"package/main.py",
r#"
from .utils import helper_function
result = helper_func<CURSOR>tion("test")
"#,
)
.source(
"package/__init__.py",
r#"
# Package init file
"#,
)
.source(
"package/utils.py",
r#"
def helper_function(arg):
"""A helper function in utils module."""
return f"Processed: {arg}"
def another_helper():
"""Another helper function."""
pass
"#,
)
.build();
// Should resolve the relative import to find the actual function definition
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> package/utils.py:2:5
|
2 | def helper_function(arg):
| ^^^^^^^^^^^^^^^
3 | """A helper function in utils module."""
4 | return f"Processed: {arg}"
|
info: Source
--> package/main.py:3:10
|
2 | from .utils import helper_function
3 | result = helper_function("test")
| ^^^^^^^^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_relative_star_import() {
let test = CursorTest::builder()
.source(
"package/main.py",
r#"
from .utils import *
result = helper_func<CURSOR>tion("test")
"#,
)
.source(
"package/__init__.py",
r#"
# Package init file
"#,
)
.source(
"package/utils.py",
r#"
def helper_function(arg):
"""A helper function in utils module."""
return f"Processed: {arg}"
def another_helper():
"""Another helper function."""
pass
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> package/utils.py:2:5
|
2 | def helper_function(arg):
| ^^^^^^^^^^^^^^^
3 | """A helper function in utils module."""
4 | return f"Processed: {arg}"
|
info: Source
--> package/main.py:3:10
|
2 | from .utils import *
3 | result = helper_function("test")
| ^^^^^^^^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_import_as_alias_name() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule.submodule as su<CURSOR>b
print(sub.helper())
",
)
.source(
"mymodule/__init__.py",
"
# Main module init
",
)
.source(
"mymodule/submodule.py",
r#"
FOO = 0
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule/submodule.py:1:1
|
1 |
| ^
2 | FOO = 0
|
info: Source
--> main.py:2:30
|
2 | import mymodule.submodule as sub
| ^^^
3 | print(sub.helper())
|
");
}
#[test]
fn goto_declaration_import_as_alias_name_on_module() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule.submod<CURSOR>ule as sub
print(sub.helper())
",
)
.source(
"mymodule/__init__.py",
"
# Main module init
",
)
.source(
"mymodule/submodule.py",
r#"
FOO = 0
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule/submodule.py:1:1
|
1 |
| ^
2 | FOO = 0
|
info: Source
--> main.py:2:17
|
2 | import mymodule.submodule as sub
| ^^^^^^^^^
3 | print(sub.helper())
|
");
}
#[test]
fn goto_declaration_from_import_symbol_original() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mypackage.utils import hel<CURSOR>per as h
result = h("/a", "/b")
"#,
)
.source(
"mypackage/__init__.py",
r#"
# Package init
"#,
)
.source(
"mypackage/utils.py",
r#"
def helper(a, b):
return a + "/" + b
def another_helper(path):
return "processed"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mypackage/utils.py:2:5
|
2 | def helper(a, b):
| ^^^^^^
3 | return a + "/" + b
|
info: Source
--> main.py:2:29
|
2 | from mypackage.utils import helper as h
| ^^^^^^
3 | result = h("/a", "/b")
|
"#);
}
#[test]
fn goto_declaration_from_import_symbol_alias() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mypackage.utils import helper as h<CURSOR>
result = h("/a", "/b")
"#,
)
.source(
"mypackage/__init__.py",
r#"
# Package init
"#,
)
.source(
"mypackage/utils.py",
r#"
def helper(a, b):
return a + "/" + b
def another_helper(path):
return "processed"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mypackage/utils.py:2:5
|
2 | def helper(a, b):
| ^^^^^^
3 | return a + "/" + b
|
info: Source
--> main.py:2:39
|
2 | from mypackage.utils import helper as h
| ^
3 | result = h("/a", "/b")
|
"#);
}
#[test]
fn goto_declaration_from_import_module() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mypackage.ut<CURSOR>ils import helper as h
result = h("/a", "/b")
"#,
)
.source(
"mypackage/__init__.py",
r#"
# Package init
"#,
)
.source(
"mypackage/utils.py",
r#"
def helper(a, b):
return a + "/" + b
def another_helper(path):
return "processed"
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mypackage/utils.py:1:1
|
1 |
| ^
2 | def helper(a, b):
3 | return a + "/" + b
|
info: Source
--> main.py:2:16
|
2 | from mypackage.utils import helper as h
| ^^^^^
3 | result = h("/a", "/b")
|
"#);
}
#[test]
fn goto_declaration_instance_attribute() {
let test = cursor_test(
"
class C:
def __init__(self):
self.x: int = 1
c = C()
y = c.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:4:9
|
2 | class C:
3 | def __init__(self):
4 | self.x: int = 1
| ^^^^^^
5 |
6 | c = C()
|
info: Source
--> main.py:7:7
|
6 | c = C()
7 | y = c.x
| ^
|
");
}
#[test]
fn goto_declaration_nested_instance_attribute() {
let test = cursor_test(
"
class C:
def __init__(self):
self.x: int = 1
class D:
def __init__(self):
self.y: C = C()
d = D()
y = d.y.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:4:9
|
2 | class C:
3 | def __init__(self):
4 | self.x: int = 1
| ^^^^^^
5 |
6 | class D:
|
info: Source
--> main.py:11:9
|
10 | d = D()
11 | y = d.y.x
| ^
|
");
}
#[test]
fn goto_declaration_instance_attribute_no_annotation() {
let test = cursor_test(
"
class C:
def __init__(self):
self.x = 1
c = C()
y = c.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:4:9
|
2 | class C:
3 | def __init__(self):
4 | self.x = 1
| ^^^^^^
5 |
6 | c = C()
|
info: Source
--> main.py:7:7
|
6 | c = C()
7 | y = c.x
| ^
|
");
}
#[test]
fn goto_declaration_method_call_to_definition() {
let test = cursor_test(
"
class C:
def foo(self):
return 42
c = C()
res = c.foo<CURSOR>()
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:9
|
2 | class C:
3 | def foo(self):
| ^^^
4 | return 42
|
info: Source
--> main.py:7:9
|
6 | c = C()
7 | res = c.foo()
| ^^^
|
");
}
#[test]
fn goto_declaration_module_attribute() {
let test = cursor_test(
r#"
x: i<CURSOR>nt = 42
"#,
);
// Test that we can navigate to builtin types, but don't snapshot the exact content
// since typeshed stubs can change frequently
let result = test.goto_declaration();
// Should not be "No goto target found" - we should find the builtin int type
assert!(
!result.contains("No goto target found"),
"Should find builtin int type"
);
assert!(
!result.contains("No declarations found"),
"Should find builtin int declarations"
);
// Should navigate to a stdlib file containing the int class
assert!(
result.contains("builtins.pyi"),
"Should navigate to builtins.pyi"
);
assert!(
result.contains("class int:"),
"Should find the int class definition"
);
assert!(
result.contains("info[goto-declaration]: Declaration"),
"Should be a goto-declaration result"
);
}
#[test]
fn goto_declaration_nonlocal_binding() {
let test = cursor_test(
r#"
def outer():
x = "outer_value"
def inner():
nonlocal x
x = "modified"
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
return inner
"#,
);
// Should find the variable declaration in the outer scope, not the nonlocal statement
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:3:5
|
2 | def outer():
3 | x = "outer_value"
| ^
4 |
5 | def inner():
|
info: Source
--> main.py:8:16
|
6 | nonlocal x
7 | x = "modified"
8 | return x # Should find the nonlocal x declaration in outer scope
| ^
9 |
10 | return inner
|
"#);
}
#[test]
fn goto_declaration_global_binding() {
let test = cursor_test(
r#"
global_var = "global_value"
def function():
global global_var
global_var = "modified"
return global_<CURSOR>var # Should find the global variable declaration
"#,
);
// Should find the global variable declaration, not the global statement
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:2:1
|
2 | global_var = "global_value"
| ^^^^^^^^^^
3 |
4 | def function():
|
info: Source
--> main.py:7:12
|
5 | global global_var
6 | global_var = "modified"
7 | return global_var # Should find the global variable declaration
| ^^^^^^^^^^
|
"#);
}
#[test]
fn goto_declaration_inherited_attribute() {
let test = cursor_test(
"
class A:
x = 10
class B(A):
pass
b = B()
y = b.x<CURSOR>
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:5
|
2 | class A:
3 | x = 10
| ^
4 |
5 | class B(A):
|
info: Source
--> main.py:9:7
|
8 | b = B()
9 | y = b.x
| ^
|
");
}
#[test]
fn goto_declaration_property_getter_setter() {
let test = cursor_test(
"
class C:
def __init__(self):
self._value = 0
@property
def value(self):
return self._value
c = C()
c.value<CURSOR> = 42
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:7:9
|
6 | @property
7 | def value(self):
| ^^^^^
8 | return self._value
|
info: Source
--> main.py:11:3
|
10 | c = C()
11 | c.value = 42
| ^^^^^
|
");
}
#[test]
fn goto_declaration_function_doc_attribute() {
let test = cursor_test(
r#"
def my_function():
"""This is a docstring."""
return 42
doc = my_function.__doc<CURSOR>__
"#,
);
// Should navigate to the __doc__ property in the FunctionType class in typeshed
let result = test.goto_declaration();
assert!(
!result.contains("No goto target found"),
"Should find builtin __doc__ attribute"
);
assert!(
!result.contains("No declarations found"),
"Should find builtin __doc__ declarations"
);
// Should navigate to a typeshed file containing the __doc__ attribute
assert!(
result.contains("types.pyi") || result.contains("builtins.pyi"),
"Should navigate to typeshed file with __doc__ definition"
);
assert!(
result.contains("__doc__"),
"Should find the __doc__ attribute definition"
);
assert!(
result.contains("info[goto-declaration]: Declaration"),
"Should be a goto-declaration result"
);
}
#[test]
fn goto_declaration_protocol_instance_attribute() {
let test = cursor_test(
"
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> None: ...
name: str
def use_drawable(obj: Drawable):
obj.na<CURSOR>me
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:6:5
|
4 | class Drawable(Protocol):
5 | def draw(self) -> None: ...
6 | name: str
| ^^^^
7 |
8 | def use_drawable(obj: Drawable):
|
info: Source
--> main.py:9:9
|
8 | def use_drawable(obj: Drawable):
9 | obj.name
| ^^^^
|
");
}
#[test]
fn goto_declaration_generic_method_class_type() {
let test = cursor_test(
r#"
class MyClass:
ClassType = int
def generic_method[T](self, value: Class<CURSOR>Type) -> T:
return value
"#,
);
// Should find the ClassType defined in the class body, not fail to resolve
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:3:5
|
2 | class MyClass:
3 | ClassType = int
| ^^^^^^^^^
4 |
5 | def generic_method[T](self, value: ClassType) -> T:
|
info: Source
--> main.py:5:40
|
3 | ClassType = int
4 |
5 | def generic_method[T](self, value: ClassType) -> T:
| ^^^^^^^^^
6 | return value
|
");
}
#[test]
fn goto_declaration_keyword_argument_simple() {
let test = cursor_test(
"
def my_function(x, y, z=10):
return x + y + z
result = my_function(1, y<CURSOR>=2, z=3)
",
);
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> main.py:2:20
|
2 | def my_function(x, y, z=10):
| ^
3 | return x + y + z
|
info: Source
--> main.py:5:25
|
3 | return x + y + z
4 |
5 | result = my_function(1, y=2, z=3)
| ^
|
");
}
#[test]
fn goto_declaration_keyword_argument_overloaded() {
let test = cursor_test(
r#"
from typing import overload
@overload
def process(data: str, format: str) -> str: ...
@overload
def process(data: int, format: int) -> int: ...
def process(data, format):
return data
# Call the overloaded function
result = process("hello", format<CURSOR>="json")
"#,
);
// Should navigate to the parameter in both matching overloads
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> main.py:5:24
|
4 | @overload
5 | def process(data: str, format: str) -> str: ...
| ^^^^^^
6 |
7 | @overload
|
info: Source
--> main.py:14:27
|
13 | # Call the overloaded function
14 | result = process("hello", format="json")
| ^^^^^^
|
info[goto-declaration]: Declaration
--> main.py:8:24
|
7 | @overload
8 | def process(data: int, format: int) -> int: ...
| ^^^^^^
9 |
10 | def process(data, format):
|
info: Source
--> main.py:14:27
|
13 | # Call the overloaded function
14 | result = process("hello", format="json")
| ^^^^^^
|
"#);
}
#[test]
fn goto_declaration_overload_type_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: str): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: str): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_type_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
r#"
from mymodule import ab
a<CURSOR>b("hello")
"#,
)
.source(
"mymodule.py",
r#"
def ab(a):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: str): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r#"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab("hello")
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: str): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab("hello")
| ^^
|
"#);
}
#[test]
fn goto_declaration_overload_arity_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, 2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int): ...
@overload
def ab(a: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int, b: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, 2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, 2)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_arity_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1)
",
)
.source(
"mymodule.py",
r#"
def ab(a, b = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int, b: int): ...
@overload
def ab(a: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int, b: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_keyword_disambiguated1() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, b=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: int, *, b: int): ...
@overload
def ab(a: int, *, c: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int, *, b: int): ...
| ^^
9 |
10 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:11:5
|
10 | @overload
11 | def ab(a: int, *, c: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, b=2)
| ^^
|
");
}
#[test]
fn goto_declaration_overload_keyword_disambiguated2() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import ab
a<CURSOR>b(1, c=2)
",
)
.source(
"mymodule.py",
r#"
def ab(a, *, b = None, c = None):
"""the real implementation!"""
"#,
)
.source(
"mymodule.pyi",
r#"
from typing import overload
@overload
def ab(a: int): ...
@overload
def ab(a: int, *, b: int): ...
@overload
def ab(a: int, *, c: int): ...
"#,
)
.build();
assert_snapshot!(test.goto_declaration(), @r"
info[goto-declaration]: Declaration
--> mymodule.pyi:5:5
|
4 | @overload
5 | def ab(a: int): ...
| ^^
6 |
7 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:8:5
|
7 | @overload
8 | def ab(a: int, *, b: int): ...
| ^^
9 |
10 | @overload
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
info[goto-declaration]: Declaration
--> mymodule.pyi:11:5
|
10 | @overload
11 | def ab(a: int, *, c: int): ...
| ^^
|
info: Source
--> main.py:4:1
|
2 | from mymodule import ab
3 |
4 | ab(1, c=2)
| ^^
|
");
}
impl CursorTest {
fn goto_declaration(&self) -> String {
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)
else {
return "No goto target found".to_string();
};
if targets.is_empty() {
return "No declarations found".to_string();
}
let source = targets.range;
self.render_diagnostics(
targets
.into_iter()
.map(|target| GotoDeclarationDiagnostic::new(source, &target)),
)
}
}
struct GotoDeclarationDiagnostic {
source: FileRange,
target: FileRange,
}
impl GotoDeclarationDiagnostic {
fn new(source: FileRange, target: &NavigationTarget) -> Self {
Self {
source,
target: FileRange::new(target.file(), target.focus_range()),
}
}
}
impl IntoDiagnostic for GotoDeclarationDiagnostic {
fn into_diagnostic(self) -> Diagnostic {
let mut source = SubDiagnostic::new(SubDiagnosticSeverity::Info, "Source");
source.annotate(Annotation::primary(
Span::from(self.source.file()).with_range(self.source.range()),
));
let mut main = Diagnostic::new(
DiagnosticId::Lint(LintName::of("goto-declaration")),
Severity::Info,
"Declaration".to_string(),
);
main.annotate(Annotation::primary(
Span::from(self.target.file()).with_range(self.target.range()),
));
main.sub(source);
main
}
}
}