[ty] improve goto/hover for definitions (#19976)
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

By computing the actual Definition for, well, definitions, we unlock a
bunch of richer machinery in the goto/hover subsystems for free.

Fixes https://github.com/astral-sh/ty/issues/1001
Fixes https://github.com/astral-sh/ty/issues/1004
This commit is contained in:
Aria Desires 2025-08-18 21:42:53 -04:00 committed by GitHub
parent a04375173c
commit c20d906503
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 316 additions and 45 deletions

View file

@ -11,6 +11,7 @@ use ruff_db::parsed::ParsedModuleRef;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::HasDefinition;
use ty_python_semantic::ImportAliasResolution;
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::types::Type;
@ -285,36 +286,24 @@ impl GotoTarget<'_> {
// For already-defined symbols, they are their own definitions
GotoTarget::FunctionDef(function) => {
let range = function.name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: range,
full_range: function.range(),
}),
))
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(function.definition(&model)),
]))
}
GotoTarget::ClassDef(class) => {
let range = class.name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: range,
full_range: class.range(),
}),
))
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(class.definition(&model)),
]))
}
GotoTarget::Parameter(parameter) => {
let range = parameter.name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget {
file,
focus_range: range,
full_range: parameter.range(),
}),
))
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(parameter.definition(&model)),
]))
}
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
@ -376,14 +365,10 @@ impl GotoTarget<'_> {
// For exception variables, they are their own definitions (like parameters)
GotoTarget::ExceptVariable(except_handler) => {
if let Some(name) = &except_handler.name {
let range = name.range;
Some(DefinitionsOrTargets::Targets(
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
))
} else {
None
}
let model = SemanticModel::new(db, file);
Some(DefinitionsOrTargets::Definitions(vec![
ResolvedDefinition::Definition(except_handler.definition(&model)),
]))
}
// For pattern match rest variables, they are their own definitions

View file

@ -181,6 +181,49 @@ def other_function(): ...
"#);
}
/// goto-definition on a function definition in a .pyi should go to the .py
#[test]
fn goto_definition_stub_map_function_def() {
let test = CursorTest::builder()
.source(
"mymodule.py",
r#"
def my_function():
return "hello"
def other_function():
return "other"
"#,
)
.source(
"mymodule.pyi",
r#"
def my_fun<CURSOR>ction(): ...
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
--> mymodule.pyi:2:5
|
2 | def my_function(): ...
| ^^^^^^^^^^^
3 |
4 | def other_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
@ -328,6 +371,53 @@ class MyOtherClass:
");
}
/// goto-definition on a class def in a .pyi should go to the .py
#[test]
fn goto_definition_stub_map_class_def() {
let test = CursorTest::builder()
.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 MyCl<CURSOR>ass:
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
--> mymodule.pyi:2:7
|
2 | class MyClass:
| ^^^^^^^
3 | def __init__(self, val: bool): ...
|
");
}
/// goto-definition on a class init should go to the .py not the .pyi
#[test]
fn goto_definition_stub_map_class_init() {

View file

@ -405,9 +405,7 @@ except ValueError as err:
",
);
// Note: Currently only finds the declaration, not the usages
// This is because semantic analysis for except handler variables isn't fully implemented
assert_snapshot!(test.references(), @r###"
assert_snapshot!(test.references(), @r"
info[references]: Reference 1
--> main.py:4:29
|
@ -418,7 +416,37 @@ except ValueError as err:
5 | print(f'Error: {err}')
6 | return err
|
"###);
info[references]: Reference 2
--> main.py:5:21
|
3 | x = 1 / 0
4 | except ZeroDivisionError as err:
5 | print(f'Error: {err}')
| ^^^
6 | return err
|
info[references]: Reference 3
--> main.py:6:12
|
4 | except ZeroDivisionError as err:
5 | print(f'Error: {err}')
6 | return err
| ^^^
7 |
8 | try:
|
info[references]: Reference 4
--> main.py:11:31
|
9 | y = 2 / 0
10 | except ValueError as err:
11 | print(f'Different error: {err}')
| ^^^
|
");
}
#[test]

View file

@ -237,6 +237,57 @@ mod tests {
");
}
#[test]
fn hover_function_def() {
let test = cursor_test(
r#"
def my_fu<CURSOR>nc(a, b):
'''This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
'''
return 0
"#,
);
assert_snapshot!(test.hover(), @r"
def my_func(a, b) -> Unknown
---------------------------------------------
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
---------------------------------------------
```python
def my_func(a, b) -> Unknown
```
---
```text
This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:13
|
2 | def my_func(a, b):
| ^^^^^-^
| | |
| | Cursor offset
| source
3 | '''This is such a great func!!
|
");
}
#[test]
fn hover_class() {
let test = cursor_test(
@ -304,6 +355,71 @@ mod tests {
");
}
#[test]
fn hover_class_def() {
let test = cursor_test(
r#"
class MyCla<CURSOR>ss:
'''
This is such a great class!!
Don't you know?
Everyone loves my class!!
'''
def __init__(self, val):
"""initializes MyClass (perfectly)"""
self.val = val
def my_method(self, a, b):
'''This is such a great func!!
Args:
a: first for a reason
b: coming for `a`'s title
'''
return 0
"#,
);
assert_snapshot!(test.hover(), @r"
<class 'MyClass'>
---------------------------------------------
This is such a great class!!
Don't you know?
Everyone loves my class!!
---------------------------------------------
```python
<class 'MyClass'>
```
---
```text
This is such a great class!!
Don't you know?
Everyone loves my class!!
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:15
|
2 | class MyClass:
| ^^^^^-^
| | |
| | Cursor offset
| source
3 | '''
4 | This is such a great class!!
|
");
}
#[test]
fn hover_class_init() {
let test = cursor_test(
@ -571,6 +687,40 @@ mod tests {
");
}
#[test]
fn hover_keyword_parameter_def() {
let test = cursor_test(
r#"
def test(a<CURSOR>b: int):
"""my cool test
Args:
ab: a nice little integer
"""
return 0
"#,
);
assert_snapshot!(test.hover(), @r#"
int
---------------------------------------------
```python
int
```
---------------------------------------------
info[hover]: Hovered content is
--> main.py:2:22
|
2 | def test(ab: int):
| ^-
| ||
| |Cursor offset
| source
3 | """my cool test
|
"#);
}
#[test]
fn hover_union() {
let test = cursor_test(

View file

@ -15,7 +15,9 @@ pub use program::{
PythonVersionWithSource, SearchPathSettings,
};
pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
pub use semantic_model::{
Completion, CompletionKind, HasDefinition, HasType, NameKind, SemanticModel,
};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
pub use types::ide_support::{
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute,

View file

@ -7,6 +7,7 @@ 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::definition::Definition;
use crate::semantic_index::scope::FileScopeId;
use crate::semantic_index::semantic_index;
use crate::types::ide_support::all_declarations_and_bindings;
@ -296,6 +297,14 @@ pub trait HasType {
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db>;
}
pub trait HasDefinition {
/// Returns the inferred type of `self`.
///
/// ## Panics
/// May panic if `self` is from another file than `model`.
fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db>;
}
impl HasType for ast::ExprRef<'_> {
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let index = semantic_index(model.db, model.file);
@ -392,24 +401,31 @@ impl HasType for ast::Expr {
}
}
macro_rules! impl_binding_has_ty {
macro_rules! impl_binding_has_ty_def {
($ty: ty) => {
impl HasDefinition for $ty {
#[inline]
fn definition<'db>(&self, model: &SemanticModel<'db>) -> Definition<'db> {
let index = semantic_index(model.db, model.file);
index.expect_single_definition(self)
}
}
impl HasType for $ty {
#[inline]
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
let index = semantic_index(model.db, model.file);
let binding = index.expect_single_definition(self);
let binding = HasDefinition::definition(self, model);
binding_type(model.db, binding)
}
}
};
}
impl_binding_has_ty!(ast::StmtFunctionDef);
impl_binding_has_ty!(ast::StmtClassDef);
impl_binding_has_ty!(ast::Parameter);
impl_binding_has_ty!(ast::ParameterWithDefault);
impl_binding_has_ty!(ast::ExceptHandlerExceptHandler);
impl_binding_has_ty_def!(ast::StmtFunctionDef);
impl_binding_has_ty_def!(ast::StmtClassDef);
impl_binding_has_ty_def!(ast::Parameter);
impl_binding_has_ty_def!(ast::ParameterWithDefault);
impl_binding_has_ty_def!(ast::ExceptHandlerExceptHandler);
impl HasType for ast::Alias {
fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {