[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

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:
UnboundVariable 2025-07-16 15:07:24 -07:00 committed by GitHub
parent cbe94b094b
commit fae0b5c89e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 2244 additions and 664 deletions

View file

@ -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::find_node::covering_node;
use crate::{Db, HasNavigationTargets, NavigationTargets, RangedValue}; use crate::stub_mapping::StubMapper;
use ruff_db::files::{File, FileRange}; use ruff_db::parsed::ParsedModuleRef;
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
use ruff_python_ast::{self as ast, AnyNodeRef}; use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_python_parser::TokenKind; use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::types::Type; use ty_python_semantic::types::Type;
use ty_python_semantic::{HasType, SemanticModel}; 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)] #[derive(Clone, Copy, Debug)]
pub(crate) enum GotoTarget<'a> { pub(crate) enum GotoTarget<'a> {
Expression(ast::ExprRef<'a>), Expression(ast::ExprRef<'a>),
@ -154,6 +136,100 @@ impl GotoTarget<'_> {
Some(ty) 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<'_> { 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( pub(crate) fn find_goto_target(
parsed: &ParsedModuleRef, parsed: &ParsedModuleRef,
offset: TextSize, offset: TextSize,
@ -250,637 +361,3 @@ pub(crate) fn find_goto_target(
node => node.as_expr_ref().map(GotoTarget::Expression), 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
}
}
}

View 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
}
}
}

View 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,
})
}

View 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
}
}
}

View file

@ -3,16 +3,20 @@ mod db;
mod docstring; mod docstring;
mod find_node; mod find_node;
mod goto; mod goto;
mod goto_declaration;
mod goto_definition;
mod goto_type_definition;
mod hover; mod hover;
mod inlay_hints; mod inlay_hints;
mod markup; mod markup;
mod semantic_tokens; mod semantic_tokens;
mod signature_help; mod signature_help;
mod stub_mapping;
pub use completion::completion; pub use completion::completion;
pub use db::Db; pub use db::Db;
pub use docstring::get_parameter_documentation; 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 hover::hover;
pub use inlay_hints::inlay_hints; pub use inlay_hints::inlay_hints;
pub use markup::MarkupKind; pub use markup::MarkupKind;

View 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()
}
}

View file

@ -17,6 +17,8 @@ pub use program::{
pub use python_platform::PythonPlatform; pub use python_platform::PythonPlatform;
pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel}; pub use semantic_model::{Completion, CompletionKind, HasType, NameKind, SemanticModel};
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin}; 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 use util::diagnostics::add_inferred_python_version_hint_to_diagnostic;
pub mod ast_node_ref; pub mod ast_node_ref;

View file

@ -388,6 +388,24 @@ impl<'db> SemanticIndex<'db> {
AncestorsIter::new(self, scope) 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`. /// Returns the [`definition::Definition`] salsa ingredient(s) for `definition_key`.
/// ///
/// There will only ever be >1 `Definition` associated with a `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<'_> {} 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> { pub struct DescendantsIter<'a> {
next_id: FileScopeId, next_id: FileScopeId,
descendants: std::slice::Iter<'a, Scope>, descendants: std::slice::Iter<'a, Scope>,

View file

@ -23,7 +23,7 @@ use crate::unpack::{Unpack, UnpackPosition};
#[salsa::tracked(debug)] #[salsa::tracked(debug)]
pub struct Definition<'db> { pub struct Definition<'db> {
/// The file in which the definition occurs. /// The file in which the definition occurs.
pub(crate) file: File, pub file: File,
/// The scope in which the definition occurs. /// The scope in which the definition occurs.
pub(crate) file_scope: FileScopeId, pub(crate) file_scope: FileScopeId,

View file

@ -49,6 +49,7 @@ use crate::types::generics::{
}; };
pub use crate::types::ide_support::{ pub use crate::types::ide_support::{
CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name, CallSignatureDetails, Member, all_members, call_signature_details, definition_kind_for_name,
definitions_for_name,
}; };
use crate::types::infer::infer_unpack_types; use crate::types::infer::infer_unpack_types;
use crate::types::mro::{Mro, MroError, MroIterator}; use crate::types::mro::{Mro, MroError, MroIterator};

View file

@ -1,6 +1,8 @@
use std::cmp::Ordering; 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::Definition;
use crate::semantic_index::definition::DefinitionKind; use crate::semantic_index::definition::DefinitionKind;
use crate::semantic_index::place::ScopeId; use crate::semantic_index::place::ScopeId;
@ -386,6 +388,121 @@ pub fn definition_kind_for_name<'db>(
None 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. /// Details about a callable signature for IDE support.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CallSignatureDetails<'db> { pub struct CallSignatureDetails<'db> {
@ -455,3 +572,201 @@ pub fn call_signature_details<'db>(
vec![] 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};

View file

@ -5,9 +5,9 @@ use crate::PositionEncoding;
use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session}; use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session};
use lsp_server::Connection; use lsp_server::Connection;
use lsp_types::{ use lsp_types::{
ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, ClientCapabilities, DeclarationCapability, DiagnosticOptions, DiagnosticServerCapabilities,
InitializeParams, InlayHintOptions, InlayHintServerCapabilities, MessageType, HoverProviderCapability, InitializeParams, InlayHintOptions, InlayHintServerCapabilities,
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities, MessageType, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind, ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions, TextDocumentSyncOptions, TypeDefinitionProviderCapability, Url, WorkDoneProgressOptions,
}; };
@ -194,6 +194,8 @@ impl Server {
}, },
)), )),
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), 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)), hover_provider: Some(HoverProviderCapability::Simple(true)),
signature_help_provider: Some(SignatureHelpOptions { signature_help_provider: Some(SignatureHelpOptions {
trigger_characters: Some(vec!["(".to_string(), ",".to_string()]), trigger_characters: Some(vec!["(".to_string(), ",".to_string()]),

View file

@ -44,6 +44,14 @@ pub(super) fn request(req: server::Request) -> Task {
>( >(
req, BackgroundSchedule::Worker 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::METHOD => background_document_request_task::<
requests::HoverRequestHandler, requests::HoverRequestHandler,
>(req, BackgroundSchedule::Worker), >(req, BackgroundSchedule::Worker),

View file

@ -1,5 +1,7 @@
mod completion; mod completion;
mod diagnostic; mod diagnostic;
mod goto_declaration;
mod goto_definition;
mod goto_type_definition; mod goto_type_definition;
mod hover; mod hover;
mod inlay_hints; mod inlay_hints;
@ -11,6 +13,8 @@ mod workspace_diagnostic;
pub(super) use completion::CompletionRequestHandler; pub(super) use completion::CompletionRequestHandler;
pub(super) use diagnostic::DocumentDiagnosticRequestHandler; 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 goto_type_definition::GotoTypeDefinitionRequestHandler;
pub(super) use hover::HoverRequestHandler; pub(super) use hover::HoverRequestHandler;
pub(super) use inlay_hints::InlayHintRequestHandler; pub(super) use inlay_hints::InlayHintRequestHandler;

View 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(&params.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 {}

View 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(&params.text_document_position_params.text_document.uri)
}
fn run_with_snapshot(
db: &ProjectDatabase,
snapshot: DocumentSnapshot,
_client: &Client,
params: GotoDefinitionParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
if snapshot.client_settings().is_language_services_disabled() {
return Ok(None);
}
let Some(file) = snapshot.file(db) else {
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 {}