ruff/crates/ty_ide/src/goto_definition.rs
Aria Desires d59282ebb5
Some checks are pending
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 / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
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 / python package (push) Waiting to run
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] render docstrings in hover (#19882)
This PR has several components:

* Introduce a Docstring String wrapper type that has render_plaintext
and render_markdown methods, to force docstring handlers to pick a
rendering format
* Implement [PEP-257](https://peps.python.org/pep-0257/) docstring
trimming for it
* The markdown rendering just renders the content in a plaintext
codeblock for now (followup work)
* Introduce a `DefinitionsOrTargets` type representing the partial
evaluation of `GotoTarget::get_definition_targets` to ideally stop at
getting `ResolvedDefinitions`
* Add `declaration_targets`, `definition_targets`, and `docstring`
methods to `DefinitionsOrTargets` for the 3 usecases we have for this
operation
* `docstring` is of course the key addition here, it uses the same basic
logic that `signature_help` was using: first check the goto-declaration
for docstrings, then check the goto-definition for docstrings.
* Refactor `signature_help` to use the new APIs instead of implementing
it itself
* Not fixed in this PR: an issue I found where `signature_help` will
erroneously cache docs between functions that have the same type (hover
docs don't have this bug)
* A handful of new tests and additions to tests to add docstrings in
various places and see which get caught


Examples of it working with stdlib, third party, and local definitions:
<img width="597" height="120" alt="Screenshot 2025-08-12 at 2 13 55 PM"
src="https://github.com/user-attachments/assets/eae54efd-882e-4b50-b5b4-721595224232"
/>
<img width="598" height="281" alt="Screenshot 2025-08-12 at 2 14 06 PM"
src="https://github.com/user-attachments/assets/5c9740d5-a06b-4c22-9349-da6eb9a9ba5a"
/>
<img width="327" height="180" alt="Screenshot 2025-08-12 at 2 14 18 PM"
src="https://github.com/user-attachments/assets/3b5647b9-2cdd-4c5b-bb7d-da23bff1bcb5"
/>

Notably modules don't work yet (followup work):
<img width="224" height="83" alt="Screenshot 2025-08-12 at 2 14 37 PM"
src="https://github.com/user-attachments/assets/7e9dcb70-a10e-46d9-a85c-9fe52c3b7e7b"
/>

Notably we don't show docs for an item if you hover its actual
definition (followup work, but also, not the most important):
<img width="324" height="69" alt="Screenshot 2025-08-12 at 2 16 54 PM"
src="https://github.com/user-attachments/assets/d4ddcdd8-c3fc-4120-ac93-cefdf57933b4"
/>
2025-08-13 14:59:20 +00:00

597 lines
14 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 definition of a symbol.
///
/// A "definition" is the actual implementation of a symbol, potentially in a source file
/// rather than a stub file. This differs from "declaration" which may navigate to stub files.
/// When possible, this function will map from stub file declarations to their corresponding
/// source file implementations using the `StubMapper`.
pub fn goto_definition(
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 definition_targets = goto_target
.get_definition_targets(file, db, ImportAliasResolution::ResolveAliases)?
.definition_targets(db)?;
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: definition_targets,
})
}
#[cfg(test)]
mod test {
use crate::tests::{CursorTest, IntoDiagnostic};
use crate::{NavigationTarget, goto_definition};
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;
/// goto-definition on a module should go to the .py not the .pyi
///
/// TODO: this currently doesn't work right! This is especially surprising
/// because [`goto_definition_stub_map_module_ref`] works fine.
#[test]
fn goto_definition_stub_map_module_import() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymo<CURSOR>dule import my_function
",
)
.source(
"mymodule.py",
r#"
def my_function():
return "hello"
"#,
)
.source(
"mymodule.pyi",
r#"
def my_function(): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:1:1
|
1 |
| ^
2 | def my_function():
3 | return "hello"
|
info: Source
--> main.py:2:6
|
2 | from mymodule import my_function
| ^^^^^^^^
|
"#);
}
/// goto-definition on a module ref should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_module_ref() {
let test = CursorTest::builder()
.source(
"main.py",
"
import mymodule
x = mymo<CURSOR>dule
",
)
.source(
"mymodule.py",
r#"
def my_function():
return "hello"
"#,
)
.source(
"mymodule.pyi",
r#"
def my_function(): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:1:1
|
1 |
| ^
2 | def my_function():
3 | return "hello"
|
info: Source
--> main.py:3:5
|
2 | import mymodule
3 | x = mymodule
| ^^^^^^^^
|
"#);
}
/// goto-definition on a function call should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_function() {
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"
"#,
)
.source(
"mymodule.pyi",
r#"
def my_function(): ...
def other_function(): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> 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())
| ^^^^^^^^^^^
|
"#);
}
/// goto-definition on a function that's redefined many times in the impl .py
///
/// Currently this yields all instances. There's an argument for only yielding
/// the final one since that's the one "exported" but, this is consistent for
/// how we do file-local goto-definition.
#[test]
fn goto_definition_stub_map_function_redefine() {
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 my_function():
return "hello again"
def my_function():
return "we can't keep doing this"
def other_function():
return "other"
"#,
)
.source(
"mymodule.pyi",
r#"
def my_function(): ...
def other_function(): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> 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())
| ^^^^^^^^^^^
|
info[goto-definition]: Definition
--> mymodule.py:5:5
|
3 | return "hello"
4 |
5 | def my_function():
| ^^^^^^^^^^^
6 | return "hello again"
|
info: Source
--> main.py:3:7
|
2 | from mymodule import my_function
3 | print(my_function())
| ^^^^^^^^^^^
|
info[goto-definition]: Definition
--> mymodule.py:8:5
|
6 | return "hello again"
7 |
8 | def my_function():
| ^^^^^^^^^^^
9 | return "we can't keep doing this"
|
info: Source
--> main.py:3:7
|
2 | from mymodule import my_function
3 | print(my_function())
| ^^^^^^^^^^^
|
"#);
}
/// goto-definition on a class ref go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_ref() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import MyClass
x = MyC<CURSOR>lass
",
)
.source(
"mymodule.py",
r#"
class MyClass:
def __init__(self, val):
self.val = val
class MyOtherClass:
def __init__(self, val):
self.val = val + 1
"#,
)
.source(
"mymodule.pyi",
r#"
class MyClass:
def __init__(self, val: bool): ...
class MyOtherClass:
def __init__(self, val: bool): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> mymodule.py:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self, val):
4 | self.val = val
|
info: Source
--> main.py:3:5
|
2 | from mymodule import MyClass
3 | x = MyClass
| ^^^^^^^
|
");
}
/// goto-definition on a class init should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_init() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import MyClass
x = MyCl<CURSOR>ass(0)
",
)
.source(
"mymodule.py",
r#"
class MyClass:
def __init__(self, val):
self.val = val
class MyOtherClass:
def __init__(self, val):
self.val = val + 1
"#,
)
.source(
"mymodule.pyi",
r#"
class MyClass:
def __init__(self, val: bool): ...
class MyOtherClass:
def __init__(self, val: bool): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> mymodule.py:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self, val):
4 | self.val = val
|
info: Source
--> main.py:3:5
|
2 | from mymodule import MyClass
3 | x = MyClass(0)
| ^^^^^^^
|
");
}
/// goto-definition on a class method should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_method() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import MyClass
x = MyClass(0)
x.act<CURSOR>ion()
",
)
.source(
"mymodule.py",
r#"
class MyClass:
def __init__(self, val):
self.val = val
def action(self):
print(self.val)
class MyOtherClass:
def __init__(self, val):
self.val = val + 1
"#,
)
.source(
"mymodule.pyi",
r#"
class MyClass:
def __init__(self, val: bool): ...
def action(self): ...
class MyOtherClass:
def __init__(self, val: bool): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> mymodule.py:5:9
|
3 | def __init__(self, val):
4 | self.val = val
5 | def action(self):
| ^^^^^^
6 | print(self.val)
|
info: Source
--> main.py:4:3
|
2 | from mymodule import MyClass
3 | x = MyClass(0)
4 | x.action()
| ^^^^^^
|
");
}
/// goto-definition on a class function should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_function() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import MyClass
x = MyClass.act<CURSOR>ion()
",
)
.source(
"mymodule.py",
r#"
class MyClass:
def __init__(self, val):
self.val = val
def action():
print("hi!")
class MyOtherClass:
def __init__(self, val):
self.val = val + 1
"#,
)
.source(
"mymodule.pyi",
r#"
class MyClass:
def __init__(self, val: bool): ...
def action(): ...
class MyOtherClass:
def __init__(self, val: bool): ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r#"
info[goto-definition]: Definition
--> mymodule.py:5:9
|
3 | def __init__(self, val):
4 | self.val = val
5 | def action():
| ^^^^^^
6 | print("hi!")
|
info: Source
--> main.py:3:13
|
2 | from mymodule import MyClass
3 | x = MyClass.action()
| ^^^^^^
|
"#);
}
/// goto-definition on a class import should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_import() {
let test = CursorTest::builder()
.source(
"main.py",
"
from mymodule import MyC<CURSOR>lass
",
)
.source(
"mymodule.py",
r#"
class MyClass: ...
"#,
)
.source(
"mymodule.pyi",
r#"
class MyClass: ...
"#,
)
.build();
assert_snapshot!(test.goto_definition(), @r"
info[goto-definition]: Definition
--> mymodule.py:2:7
|
2 | class MyClass: ...
| ^^^^^^^
|
info: Source
--> main.py:2:22
|
2 | from mymodule import MyClass
| ^^^^^^^
|
");
}
impl CursorTest {
fn goto_definition(&self) -> String {
let Some(targets) = goto_definition(&self.db, self.cursor.file, self.cursor.offset)
else {
return "No goto target found".to_string();
};
if targets.is_empty() {
return "No definitions found".to_string();
}
let source = targets.range;
self.render_diagnostics(
targets
.into_iter()
.map(|target| GotoDefinitionDiagnostic::new(source, &target)),
)
}
}
struct GotoDefinitionDiagnostic {
source: FileRange,
target: FileRange,
}
impl GotoDefinitionDiagnostic {
fn new(source: FileRange, target: &NavigationTarget) -> Self {
Self {
source,
target: FileRange::new(target.file(), target.focus_range()),
}
}
}
impl IntoDiagnostic for GotoDefinitionDiagnostic {
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-definition")),
Severity::Info,
"Definition".to_string(),
);
main.annotate(Annotation::primary(
Span::from(self.target.file()).with_range(self.target.range()),
));
main.sub(source);
main
}
}
}