mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-28 18:53:25 +00:00
[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
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:
parent
a04375173c
commit
c20d906503
6 changed files with 316 additions and 45 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue