mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-22 16:22:52 +00:00
[ty] Initial implementation of declaration and definition providers. (#19371)
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
This PR implements "go to definition" and "go to declaration" functionality for name nodes only. Future PRs will add support for attributes, module names in import statements, keyword argument names, etc. This PR: * Registers a declaration and definition request handler for the language server. * Splits out the `goto_type_definition` into its own module. The `goto` module contains functionality that is common to `goto_type_definition`, `goto_declaration` and `goto_definition`. * Roughs in a new module `stub_mapping` that is not yet implemented. It will be responsible for mapping a definition in a stub file to its corresponding definition(s) in an implementation (source) file. * Adds a new IDE support function `definitions_for_name` that collects all of the definitions associated with a name and resolves any imports (recursively) to find the original definitions associated with that name. * Adds a new `VisibleAncestorsIter` stuct that iterates up the scope hierarchy but skips scopes that are not visible to starting scope. --------- Co-authored-by: UnboundVariable <unbound@gmail.com>
This commit is contained in:
parent
cbe94b094b
commit
fae0b5c89e
16 changed files with 2244 additions and 664 deletions
|
@ -1,34 +1,16 @@
|
|||
pub use crate::goto_declaration::goto_declaration;
|
||||
pub use crate::goto_definition::goto_definition;
|
||||
pub use crate::goto_type_definition::goto_type_definition;
|
||||
|
||||
use crate::find_node::covering_node;
|
||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||
use crate::stub_mapping::StubMapper;
|
||||
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::types::Type;
|
||||
use ty_python_semantic::{HasType, SemanticModel};
|
||||
|
||||
pub fn goto_type_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 ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
|
||||
|
||||
let navigation_targets = ty.navigation_targets(db);
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: navigation_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum GotoTarget<'a> {
|
||||
Expression(ast::ExprRef<'a>),
|
||||
|
@ -154,6 +136,100 @@ impl GotoTarget<'_> {
|
|||
|
||||
Some(ty)
|
||||
}
|
||||
|
||||
/// Gets the navigation ranges for this goto target.
|
||||
/// If a stub mapper is provided, definitions from stub files will be mapped to
|
||||
/// their corresponding source file implementations.
|
||||
pub(crate) fn get_definition_targets(
|
||||
self,
|
||||
file: ruff_db::files::File,
|
||||
db: &dyn crate::Db,
|
||||
stub_mapper: Option<&StubMapper>,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
use crate::NavigationTarget;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
match self {
|
||||
// For names, find the definitions of the symbol
|
||||
GotoTarget::Expression(expression) => {
|
||||
if let ast::ExprRef::Name(name) = expression {
|
||||
Self::get_name_definition_targets(name, file, db, stub_mapper)
|
||||
} else {
|
||||
// For other expressions, we can't find definitions
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// For already-defined symbols, they are their own definitions
|
||||
GotoTarget::FunctionDef(function) => {
|
||||
let range = function.name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: function.range(),
|
||||
}))
|
||||
}
|
||||
|
||||
GotoTarget::ClassDef(class) => {
|
||||
let range = class.name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: class.range(),
|
||||
}))
|
||||
}
|
||||
|
||||
GotoTarget::Parameter(parameter) => {
|
||||
let range = parameter.name.range;
|
||||
Some(crate::NavigationTargets::single(NavigationTarget {
|
||||
file,
|
||||
focus_range: range,
|
||||
full_range: parameter.range(),
|
||||
}))
|
||||
}
|
||||
|
||||
// For imports, find the symbol being imported
|
||||
GotoTarget::Alias(_alias) => {
|
||||
// For aliases, we don't have the ExprName node, so we can't get the scope
|
||||
// For now, return None. In the future, we could look up the imported symbol
|
||||
None
|
||||
}
|
||||
|
||||
// TODO: Handle attribute and method accesses (y in `x.y` expressions)
|
||||
// TODO: Handle keyword arguments in call expression
|
||||
// TODO: Handle multi-part module names in import statements
|
||||
// TODO: Handle imported symbol in y in `from x import y as z` statement
|
||||
// TODO: Handle string literals that map to TypedDict fields
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get navigation targets for definitions associated with a name expression
|
||||
fn get_name_definition_targets(
|
||||
name: &ruff_python_ast::ExprName,
|
||||
file: ruff_db::files::File,
|
||||
db: &dyn crate::Db,
|
||||
stub_mapper: Option<&StubMapper>,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
use ty_python_semantic::definitions_for_name;
|
||||
|
||||
// Get all definitions for this name
|
||||
let mut definitions = definitions_for_name(db, file, name);
|
||||
|
||||
// Apply stub mapping if a mapper is provided
|
||||
if let Some(mapper) = stub_mapper {
|
||||
definitions = mapper.map_definitions(definitions);
|
||||
}
|
||||
|
||||
if definitions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Convert definitions to navigation targets
|
||||
let targets = convert_resolved_definitions_to_targets(db, definitions);
|
||||
|
||||
Some(crate::NavigationTargets::unique(targets))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for GotoTarget<'_> {
|
||||
|
@ -180,6 +256,41 @@ impl Ranged for GotoTarget<'_> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items.
|
||||
fn convert_resolved_definitions_to_targets(
|
||||
db: &dyn crate::Db,
|
||||
definitions: Vec<ty_python_semantic::ResolvedDefinition<'_>>,
|
||||
) -> Vec<crate::NavigationTarget> {
|
||||
definitions
|
||||
.into_iter()
|
||||
.map(|resolved_definition| match resolved_definition {
|
||||
ty_python_semantic::ResolvedDefinition::Definition(definition) => {
|
||||
// Get the parsed module for range calculation
|
||||
let definition_file = definition.file(db);
|
||||
let module = ruff_db::parsed::parsed_module(db, definition_file).load(db);
|
||||
|
||||
// Get the ranges for this definition
|
||||
let focus_range = definition.focus_range(db, &module);
|
||||
let full_range = definition.full_range(db, &module);
|
||||
|
||||
crate::NavigationTarget {
|
||||
file: focus_range.file(),
|
||||
focus_range: focus_range.range(),
|
||||
full_range: full_range.range(),
|
||||
}
|
||||
}
|
||||
ty_python_semantic::ResolvedDefinition::ModuleFile(module_file) => {
|
||||
// For module files, navigate to the beginning of the file
|
||||
crate::NavigationTarget {
|
||||
file: module_file,
|
||||
focus_range: ruff_text_size::TextRange::default(), // Start of file
|
||||
full_range: ruff_text_size::TextRange::default(), // Start of file
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn find_goto_target(
|
||||
parsed: &ParsedModuleRef,
|
||||
offset: TextSize,
|
||||
|
@ -250,637 +361,3 @@ pub(crate) fn find_goto_target(
|
|||
node => node.as_expr_ref().map(GotoTarget::Expression),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
|
||||
use crate::{NavigationTarget, goto_type_definition};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_class_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Test: ...
|
||||
|
||||
a<CURSOR>b = Test()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:19
|
||||
|
|
||||
2 | class Test: ...
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | class Test: ...
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_function_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
ab = foo
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | ab = foo
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:13
|
||||
|
|
||||
4 | ab = foo
|
||||
5 |
|
||||
6 | ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_union_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice():
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:3:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
| ^^^
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
| ^^^
|
||||
6 |
|
||||
7 | if random.choice():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
lib<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "test"
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | a: str = "test"
|
||||
3 |
|
||||
4 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "te<CURSOR>st"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
|
|
||||
2 | a: str = "test"
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:24
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_param_spec() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type param specs
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_tuple() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type var tuples
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_bare_type_alias_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
Alias = TypeAliasType("Alias", tuple[int, int])
|
||||
|
||||
Alias<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | from typing_extensions import TypeAliasType
|
||||
3 |
|
||||
4 | Alias = TypeAliasType("Alias", tuple[int, int])
|
||||
| ^^^^^
|
||||
5 |
|
||||
6 | Alias
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:13
|
||||
|
|
||||
4 | Alias = TypeAliasType("Alias", tuple[int, int])
|
||||
5 |
|
||||
6 | Alias
|
||||
| ^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= "123")
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= "123")
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should jump to `str` and not `int` because
|
||||
// the keyword is typed as a string. It's only the passed argument that
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:338:7
|
||||
|
|
||||
336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
337 |
|
||||
338 | class int:
|
||||
| ^^^
|
||||
339 | """int([x]) -> integer
|
||||
340 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_kwargs() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def f(name: str): ...
|
||||
|
||||
kwargs = { "name": "test"}
|
||||
|
||||
f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:2892:7
|
||||
|
|
||||
2890 | """See PEP 585"""
|
||||
2891 |
|
||||
2892 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
2893 | """dict() -> new empty dictionary
|
||||
2894 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:5
|
||||
|
|
||||
4 | kwargs = { "name": "test"}
|
||||
5 |
|
||||
6 | f(**kwargs)
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class X:
|
||||
def foo(a, b): ...
|
||||
|
||||
x = X()
|
||||
|
||||
x<CURSOR>.foo()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:19
|
||||
|
|
||||
2 | class X:
|
||||
| ^
|
||||
3 | def foo(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:13
|
||||
|
|
||||
5 | x = X()
|
||||
6 |
|
||||
7 | x.foo()
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_between_call_arguments() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | foo()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo()
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_narrowing() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_none() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:922:11
|
||||
|
|
||||
920 | if sys.version_info >= (3, 10):
|
||||
921 | @final
|
||||
922 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
923 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) =
|
||||
goto_type_definition(&self.db, self.cursor.file, self.cursor.offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No type definitions found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTypeDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoTypeDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(Severity::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-type-definition")),
|
||||
Severity::Info,
|
||||
"Type definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
813
crates/ty_ide/src/goto_declaration.rs
Normal file
813
crates/ty_ide/src/goto_declaration.rs
Normal file
|
@ -0,0 +1,813 @@
|
|||
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};
|
||||
|
||||
/// Navigate to the declaration of a symbol.
|
||||
///
|
||||
/// A "declaration" includes both formal declarations (class statements, def statements,
|
||||
/// and variable annotations) but also variable assignments. This expansive definition
|
||||
/// is needed because Python doesn't require formal declarations of variables like most languages do.
|
||||
pub fn goto_declaration(
|
||||
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 declaration_targets = goto_target.get_definition_targets(file, db, None)?;
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: declaration_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
|
||||
use crate::{NavigationTarget, goto_declaration};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_function_call_to_definition() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def my_function(x, y):
|
||||
return x + y
|
||||
|
||||
result = my_func<CURSOR>tion(1, 2)
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | def my_function(x, y):
|
||||
| ^^^^^^^^^^^
|
||||
3 | return x + y
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:22
|
||||
|
|
||||
3 | return x + y
|
||||
4 |
|
||||
5 | result = my_function(1, 2)
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_variable_assignment() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
x = 42
|
||||
y = x<CURSOR>
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | x = 42
|
||||
| ^
|
||||
3 | y = x
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | x = 42
|
||||
3 | y = x
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_class_instantiation() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
instance = My<CURSOR>Class()
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:19
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self):
|
||||
4 | pass
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:24
|
||||
|
|
||||
4 | pass
|
||||
5 |
|
||||
6 | instance = MyClass()
|
||||
| ^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_parameter_usage() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def foo(param):
|
||||
return pa<CURSOR>ram * 2
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:21
|
||||
|
|
||||
2 | def foo(param):
|
||||
| ^^^^^
|
||||
3 | return param * 2
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:24
|
||||
|
|
||||
2 | def foo(param):
|
||||
3 | return param * 2
|
||||
| ^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_type_parameter() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def generic_func[T](value: T) -> T:
|
||||
v: T<CURSOR> = value
|
||||
return v
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:30
|
||||
|
|
||||
2 | def generic_func[T](value: T) -> T:
|
||||
| ^
|
||||
3 | v: T = value
|
||||
4 | return v
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:20
|
||||
|
|
||||
2 | def generic_func[T](value: T) -> T:
|
||||
3 | v: T = value
|
||||
| ^
|
||||
4 | return v
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_type_parameter_class() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class GenericClass[T]:
|
||||
def __init__(self, value: T<CURSOR>):
|
||||
self.value = value
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:32
|
||||
|
|
||||
2 | class GenericClass[T]:
|
||||
| ^
|
||||
3 | def __init__(self, value: T):
|
||||
4 | self.value = value
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | class GenericClass[T]:
|
||||
3 | def __init__(self, value: T):
|
||||
| ^
|
||||
4 | self.value = value
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_nested_scope_variable() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
x = \"outer\"
|
||||
def outer_func():
|
||||
def inner_func():
|
||||
return x<CURSOR> # Should find outer x
|
||||
return inner_func
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | x = "outer"
|
||||
| ^
|
||||
3 | def outer_func():
|
||||
4 | def inner_func():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:28
|
||||
|
|
||||
3 | def outer_func():
|
||||
4 | def inner_func():
|
||||
5 | return x # Should find outer x
|
||||
| ^
|
||||
6 | return inner_func
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_class_scope_skipped() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class A:
|
||||
x = 1
|
||||
|
||||
def method(self):
|
||||
def inner():
|
||||
return <CURSOR>x # Should NOT find class variable x
|
||||
return inner
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should not find the class variable 'x' due to Python's scoping rules
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_import_simple() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
import mymodule
|
||||
print(mymod<CURSOR>ule.function())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule.py",
|
||||
r#"
|
||||
def function():
|
||||
return "hello from mymodule"
|
||||
|
||||
variable = 42
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mymodule.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | def function():
|
||||
3 | return "hello from mymodule"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | import mymodule
|
||||
3 | print(mymodule.function())
|
||||
| ^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_import_from() {
|
||||
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"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> 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())
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_import_as() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
import mymodule.submodule as sub
|
||||
print(<CURSOR>sub.helper())
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule/__init__.py",
|
||||
"
|
||||
# Main module init
|
||||
",
|
||||
)
|
||||
.source(
|
||||
"mymodule/submodule.py",
|
||||
r#"
|
||||
FOO = 0
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Should find the submodule file itself
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> mymodule/submodule.py:1:1
|
||||
|
|
||||
1 |
|
||||
| ^
|
||||
2 | FOO = 0
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | import mymodule.submodule as sub
|
||||
3 | print(sub.helper())
|
||||
| ^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_from_import_as() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from utils import func as h
|
||||
print(<CURSOR>h("test"))
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"utils.py",
|
||||
r#"
|
||||
def func(arg):
|
||||
return f"Processed: {arg}"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Should resolve to the actual function definition, not the import statement
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> utils.py:2:5
|
||||
|
|
||||
2 | def func(arg):
|
||||
| ^^^^
|
||||
3 | return f"Processed: {arg}"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from utils import func as h
|
||||
3 | print(h("test"))
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_from_import_chain() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from intermediate import shared_function
|
||||
print(shared_func<CURSOR>tion())
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"intermediate.py",
|
||||
r#"
|
||||
# Re-export the function from the original module
|
||||
from original import shared_function
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"original.py",
|
||||
r#"
|
||||
def shared_function():
|
||||
return "from original"
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> original.py:2:5
|
||||
|
|
||||
2 | def shared_function():
|
||||
| ^^^^^^^^^^^^^^^
|
||||
3 | return "from original"
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | from intermediate import shared_function
|
||||
3 | print(shared_function())
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_from_star_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from math_utils import *
|
||||
result = add_n<CURSOR>umbers(5, 3)
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"math_utils.py",
|
||||
r#"
|
||||
def add_numbers(a, b):
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
def multiply_numbers(a, b):
|
||||
"""Multiply two numbers together."""
|
||||
return a * b
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> math_utils.py:2:5
|
||||
|
|
||||
2 | def add_numbers(a, b):
|
||||
| ^^^^^^^^^^^
|
||||
3 | """Add two numbers together."""
|
||||
4 | return a + b
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:10
|
||||
|
|
||||
2 | from math_utils import *
|
||||
3 | result = add_numbers(5, 3)
|
||||
| ^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_relative_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"package/main.py",
|
||||
r#"
|
||||
from .utils import helper_function
|
||||
result = helper_func<CURSOR>tion("test")
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"package/__init__.py",
|
||||
r#"
|
||||
# Package init file
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"package/utils.py",
|
||||
r#"
|
||||
def helper_function(arg):
|
||||
"""A helper function in utils module."""
|
||||
return f"Processed: {arg}"
|
||||
|
||||
def another_helper():
|
||||
"""Another helper function."""
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
// Should resolve the relative import to find the actual function definition
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> package/utils.py:2:5
|
||||
|
|
||||
2 | def helper_function(arg):
|
||||
| ^^^^^^^^^^^^^^^
|
||||
3 | """A helper function in utils module."""
|
||||
4 | return f"Processed: {arg}"
|
||||
|
|
||||
info: Source
|
||||
--> package/main.py:3:10
|
||||
|
|
||||
2 | from .utils import helper_function
|
||||
3 | result = helper_function("test")
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_relative_star_import() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"package/main.py",
|
||||
r#"
|
||||
from .utils import *
|
||||
result = helper_func<CURSOR>tion("test")
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"package/__init__.py",
|
||||
r#"
|
||||
# Package init file
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"package/utils.py",
|
||||
r#"
|
||||
def helper_function(arg):
|
||||
"""A helper function in utils module."""
|
||||
return f"Processed: {arg}"
|
||||
|
||||
def another_helper():
|
||||
"""Another helper function."""
|
||||
pass
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> package/utils.py:2:5
|
||||
|
|
||||
2 | def helper_function(arg):
|
||||
| ^^^^^^^^^^^^^^^
|
||||
3 | """A helper function in utils module."""
|
||||
4 | return f"Processed: {arg}"
|
||||
|
|
||||
info: Source
|
||||
--> package/main.py:3:10
|
||||
|
|
||||
2 | from .utils import *
|
||||
3 | result = helper_function("test")
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_builtin_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
x: i<CURSOR>nt = 42
|
||||
"#,
|
||||
);
|
||||
|
||||
// Test that we can navigate to builtin types, but don't snapshot the exact content
|
||||
// since typeshed stubs can change frequently
|
||||
let result = test.goto_declaration();
|
||||
|
||||
// Should not be "No goto target found" - we should find the builtin int type
|
||||
assert!(
|
||||
!result.contains("No goto target found"),
|
||||
"Should find builtin int type"
|
||||
);
|
||||
assert!(
|
||||
!result.contains("No declarations found"),
|
||||
"Should find builtin int declarations"
|
||||
);
|
||||
|
||||
// Should navigate to a stdlib file containing the int class
|
||||
assert!(
|
||||
result.contains("builtins.pyi"),
|
||||
"Should navigate to builtins.pyi"
|
||||
);
|
||||
assert!(
|
||||
result.contains("class int:"),
|
||||
"Should find the int class definition"
|
||||
);
|
||||
assert!(
|
||||
result.contains("info[goto-declaration]: Declaration"),
|
||||
"Should be a goto-declaration result"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_nonlocal_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def outer():
|
||||
x = "outer_value"
|
||||
|
||||
def inner():
|
||||
nonlocal x
|
||||
x = "modified"
|
||||
return x<CURSOR> # Should find the nonlocal x declaration in outer scope
|
||||
|
||||
return inner
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the variable declaration in the outer scope, not the nonlocal statement
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | def outer():
|
||||
3 | x = "outer_value"
|
||||
| ^
|
||||
4 |
|
||||
5 | def inner():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:8:16
|
||||
|
|
||||
6 | nonlocal x
|
||||
7 | x = "modified"
|
||||
8 | return x # Should find the nonlocal x declaration in outer scope
|
||||
| ^
|
||||
9 |
|
||||
10 | return inner
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_global_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
global_var = "global_value"
|
||||
|
||||
def function():
|
||||
global global_var
|
||||
global_var = "modified"
|
||||
return global_<CURSOR>var # Should find the global variable declaration
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the global variable declaration, not the global statement
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:1
|
||||
|
|
||||
2 | global_var = "global_value"
|
||||
| ^^^^^^^^^^
|
||||
3 |
|
||||
4 | def function():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:12
|
||||
|
|
||||
5 | global global_var
|
||||
6 | global_var = "modified"
|
||||
7 | return global_var # Should find the global variable declaration
|
||||
| ^^^^^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_generic_method_class_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class MyClass:
|
||||
ClassType = int
|
||||
|
||||
def generic_method[T](self, value: Class<CURSOR>Type) -> T:
|
||||
return value
|
||||
"#,
|
||||
);
|
||||
|
||||
// Should find the ClassType defined in the class body, not fail to resolve
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:5
|
||||
|
|
||||
2 | class MyClass:
|
||||
3 | ClassType = int
|
||||
| ^^^^^^^^^
|
||||
4 |
|
||||
5 | def generic_method[T](self, value: ClassType) -> T:
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:40
|
||||
|
|
||||
3 | ClassType = int
|
||||
4 |
|
||||
5 | def generic_method[T](self, value: ClassType) -> T:
|
||||
| ^^^^^^^^^
|
||||
6 | return value
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_declaration(&self) -> String {
|
||||
let Some(targets) = goto_declaration(&self.db, self.cursor.file, self.cursor.offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No declarations found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoDeclarationDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoDeclarationDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoDeclarationDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoDeclarationDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(Severity::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-declaration")),
|
||||
Severity::Info,
|
||||
"Declaration".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
31
crates/ty_ide/src/goto_definition.rs
Normal file
31
crates/ty_ide/src/goto_definition.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::goto::find_goto_target;
|
||||
use crate::stub_mapping::StubMapper;
|
||||
use crate::{Db, NavigationTargets, RangedValue};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
|
||||
/// 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)?;
|
||||
|
||||
// Create a StubMapper to map from stub files to source files
|
||||
let stub_mapper = StubMapper::new(db);
|
||||
|
||||
let definition_targets = goto_target.get_definition_targets(file, db, Some(&stub_mapper))?;
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: definition_targets,
|
||||
})
|
||||
}
|
661
crates/ty_ide/src/goto_type_definition.rs
Normal file
661
crates/ty_ide/src/goto_type_definition.rs
Normal file
|
@ -0,0 +1,661 @@
|
|||
use crate::goto::find_goto_target;
|
||||
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue};
|
||||
use ruff_db::files::{File, FileRange};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ty_python_semantic::SemanticModel;
|
||||
|
||||
pub fn goto_type_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 ty = goto_target.inferred_type(&model)?;
|
||||
|
||||
tracing::debug!("Inferred type of covering node is {}", ty.display(db));
|
||||
|
||||
let navigation_targets = ty.navigation_targets(db);
|
||||
|
||||
Some(RangedValue {
|
||||
range: FileRange::new(file, goto_target.range()),
|
||||
value: navigation_targets,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tests::{CursorTest, IntoDiagnostic, cursor_test};
|
||||
use crate::{NavigationTarget, goto_type_definition};
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span, SubDiagnostic,
|
||||
};
|
||||
use ruff_db::files::FileRange;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_class_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Test: ...
|
||||
|
||||
a<CURSOR>b = Test()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:19
|
||||
|
|
||||
2 | class Test: ...
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | class Test: ...
|
||||
3 |
|
||||
4 | ab = Test()
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_function_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
ab = foo
|
||||
|
||||
a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | ab = foo
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:13
|
||||
|
|
||||
4 | ab = foo
|
||||
5 |
|
||||
6 | ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_union_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
|
||||
def foo(a, b): ...
|
||||
|
||||
def bar(a, b): ...
|
||||
|
||||
if random.choice():
|
||||
a = foo
|
||||
else:
|
||||
a = bar
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:3:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
| ^^^
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | def foo(a, b): ...
|
||||
4 |
|
||||
5 | def bar(a, b): ...
|
||||
| ^^^
|
||||
6 |
|
||||
7 | if random.choice():
|
||||
|
|
||||
info: Source
|
||||
--> main.py:12:13
|
||||
|
|
||||
10 | a = bar
|
||||
11 |
|
||||
12 | a
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_module() {
|
||||
let mut test = cursor_test(
|
||||
r#"
|
||||
import lib
|
||||
|
||||
lib<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
test.write_file("lib.py", "a = 10").unwrap();
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> lib.py:1:1
|
||||
|
|
||||
1 | a = 10
|
||||
| ^^^^^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | import lib
|
||||
3 |
|
||||
4 | lib
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "test"
|
||||
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | a: str = "test"
|
||||
3 |
|
||||
4 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_literal_node() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
a: str = "te<CURSOR>st"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:22
|
||||
|
|
||||
2 | a: str = "test"
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[T: int = bool] = list[T<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:24
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:46
|
||||
|
|
||||
2 | type Alias[T: int = bool] = list[T]
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_param_spec() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[**P = [int, str]] = Callable[P<CURSOR>, int]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type param specs
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_type_var_tuple() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias[*Ts = ()] = tuple[*Ts<CURSOR>]
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: Goto type definition currently doesn't work for type var tuples
|
||||
// because the inference doesn't support them yet.
|
||||
// This snapshot should show a single target pointing to `T`
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_bare_type_alias_type() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
Alias = TypeAliasType("Alias", tuple[int, int])
|
||||
|
||||
Alias<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | from typing_extensions import TypeAliasType
|
||||
3 |
|
||||
4 | Alias = TypeAliasType("Alias", tuple[int, int])
|
||||
| ^^^^^
|
||||
5 |
|
||||
6 | Alias
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:13
|
||||
|
|
||||
4 | Alias = TypeAliasType("Alias", tuple[int, int])
|
||||
5 |
|
||||
6 | Alias
|
||||
| ^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= "123")
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= "123")
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_incorrectly_typed_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def test(a: str): ...
|
||||
|
||||
test(a<CURSOR>= 123)
|
||||
"#,
|
||||
);
|
||||
|
||||
// TODO: This should jump to `str` and not `int` because
|
||||
// the keyword is typed as a string. It's only the passed argument that
|
||||
// is an int. Navigating to `str` would match pyright's behavior.
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:338:7
|
||||
|
|
||||
336 | _LiteralInteger = _PositiveInteger | _NegativeInteger | Literal[0] # noqa: Y026 # TODO: Use TypeAlias once mypy bugs are fixed
|
||||
337 |
|
||||
338 | class int:
|
||||
| ^^^
|
||||
339 | """int([x]) -> integer
|
||||
340 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:18
|
||||
|
|
||||
2 | def test(a: str): ...
|
||||
3 |
|
||||
4 | test(a= 123)
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_kwargs() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def f(name: str): ...
|
||||
|
||||
kwargs = { "name": "test"}
|
||||
|
||||
f(**kwargs<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:2892:7
|
||||
|
|
||||
2890 | """See PEP 585"""
|
||||
2891 |
|
||||
2892 | class dict(MutableMapping[_KT, _VT]):
|
||||
| ^^^^
|
||||
2893 | """dict() -> new empty dictionary
|
||||
2894 | dict(mapping) -> new dictionary initialized from a mapping object's
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:5
|
||||
|
|
||||
4 | kwargs = { "name": "test"}
|
||||
5 |
|
||||
6 | f(**kwargs)
|
||||
| ^^^^^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_of_expression_with_builtin() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_definition_cursor_between_object_and_attribute() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class X:
|
||||
def foo(a, b): ...
|
||||
|
||||
x = X()
|
||||
|
||||
x<CURSOR>.foo()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:19
|
||||
|
|
||||
2 | class X:
|
||||
| ^
|
||||
3 | def foo(a, b): ...
|
||||
|
|
||||
info: Source
|
||||
--> main.py:7:13
|
||||
|
|
||||
5 | x = X()
|
||||
6 |
|
||||
7 | x.foo()
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_between_call_arguments() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a, b): ...
|
||||
|
||||
foo<CURSOR>()
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
| ^^^
|
||||
3 |
|
||||
4 | foo()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:13
|
||||
|
|
||||
2 | def foo(a, b): ...
|
||||
3 |
|
||||
4 | foo()
|
||||
| ^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_narrowing() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
if a is not None:
|
||||
print(a<CURSOR>)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:27
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | if a is not None:
|
||||
4 | print(a)
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_none() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def foo(a: str | None, b):
|
||||
a<CURSOR>
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/types.pyi:922:11
|
||||
|
|
||||
920 | if sys.version_info >= (3, 10):
|
||||
921 | @final
|
||||
922 | class NoneType:
|
||||
| ^^^^^^^^
|
||||
923 | """The type of the None singleton."""
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/builtins.pyi:892:7
|
||||
|
|
||||
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
|
||||
891 |
|
||||
892 | class str(Sequence[str]):
|
||||
| ^^^
|
||||
893 | """str(object='') -> str
|
||||
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:17
|
||||
|
|
||||
2 | def foo(a: str | None, b):
|
||||
3 | a
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
impl CursorTest {
|
||||
fn goto_type_definition(&self) -> String {
|
||||
let Some(targets) =
|
||||
goto_type_definition(&self.db, self.cursor.file, self.cursor.offset)
|
||||
else {
|
||||
return "No goto target found".to_string();
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
return "No type definitions found".to_string();
|
||||
}
|
||||
|
||||
let source = targets.range;
|
||||
self.render_diagnostics(
|
||||
targets
|
||||
.into_iter()
|
||||
.map(|target| GotoTypeDefinitionDiagnostic::new(source, &target)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct GotoTypeDefinitionDiagnostic {
|
||||
source: FileRange,
|
||||
target: FileRange,
|
||||
}
|
||||
|
||||
impl GotoTypeDefinitionDiagnostic {
|
||||
fn new(source: FileRange, target: &NavigationTarget) -> Self {
|
||||
Self {
|
||||
source,
|
||||
target: FileRange::new(target.file(), target.focus_range()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoDiagnostic for GotoTypeDefinitionDiagnostic {
|
||||
fn into_diagnostic(self) -> Diagnostic {
|
||||
let mut source = SubDiagnostic::new(Severity::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-type-definition")),
|
||||
Severity::Info,
|
||||
"Type definition".to_string(),
|
||||
);
|
||||
main.annotate(Annotation::primary(
|
||||
Span::from(self.target.file()).with_range(self.target.range()),
|
||||
));
|
||||
main.sub(source);
|
||||
|
||||
main
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,16 +3,20 @@ mod db;
|
|||
mod docstring;
|
||||
mod find_node;
|
||||
mod goto;
|
||||
mod goto_declaration;
|
||||
mod goto_definition;
|
||||
mod goto_type_definition;
|
||||
mod hover;
|
||||
mod inlay_hints;
|
||||
mod markup;
|
||||
mod semantic_tokens;
|
||||
mod signature_help;
|
||||
mod stub_mapping;
|
||||
|
||||
pub use completion::completion;
|
||||
pub use db::Db;
|
||||
pub use docstring::get_parameter_documentation;
|
||||
pub use goto::goto_type_definition;
|
||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||
pub use hover::hover;
|
||||
pub use inlay_hints::inlay_hints;
|
||||
pub use markup::MarkupKind;
|
||||
|
|
47
crates/ty_ide/src/stub_mapping.rs
Normal file
47
crates/ty_ide/src/stub_mapping.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use ty_python_semantic::ResolvedDefinition;
|
||||
|
||||
/// Maps `ResolvedDefinitions` from stub files to corresponding definitions in source files.
|
||||
///
|
||||
/// This mapper is used to implement "Go To Definition" functionality that navigates from
|
||||
/// stub file declarations to their actual implementations in source files. It also allows
|
||||
/// other language server providers (like hover, completion, and signature help) to find
|
||||
/// docstrings for functions that resolve to stubs.
|
||||
pub(crate) struct StubMapper<'db> {
|
||||
#[allow(dead_code)] // Will be used when implementation is added
|
||||
db: &'db dyn crate::Db,
|
||||
}
|
||||
|
||||
impl<'db> StubMapper<'db> {
|
||||
#[allow(dead_code)] // Will be used in the future
|
||||
pub(crate) fn new(db: &'db dyn crate::Db) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
|
||||
/// Map a `ResolvedDefinition` from a stub file to corresponding definitions in source files.
|
||||
///
|
||||
/// If the definition is in a stub file and a corresponding source file definition exists,
|
||||
/// returns the source file definition(s). Otherwise, returns the original definition.
|
||||
#[allow(dead_code)] // Will be used when implementation is added
|
||||
#[allow(clippy::unused_self)] // Will use self when implementation is added
|
||||
pub(crate) fn map_definition(
|
||||
&self,
|
||||
def: ResolvedDefinition<'db>,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
// TODO: Implement stub-to-source mapping logic
|
||||
// For now, just return the original definition
|
||||
vec![def]
|
||||
}
|
||||
|
||||
/// Map multiple `ResolvedDefinitions`, applying stub-to-source mapping to each.
|
||||
///
|
||||
/// This is a convenience method that applies `map_definition` to each element
|
||||
/// in the input vector and flattens the results.
|
||||
pub(crate) fn map_definitions(
|
||||
&self,
|
||||
defs: Vec<ResolvedDefinition<'db>>,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
defs.into_iter()
|
||||
.flat_map(|def| self.map_definition(def))
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ pub use program::{
|
|||
pub use python_platform::PythonPlatform;
|
||||
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
|
||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||
pub use types::definitions_for_name;
|
||||
pub use types::ide_support::ResolvedDefinition;
|
||||
pub use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
|
||||
|
||||
pub mod ast_node_ref;
|
||||
|
|
|
@ -388,6 +388,24 @@ impl<'db> SemanticIndex<'db> {
|
|||
AncestorsIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns an iterator over ancestors of `scope` that are visible for name resolution,
|
||||
/// starting with `scope` itself. This follows Python's lexical scoping rules where
|
||||
/// class scopes are skipped during name resolution (except for the starting scope
|
||||
/// if it happens to be a class scope).
|
||||
///
|
||||
/// For example, in this code:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
/// class A:
|
||||
/// x = 2
|
||||
/// def method(self):
|
||||
/// print(x) # Refers to global x=1, not class x=2
|
||||
/// ```
|
||||
/// The `method` function can see the global scope but not the class scope.
|
||||
pub(crate) fn visible_ancestor_scopes(&self, scope: FileScopeId) -> VisibleAncestorsIter {
|
||||
VisibleAncestorsIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns the [`definition::Definition`] salsa ingredient(s) for `definition_key`.
|
||||
///
|
||||
/// There will only ever be >1 `Definition` associated with a `definition_key`
|
||||
|
@ -553,6 +571,53 @@ impl<'a> Iterator for AncestorsIter<'a> {
|
|||
|
||||
impl FusedIterator for AncestorsIter<'_> {}
|
||||
|
||||
pub struct VisibleAncestorsIter<'a> {
|
||||
inner: AncestorsIter<'a>,
|
||||
starting_scope_kind: ScopeKind,
|
||||
yielded_count: usize,
|
||||
}
|
||||
|
||||
impl<'a> VisibleAncestorsIter<'a> {
|
||||
fn new(module_table: &'a SemanticIndex, start: FileScopeId) -> Self {
|
||||
let starting_scope = &module_table.scopes[start];
|
||||
Self {
|
||||
inner: AncestorsIter::new(module_table, start),
|
||||
starting_scope_kind: starting_scope.kind(),
|
||||
yielded_count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for VisibleAncestorsIter<'a> {
|
||||
type Item = (FileScopeId, &'a Scope);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
let (scope_id, scope) = self.inner.next()?;
|
||||
self.yielded_count += 1;
|
||||
|
||||
// Always return the first scope (the starting scope)
|
||||
if self.yielded_count == 1 {
|
||||
return Some((scope_id, scope));
|
||||
}
|
||||
|
||||
// Skip class scopes for subsequent scopes (following Python's lexical scoping rules)
|
||||
// Exception: type parameter scopes can see names defined in an immediately-enclosing class scope
|
||||
if scope.kind() == ScopeKind::Class {
|
||||
// Allow type parameter scopes to see their immediately-enclosing class scope exactly once
|
||||
if self.starting_scope_kind.is_type_parameter() && self.yielded_count == 2 {
|
||||
return Some((scope_id, scope));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return Some((scope_id, scope));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for VisibleAncestorsIter<'_> {}
|
||||
|
||||
pub struct DescendantsIter<'a> {
|
||||
next_id: FileScopeId,
|
||||
descendants: std::slice::Iter<'a, Scope>,
|
||||
|
|
|
@ -23,7 +23,7 @@ use crate::unpack::{Unpack, UnpackPosition};
|
|||
#[salsa::tracked(debug)]
|
||||
pub struct Definition<'db> {
|
||||
/// The file in which the definition occurs.
|
||||
pub(crate) file: File,
|
||||
pub file: File,
|
||||
|
||||
/// The scope in which the definition occurs.
|
||||
pub(crate) file_scope: FileScopeId,
|
||||
|
|
|
@ -49,6 +49,7 @@ use crate::types::generics::{
|
|||
};
|
||||
pub use crate::types::ide_support::{
|
||||
CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name,
|
||||
definitions_for_name,
|
||||
};
|
||||
use crate::types::infer::infer_unpack_types;
|
||||
use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use crate::place::{Place, imported_symbol, place_from_bindings, place_from_declarations};
|
||||
use crate::place::{
|
||||
Place, builtins_module_scope, imported_symbol, place_from_bindings, place_from_declarations,
|
||||
};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::definition::DefinitionKind;
|
||||
use crate::semantic_index::place::ScopeId;
|
||||
|
@ -386,6 +388,121 @@ pub fn definition_kind_for_name<'db>(
|
|||
None
|
||||
}
|
||||
|
||||
/// Returns all definitions for a name. If any definitions are imports, they
|
||||
/// are resolved (recursively) to the original definitions or module files.
|
||||
pub fn definitions_for_name<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
name: &ast::ExprName,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
let index = semantic_index(db, file);
|
||||
let name_str = name.id.as_str();
|
||||
|
||||
// Get the scope for this name expression
|
||||
let Some(file_scope) = index.try_expression_scope_id(&ast::Expr::Name(name.clone())) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut all_definitions = Vec::new();
|
||||
|
||||
// Search through the scope hierarchy: start from the current scope and
|
||||
// traverse up through parent scopes to find definitions
|
||||
for (scope_id, _scope) in index.visible_ancestor_scopes(file_scope) {
|
||||
let place_table = index.place_table(scope_id);
|
||||
|
||||
let Some(place_id) = place_table.place_id_by_name(name_str) else {
|
||||
continue; // Name not found in this scope, try parent scope
|
||||
};
|
||||
|
||||
// Check if this place is marked as global or nonlocal
|
||||
let place_expr = place_table.place_expr(place_id);
|
||||
let is_global = place_expr.is_marked_global();
|
||||
let is_nonlocal = place_expr.is_marked_nonlocal();
|
||||
|
||||
// TODO: The current algorithm doesn't return definintions or bindings
|
||||
// for other scopes that are outside of this scope hierarchy that target
|
||||
// this name using a nonlocal or global binding. The semantic analyzer
|
||||
// doesn't appear to track these in a way that we can easily access
|
||||
// them from here without walking all scopes in the module.
|
||||
|
||||
// If marked as global, skip to global scope
|
||||
if is_global {
|
||||
let global_scope_id = global_scope(db, file);
|
||||
let global_place_table = crate::semantic_index::place_table(db, global_scope_id);
|
||||
|
||||
if let Some(global_place_id) = global_place_table.place_id_by_name(name_str) {
|
||||
let global_use_def_map = crate::semantic_index::use_def_map(db, global_scope_id);
|
||||
let global_bindings = global_use_def_map.all_reachable_bindings(global_place_id);
|
||||
let global_declarations =
|
||||
global_use_def_map.all_reachable_declarations(global_place_id);
|
||||
|
||||
for binding in global_bindings {
|
||||
if let Some(def) = binding.binding.definition() {
|
||||
all_definitions.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
for declaration in global_declarations {
|
||||
if let Some(def) = declaration.declaration.definition() {
|
||||
all_definitions.push(def);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If marked as nonlocal, skip current scope and search in ancestor scopes
|
||||
if is_nonlocal {
|
||||
// Continue searching in parent scopes, but skip the current scope
|
||||
continue;
|
||||
}
|
||||
|
||||
let use_def_map = index.use_def_map(scope_id);
|
||||
|
||||
// Get all definitions (both bindings and declarations) for this place
|
||||
let bindings = use_def_map.all_reachable_bindings(place_id);
|
||||
let declarations = use_def_map.all_reachable_declarations(place_id);
|
||||
|
||||
for binding in bindings {
|
||||
if let Some(def) = binding.binding.definition() {
|
||||
all_definitions.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
for declaration in declarations {
|
||||
if let Some(def) = declaration.declaration.definition() {
|
||||
all_definitions.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
// If we found definitions in this scope, we can stop searching
|
||||
if !all_definitions.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve import definitions to their targets
|
||||
let mut resolved_definitions = Vec::new();
|
||||
|
||||
for definition in &all_definitions {
|
||||
let resolved = resolve_definition(db, *definition, Some(name_str));
|
||||
resolved_definitions.extend(resolved);
|
||||
}
|
||||
|
||||
// If we didn't find any definitions in scopes, fallback to builtins
|
||||
if resolved_definitions.is_empty() {
|
||||
let Some(builtins_scope) = builtins_module_scope(db) else {
|
||||
return Vec::new();
|
||||
};
|
||||
find_symbol_in_scope(db, builtins_scope, name_str)
|
||||
.into_iter()
|
||||
.flat_map(|def| resolve_definition(db, def, Some(name_str)))
|
||||
.collect()
|
||||
} else {
|
||||
resolved_definitions
|
||||
}
|
||||
}
|
||||
|
||||
/// Details about a callable signature for IDE support.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CallSignatureDetails<'db> {
|
||||
|
@ -455,3 +572,201 @@ pub fn call_signature_details<'db>(
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
mod resolve_definition {
|
||||
//! Resolves an Import, `ImportFrom` or `StarImport` definition to one or more
|
||||
//! "resolved definitions". This is done recursively to find the original
|
||||
//! definition targeted by the import.
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast as ast;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::place::ScopeId;
|
||||
use crate::semantic_index::{global_scope, place_table, use_def_map};
|
||||
use crate::{Db, ModuleName, resolve_module};
|
||||
|
||||
/// Represents the result of resolving an import to either a specific definition or a module file.
|
||||
/// This enum helps distinguish between cases where an import resolves to:
|
||||
/// - A specific definition within a module (e.g., `from os import path` -> definition of `path`)
|
||||
/// - An entire module file (e.g., `import os` -> the `os` module file itself)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ResolvedDefinition<'db> {
|
||||
/// The import resolved to a specific definition within a module
|
||||
Definition(Definition<'db>),
|
||||
/// The import resolved to an entire module file
|
||||
ModuleFile(File),
|
||||
}
|
||||
|
||||
/// Resolve import definitions to their targets.
|
||||
/// Returns resolved definitions which can be either specific definitions or module files.
|
||||
/// For non-import definitions, returns the definition wrapped in `ResolvedDefinition::Definition`.
|
||||
/// Always returns at least the original definition as a fallback if resolution fails.
|
||||
pub(crate) fn resolve_definition<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
symbol_name: Option<&str>,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
let mut visited = FxHashSet::default();
|
||||
let resolved = resolve_definition_recursive(db, definition, &mut visited, symbol_name);
|
||||
|
||||
// If resolution failed, return the original definition as fallback
|
||||
if resolved.is_empty() {
|
||||
vec![ResolvedDefinition::Definition(definition)]
|
||||
} else {
|
||||
resolved
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to resolve import definitions recursively.
|
||||
fn resolve_definition_recursive<'db>(
|
||||
db: &'db dyn Db,
|
||||
definition: Definition<'db>,
|
||||
visited: &mut FxHashSet<Definition<'db>>,
|
||||
symbol_name: Option<&str>,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
// Prevent infinite recursion if there are circular imports
|
||||
if visited.contains(&definition) {
|
||||
return Vec::new(); // Return empty list for circular imports
|
||||
}
|
||||
visited.insert(definition);
|
||||
|
||||
let kind = definition.kind(db);
|
||||
|
||||
match kind {
|
||||
DefinitionKind::Import(import_def) => {
|
||||
let file = definition.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
let alias = import_def.alias(&module);
|
||||
|
||||
// Get the full module name being imported
|
||||
let Some(module_name) = ModuleName::new(&alias.name) else {
|
||||
return Vec::new(); // Invalid module name, return empty list
|
||||
};
|
||||
|
||||
// Resolve the module to its file
|
||||
let Some(resolved_module) = resolve_module(db, &module_name) else {
|
||||
return Vec::new(); // Module not found, return empty list
|
||||
};
|
||||
|
||||
let Some(module_file) = resolved_module.file() else {
|
||||
return Vec::new(); // No file for module, return empty list
|
||||
};
|
||||
|
||||
// For simple imports like "import os", we want to navigate to the module itself.
|
||||
// Return the module file directly instead of trying to find definitions within it.
|
||||
vec![ResolvedDefinition::ModuleFile(module_file)]
|
||||
}
|
||||
|
||||
DefinitionKind::ImportFrom(import_from_def) => {
|
||||
let file = definition.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
let import_node = import_from_def.import(&module);
|
||||
let alias = import_from_def.alias(&module);
|
||||
|
||||
// For `ImportFrom`, we need to resolve the original imported symbol name
|
||||
// (alias.name), not the local alias (symbol_name)
|
||||
resolve_from_import_definitions(db, file, import_node, &alias.name, visited)
|
||||
}
|
||||
|
||||
// For star imports, try to resolve to the specific symbol being accessed
|
||||
DefinitionKind::StarImport(star_import_def) => {
|
||||
let file = definition.file(db);
|
||||
let module = parsed_module(db, file).load(db);
|
||||
let import_node = star_import_def.import(&module);
|
||||
|
||||
// If we have a symbol name, use the helper to resolve it in the target module
|
||||
if let Some(symbol_name) = symbol_name {
|
||||
resolve_from_import_definitions(db, file, import_node, symbol_name, visited)
|
||||
} else {
|
||||
// No symbol context provided, can't resolve star import
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
// For non-import definitions, return the definition as is
|
||||
_ => vec![ResolvedDefinition::Definition(definition)],
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to resolve import definitions for `ImportFrom` and `StarImport` cases.
|
||||
fn resolve_from_import_definitions<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
import_node: &ast::StmtImportFrom,
|
||||
symbol_name: &str,
|
||||
visited: &mut FxHashSet<Definition<'db>>,
|
||||
) -> Vec<ResolvedDefinition<'db>> {
|
||||
// Resolve the target module file
|
||||
let module_file = {
|
||||
// Resolve the module being imported from (handles both relative and absolute imports)
|
||||
let Some(module_name) = ModuleName::from_import_statement(db, file, import_node).ok()
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(resolved_module) = resolve_module(db, &module_name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
resolved_module.file()
|
||||
};
|
||||
|
||||
let Some(module_file) = module_file else {
|
||||
return Vec::new(); // Module resolution failed
|
||||
};
|
||||
|
||||
// Find the definition of this symbol in the imported module's global scope
|
||||
let global_scope = global_scope(db, module_file);
|
||||
let definitions_in_module = find_symbol_in_scope(db, global_scope, symbol_name);
|
||||
|
||||
// Recursively resolve any import definitions found in the target module
|
||||
if definitions_in_module.is_empty() {
|
||||
// If we can't find the specific symbol, return empty list
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut resolved_definitions = Vec::new();
|
||||
for def in definitions_in_module {
|
||||
let resolved = resolve_definition_recursive(db, def, visited, Some(symbol_name));
|
||||
resolved_definitions.extend(resolved);
|
||||
}
|
||||
resolved_definitions
|
||||
}
|
||||
}
|
||||
|
||||
/// Find definitions for a symbol name in a specific scope.
|
||||
pub(crate) fn find_symbol_in_scope<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
symbol_name: &str,
|
||||
) -> Vec<Definition<'db>> {
|
||||
let place_table = place_table(db, scope);
|
||||
let Some(place_id) = place_table.place_id_by_name(symbol_name) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let use_def_map = use_def_map(db, scope);
|
||||
let mut definitions = Vec::new();
|
||||
|
||||
// Get all definitions (both bindings and declarations) for this place
|
||||
let bindings = use_def_map.all_reachable_bindings(place_id);
|
||||
let declarations = use_def_map.all_reachable_declarations(place_id);
|
||||
|
||||
for binding in bindings {
|
||||
if let Some(def) = binding.binding.definition() {
|
||||
definitions.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
for declaration in declarations {
|
||||
if let Some(def) = declaration.declaration.definition() {
|
||||
definitions.push(def);
|
||||
}
|
||||
}
|
||||
|
||||
definitions
|
||||
}
|
||||
}
|
||||
|
||||
pub use resolve_definition::ResolvedDefinition;
|
||||
use resolve_definition::{find_symbol_in_scope, resolve_definition};
|
||||
|
|
|
@ -5,9 +5,9 @@ use crate::PositionEncoding;
|
|||
use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session};
|
||||
use lsp_server::Connection;
|
||||
use lsp_types::{
|
||||
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability,
|
||||
InitializeParams, InlayHintOptions, InlayHintServerCapabilities, MessageType,
|
||||
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
|
||||
ClientCapabilities, DeclarationCapability, DiagnosticOptions, DiagnosticServerCapabilities,
|
||||
HoverProviderCapability, InitializeParams, InlayHintOptions, InlayHintServerCapabilities,
|
||||
MessageType, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
|
||||
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
|
||||
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
|
||||
};
|
||||
|
@ -194,6 +194,8 @@ impl Server {
|
|||
},
|
||||
)),
|
||||
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
|
||||
definition_provider: Some(lsp_types::OneOf::Left(true)),
|
||||
declaration_provider: Some(DeclarationCapability::Simple(true)),
|
||||
hover_provider: Some(HoverProviderCapability::Simple(true)),
|
||||
signature_help_provider: Some(SignatureHelpOptions {
|
||||
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),
|
||||
|
|
|
@ -44,6 +44,14 @@ pub(super) fn request(req: server::Request) -> Task {
|
|||
>(
|
||||
req, BackgroundSchedule::Worker
|
||||
),
|
||||
requests::GotoDeclarationRequestHandler::METHOD => background_document_request_task::<
|
||||
requests::GotoDeclarationRequestHandler,
|
||||
>(
|
||||
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),
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
mod completion;
|
||||
mod diagnostic;
|
||||
mod goto_declaration;
|
||||
mod goto_definition;
|
||||
mod goto_type_definition;
|
||||
mod hover;
|
||||
mod inlay_hints;
|
||||
|
@ -11,6 +13,8 @@ mod workspace_diagnostic;
|
|||
|
||||
pub(super) use completion::CompletionRequestHandler;
|
||||
pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
|
||||
pub(super) use goto_declaration::GotoDeclarationRequestHandler;
|
||||
pub(super) use goto_definition::GotoDefinitionRequestHandler;
|
||||
pub(super) use goto_type_definition::GotoTypeDefinitionRequestHandler;
|
||||
pub(super) use hover::HoverRequestHandler;
|
||||
pub(super) use inlay_hints::InlayHintRequestHandler;
|
||||
|
|
75
crates/ty_server/src/server/api/requests/goto_declaration.rs
Normal file
75
crates/ty_server/src/server/api/requests/goto_declaration.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use lsp_types::request::{GotoDeclaration, GotoDeclarationParams};
|
||||
use lsp_types::{GotoDefinitionResponse, Url};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ty_ide::goto_declaration;
|
||||
use ty_project::ProjectDatabase;
|
||||
|
||||
use crate::document::{PositionExt, ToLink};
|
||||
use crate::server::api::traits::{
|
||||
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||
};
|
||||
use crate::session::DocumentSnapshot;
|
||||
use crate::session::client::Client;
|
||||
|
||||
pub(crate) struct GotoDeclarationRequestHandler;
|
||||
|
||||
impl RequestHandler for GotoDeclarationRequestHandler {
|
||||
type RequestType = GotoDeclaration;
|
||||
}
|
||||
|
||||
impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler {
|
||||
fn document_url(params: &GotoDeclarationParams) -> Cow<Url> {
|
||||
Cow::Borrowed(¶ms.text_document_position_params.text_document.uri)
|
||||
}
|
||||
|
||||
fn run_with_snapshot(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
_client: &Client,
|
||||
params: GotoDeclarationParams,
|
||||
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
|
||||
if snapshot.client_settings().is_language_services_disabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(file) = snapshot.file(db) else {
|
||||
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_declaration(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 GotoDeclarationRequestHandler {}
|
75
crates/ty_server/src/server/api/requests/goto_definition.rs
Normal file
75
crates/ty_server/src/server/api/requests/goto_definition.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use lsp_types::request::GotoDefinition;
|
||||
use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse, Url};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ty_ide::goto_definition;
|
||||
use ty_project::ProjectDatabase;
|
||||
|
||||
use crate::document::{PositionExt, ToLink};
|
||||
use crate::server::api::traits::{
|
||||
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||
};
|
||||
use crate::session::DocumentSnapshot;
|
||||
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(¶ms.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 {
|
||||
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 {}
|
Loading…
Add table
Add a link
Reference in a new issue