[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

@ -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;

View file

@ -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>,

View file

@ -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,

View file

@ -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};

View file

@ -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};