mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-28 10:50:26 +00:00
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
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" />
597 lines
14 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|