Compare commits

...

4 Commits

Author SHA1 Message Date
Aria Desires
97af9d8466 fmt 2025-07-07 12:46:33 -04:00
Aria Desires
5cc6762c23 fixup 2025-07-03 19:20:47 -04:00
Aria Desires
1bee527cb2 add many tests 2025-07-03 19:20:47 -04:00
Aria Desires
d7b7b835e1 Add initial implementation of goto definition for loads of local names 2025-07-03 19:20:44 -04:00
8 changed files with 695 additions and 7 deletions

View File

@@ -1,12 +1,13 @@
use crate::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
use crate::{Db, HasNavigationTargets, NavigationTarget, NavigationTargets, RangedValue};
use ruff_db::files::{File, FileRange};
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::Type;
use ty_python_semantic::{HasType, SemanticModel};
use ty_python_semantic::{HasDefinition, HasType, SemanticModel};
pub fn goto_type_definition(
db: &dyn Db,
@@ -29,6 +30,35 @@ pub fn goto_type_definition(
})
}
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 model = SemanticModel::new(db, file);
let definitions = goto_target.definitions(&model)?;
tracing::debug!("Definitions of covering node is found");
let targets = definitions.into_iter().map(|definition| {
let full_range = definition.full_range(db, &module);
NavigationTarget {
file: full_range.file(),
focus_range: definition.focus_range(db, &module).range(),
full_range: full_range.range(),
}
});
Some(RangedValue {
range: FileRange::new(file, goto_target.range()),
value: NavigationTargets::unique(targets),
})
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum GotoTarget<'a> {
Expression(ast::ExprRef<'a>),
@@ -154,6 +184,16 @@ impl GotoTarget<'_> {
Some(ty)
}
pub(crate) fn definitions<'db>(
self,
model: &SemanticModel<'db>,
) -> Option<Vec<Definition<'db>>> {
match self {
GotoTarget::Expression(expr_ref) => expr_ref.definitions(model),
_ => None,
}
}
}
impl Ranged for GotoTarget<'_> {
@@ -254,7 +294,7 @@ pub(crate) fn find_goto_target(
#[cfg(test)]
mod tests {
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
use crate::{NavigationTarget, goto_type_definition};
use crate::{NavigationTarget, goto_definition, goto_type_definition};
use insta::assert_snapshot;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
@@ -828,6 +868,516 @@ f(**kwargs<CURSOR>)
");
}
#[test]
fn goto_def_function_call() {
let test = cursor_test(
r#"
def ab(a, b): ...
a<CURSOR>b(1, 2)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:17
|
2 | def ab(a, b): ...
| ^^
3 |
4 | ab(1, 2)
|
info: Source
--> main.py:4:13
|
2 | def ab(a, b): ...
3 |
4 | ab(1, 2)
| ^^
|
");
}
#[test]
fn goto_def_local_load() {
let test = cursor_test(
r#"
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | ab = 1
| ^^
3 | print(ab)
|
info: Source
--> main.py:3:19
|
2 | ab = 1
3 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_rebind() {
let test = cursor_test(
r#"
ab = 1
ab = 2
ab = 3
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:4:13
|
2 | ab = 1
3 | ab = 2
4 | ab = 3
| ^^
5 | print(ab)
|
info: Source
--> main.py:5:19
|
3 | ab = 2
4 | ab = 3
5 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_cond_rebind() {
let test = cursor_test(
r#"
ab = 1
if cond:
ab = 2
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:13
|
2 | ab = 1
| ^^
3 | if cond:
4 | ab = 2
|
info: Source
--> main.py:5:19
|
3 | if cond:
4 | ab = 2
5 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:4:17
|
2 | ab = 1
3 | if cond:
4 | ab = 2
| ^^
5 | print(ab)
|
info: Source
--> main.py:5:19
|
3 | if cond:
4 | ab = 2
5 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_exhaustive_bind() {
let test = cursor_test(
r#"
if cond:
ab = 2
else:
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:3:17
|
2 | if cond:
3 | ab = 2
| ^^
4 | else:
5 | ab = 1
|
info: Source
--> main.py:6:19
|
4 | else:
5 | ab = 1
6 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:5:17
|
3 | ab = 2
4 | else:
5 | ab = 1
| ^^
6 | print(ab)
|
info: Source
--> main.py:6:19
|
4 | else:
5 | ab = 1
6 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_only_decl() {
let test = cursor_test(
r#"
ab: int
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @"No definitions found");
}
#[test]
fn goto_def_local_load_exhaustive_bind_decl() {
let test = cursor_test(
r#"
ab: int
if cond:
ab = 2
else:
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:4:17
|
2 | ab: int
3 | if cond:
4 | ab = 2
| ^^
5 | else:
6 | ab = 1
|
info: Source
--> main.py:7:19
|
5 | else:
6 | ab = 1
7 | print(ab)
| ^^
|
info[goto-type-definition]: Type definition
--> main.py:6:17
|
4 | ab = 2
5 | else:
6 | ab = 1
| ^^
7 | print(ab)
|
info: Source
--> main.py:7:19
|
5 | else:
6 | ab = 1
7 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_load_bind_decl() {
let test = cursor_test(
r#"
ab: int
ab = 1
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:3:13
|
2 | ab: int
3 | ab = 1
| ^^
4 | print(ab)
|
info: Source
--> main.py:4:19
|
2 | ab: int
3 | ab = 1
4 | print(ab)
| ^^
|
");
}
#[test]
fn goto_def_local_first_store() {
let test = cursor_test(
r#"
a<CURSOR>b = 1
print(ab)
ab = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_local_second_store() {
let test = cursor_test(
r#"
ab = 1
print(ab)
a<CURSOR>b = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_local_loadstore() {
let test = cursor_test(
r#"
ab = 1
print(ab)
a<CURSOR>b += 2
print(ab)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
x = A<CURSOR>B(5)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:2:19
|
2 | class AB:
| ^^
3 | def __init__(self, val: int):
4 | self.myval = val
|
info: Source
--> main.py:6:17
|
4 | self.myval = val
5 |
6 | x = AB(5)
| ^^
|
");
}
#[test]
fn goto_def_class_implicit_instance_variable() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
x = AB(5)
print(x.my<CURSOR>val)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class_explicit_instance_variable() {
let test = cursor_test(
r#"
class AB:
myval: int
def __init__(self, val: int):
self.myval = val
x = AB(5)
print(x.my<CURSOR>val)
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_path_parent() {
let test = cursor_test(
r#"
class AB:
def __init__(self, val: int):
self.myval = val
xyz = AB(5)
print(x<CURSOR>yz.myval)
"#,
);
assert_snapshot!(test.goto_definition(), @r"
info[goto-type-definition]: Type definition
--> main.py:6:13
|
4 | self.myval = val
5 |
6 | xyz = AB(5)
| ^^^
7 | print(xyz.myval)
|
info: Source
--> main.py:7:19
|
6 | xyz = AB(5)
7 | print(xyz.myval)
| ^^^
|
");
}
#[test]
fn goto_def_class_class_variable() {
let test = cursor_test(
r#"
class AB:
RED = "red"
BLUE = "blue"
x = AB.RE<CURSOR>D
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_class_path_parent() {
let test = cursor_test(
r#"
class AB:
RED = "red"
BLUE = "blue"
x = A<CURSOR>B.RED
"#,
);
assert_snapshot!(test.goto_definition(), @r#"
info[goto-type-definition]: Type definition
--> main.py:2:19
|
2 | class AB:
| ^^
3 | RED = "red"
4 | BLUE = "blue"
|
info: Source
--> main.py:6:17
|
4 | BLUE = "blue"
5 |
6 | x = AB.RED
| ^^
|
"#);
}
#[test]
fn goto_def_global_decl() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global a<CURSOR>b
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
#[test]
fn goto_def_global_load() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global ab
print(a<CURSOR>b)
"#,
);
assert_snapshot!(test.goto_definition(), @"No definitions found");
}
#[test]
fn goto_def_global_store() {
let test = cursor_test(
r#"
ab = 1
def myfunc():
global ab
a<CURSOR>b = 2
"#,
);
assert_snapshot!(test.goto_definition(), @"No goto target found");
}
impl CursorTest {
fn goto_type_definition(&self) -> String {
let Some(targets) =
@@ -847,6 +1397,24 @@ f(**kwargs<CURSOR>)
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
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| GotoTypeDefinitionDiagnostic::new(source, &target)),
)
}
}
struct GotoTypeDefinitionDiagnostic {

View File

@@ -8,7 +8,7 @@ mod markup;
pub use completion::completion;
pub use db::Db;
pub use goto::goto_type_definition;
pub use goto::{goto_definition, goto_type_definition};
pub use hover::hover;
pub use inlay_hints::inlay_hints;
pub use markup::MarkupKind;

View File

@@ -15,7 +15,7 @@ pub use program::{
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, HasType, NameKind, SemanticModel};
pub use semantic_model::{Completion, HasDefinition, HasType, NameKind, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;

View File

@@ -1,12 +1,14 @@
use ruff_db::files::{File, FilePath};
use ruff_db::source::line_index;
use ruff_python_ast as ast;
use ruff_python_ast::{self as ast, ExprContext};
use ruff_python_ast::{Expr, ExprRef, name::Name};
use ruff_source_file::LineIndex;
use crate::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::{KnownModule, Module, resolve_module};
use crate::semantic_index::ast_ids::HasScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::place::FileScopeId;
use crate::semantic_index::semantic_index;
use crate::types::ide_support::all_declarations_and_bindings;
@@ -175,6 +177,41 @@ pub struct Completion {
pub builtin: bool,
}
pub trait HasDefinition {
/// Returns the definitions of `self`.
///
/// ## Panics
/// May panic if `self` is from another file than `model`.
fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option<Vec<Definition<'db>>>;
}
impl HasDefinition for ast::ExprRef<'_> {
fn definitions<'db>(&self, model: &SemanticModel<'db>) -> Option<Vec<Definition<'db>>> {
match self {
ExprRef::Name(name) => match name.ctx {
ExprContext::Load => {
let index = semantic_index(model.db, model.file);
let file_scope = index.expression_scope_id(*self);
let scope = file_scope.to_scope_id(model.db, model.file);
let use_def = index.use_def_map(file_scope);
let use_id = self.scoped_use_id(model.db, scope);
Some(
use_def
.bindings_at_use(use_id)
.filter_map(|binding| binding.binding.definition())
.collect(),
)
}
ExprContext::Store => None,
ExprContext::Del => None,
ExprContext::Invalid => None,
},
_ => None,
}
}
}
pub trait HasType {
/// Returns the inferred type of `self`.
///

View File

@@ -6,7 +6,7 @@ use crate::session::{AllOptions, ClientOptions, Session};
use lsp_server::Connection;
use lsp_types::{
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
InlayHintOptions, InlayHintServerCapabilities, MessageType, ServerCapabilities,
InlayHintOptions, InlayHintServerCapabilities, MessageType, OneOf, ServerCapabilities,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TypeDefinitionProviderCapability, Url,
};
@@ -183,6 +183,7 @@ impl Server {
..Default::default()
},
)),
definition_provider: Some(OneOf::Left(true)),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
inlay_hint_provider: Some(lsp_types::OneOf::Right(

View File

@@ -43,6 +43,9 @@ pub(super) fn request(req: server::Request) -> Task {
>(
req, BackgroundSchedule::Worker
),
requests::GotoDefinitionRequestHandler::METHOD => background_document_request_task::<
requests::GotoDefinitionRequestHandler,
>(req, BackgroundSchedule::Worker),
requests::HoverRequestHandler::METHOD => background_document_request_task::<
requests::HoverRequestHandler,
>(req, BackgroundSchedule::Worker),

View File

@@ -1,5 +1,6 @@
mod completion;
mod diagnostic;
mod goto_definition;
mod goto_type_definition;
mod hover;
mod inlay_hints;
@@ -8,6 +9,7 @@ mod workspace_diagnostic;
pub(super) use completion::CompletionRequestHandler;
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
pub(super) use goto_definition::GotoDefinitionRequestHandler;
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
pub(super) use hover::HoverRequestHandler;
pub(super) use inlay_hints::InlayHintRequestHandler;

View File

@@ -0,0 +1,77 @@
use std::borrow::Cow;
use lsp_types::GotoDefinitionParams;
use lsp_types::request::GotoDefinition;
use lsp_types::{GotoDefinitionResponse, Url};
use ruff_db::source::{line_index, source_text};
use ty_ide::goto_definition;
use ty_project::ProjectDatabase;
use crate::DocumentSnapshot;
use crate::document::{PositionExt, ToLink};
use crate::server::api::traits::{
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
};
use crate::session::client::Client;
pub(crate) struct GotoDefinitionRequestHandler;
impl RequestHandler for GotoDefinitionRequestHandler {
type RequestType = GotoDefinition;
}
impl BackgroundDocumentRequestHandler for GotoDefinitionRequestHandler {
fn document_url(params: &GotoDefinitionParams) -> Cow<Url> {
Cow::Borrowed(&params.text_document_position_params.text_document.uri)
}
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
_client: &Client,
params: GotoDefinitionParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
if snapshot.client_settings().is_language_services_disabled() {
return Ok(None);
}
let Some(file) = snapshot.file(db) else {
tracing::debug!("Failed to resolve file for {:?}", params);
return Ok(None);
};
let source = source_text(db, file);
let line_index = line_index(db, file);
let offset = params.text_document_position_params.position.to_text_size(
&source,
&line_index,
snapshot.encoding(),
);
let Some(ranged) = goto_definition(db, file, offset) else {
return Ok(None);
};
if snapshot
.resolved_client_capabilities()
.type_definition_link_support
{
let src = Some(ranged.range);
let links: Vec<_> = ranged
.into_iter()
.filter_map(|target| target.to_link(db, src, snapshot.encoding()))
.collect();
Ok(Some(GotoDefinitionResponse::Link(links)))
} else {
let locations: Vec<_> = ranged
.into_iter()
.filter_map(|target| target.to_location(db, snapshot.encoding()))
.collect();
Ok(Some(GotoDefinitionResponse::Array(locations)))
}
}
}
impl RetriableRequestHandler for GotoDefinitionRequestHandler {}