mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-25 17:38:19 +00:00
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>
1666 lines
55 KiB
Rust
1666 lines
55 KiB
Rust
use std::iter::FusedIterator;
|
|
use std::sync::Arc;
|
|
|
|
use ruff_db::files::File;
|
|
use ruff_db::parsed::parsed_module;
|
|
use ruff_index::{IndexSlice, IndexVec};
|
|
|
|
use ruff_python_ast::NodeIndex;
|
|
use ruff_python_parser::semantic_errors::SemanticSyntaxError;
|
|
use rustc_hash::{FxHashMap, FxHashSet};
|
|
use salsa::Update;
|
|
use salsa::plumbing::AsId;
|
|
|
|
use crate::Db;
|
|
use crate::module_name::ModuleName;
|
|
use crate::node_key::NodeKey;
|
|
use crate::semantic_index::ast_ids::AstIds;
|
|
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
|
use crate::semantic_index::builder::SemanticIndexBuilder;
|
|
use crate::semantic_index::definition::{Definition, DefinitionNodeKey, Definitions};
|
|
use crate::semantic_index::expression::Expression;
|
|
use crate::semantic_index::narrowing_constraints::ScopedNarrowingConstraint;
|
|
use crate::semantic_index::place::{
|
|
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, PlaceExpr, PlaceTable, Scope, ScopeId,
|
|
ScopeKind, ScopedPlaceId,
|
|
};
|
|
use crate::semantic_index::use_def::{EagerSnapshotKey, ScopedEagerSnapshotId, UseDefMap};
|
|
use crate::semantic_model::HasTrackedScope;
|
|
use crate::util::get_size::untracked_arc_size;
|
|
|
|
pub mod ast_ids;
|
|
mod builder;
|
|
pub mod definition;
|
|
pub mod expression;
|
|
pub(crate) mod narrowing_constraints;
|
|
pub mod place;
|
|
pub(crate) mod predicate;
|
|
mod re_exports;
|
|
mod reachability_constraints;
|
|
mod use_def;
|
|
|
|
pub(crate) use self::use_def::{
|
|
ApplicableConstraints, BindingWithConstraints, BindingWithConstraintsIterator,
|
|
DeclarationWithConstraint, DeclarationsIterator,
|
|
};
|
|
|
|
type PlaceSet = hashbrown::HashTable<ScopedPlaceId>;
|
|
|
|
/// Returns the semantic index for `file`.
|
|
///
|
|
/// Prefer using [`symbol_table`] when working with symbols from a single scope.
|
|
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)]
|
|
pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
|
|
let _span = tracing::trace_span!("semantic_index", ?file).entered();
|
|
|
|
let module = parsed_module(db, file).load(db);
|
|
|
|
SemanticIndexBuilder::new(db, file, &module).build()
|
|
}
|
|
|
|
/// Returns the place table for a specific `scope`.
|
|
///
|
|
/// Using [`place_table`] over [`semantic_index`] has the advantage that
|
|
/// Salsa can avoid invalidating dependent queries if this scope's place table
|
|
/// is unchanged.
|
|
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
|
|
pub(crate) fn place_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<PlaceTable> {
|
|
let file = scope.file(db);
|
|
let _span = tracing::trace_span!("place_table", scope=?scope.as_id(), ?file).entered();
|
|
let index = semantic_index(db, file);
|
|
|
|
index.place_table(scope.file_scope_id(db))
|
|
}
|
|
|
|
/// Returns the set of modules that are imported anywhere in `file`.
|
|
///
|
|
/// This set only considers `import` statements, not `from...import` statements, because:
|
|
///
|
|
/// - In `from foo import bar`, we cannot determine whether `foo.bar` is a submodule (and is
|
|
/// therefore imported) without looking outside the content of this file. (We could turn this
|
|
/// into a _potentially_ imported modules set, but that would change how it's used in our type
|
|
/// inference logic.)
|
|
///
|
|
/// - We cannot resolve relative imports (which aren't allowed in `import` statements) without
|
|
/// knowing the name of the current module, and whether it's a package.
|
|
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
|
|
pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc<FxHashSet<ModuleName>> {
|
|
semantic_index(db, file).imported_modules.clone()
|
|
}
|
|
|
|
/// Returns the use-def map for a specific `scope`.
|
|
///
|
|
/// Using [`use_def_map`] over [`semantic_index`] has the advantage that
|
|
/// Salsa can avoid invalidating dependent queries if this scope's use-def map
|
|
/// is unchanged.
|
|
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
|
|
pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> ArcUseDefMap<'db> {
|
|
let file = scope.file(db);
|
|
let _span = tracing::trace_span!("use_def_map", scope=?scope.as_id(), ?file).entered();
|
|
let index = semantic_index(db, file);
|
|
|
|
index.use_def_map(scope.file_scope_id(db))
|
|
}
|
|
|
|
/// Returns all attribute assignments (and their method scope IDs) with a symbol name matching
|
|
/// the one given for a specific class body scope.
|
|
///
|
|
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
|
/// introduces a direct dependency on that file's AST.
|
|
pub(crate) fn attribute_assignments<'db, 's>(
|
|
db: &'db dyn Db,
|
|
class_body_scope: ScopeId<'db>,
|
|
name: &'s str,
|
|
) -> impl Iterator<Item = (BindingWithConstraintsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
|
|
let file = class_body_scope.file(db);
|
|
let index = semantic_index(db, file);
|
|
|
|
attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| {
|
|
let place_table = index.place_table(function_scope_id);
|
|
let place = place_table.place_id_by_instance_attribute_name(name)?;
|
|
let use_def = &index.use_def_maps[function_scope_id];
|
|
Some((
|
|
use_def.inner.all_reachable_bindings(place),
|
|
function_scope_id,
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Returns all attribute declarations (and their method scope IDs) with a symbol name matching
|
|
/// the one given for a specific class body scope.
|
|
///
|
|
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
|
/// introduces a direct dependency on that file's AST.
|
|
pub(crate) fn attribute_declarations<'db, 's>(
|
|
db: &'db dyn Db,
|
|
class_body_scope: ScopeId<'db>,
|
|
name: &'s str,
|
|
) -> impl Iterator<Item = (DeclarationsIterator<'db, 'db>, FileScopeId)> + use<'s, 'db> {
|
|
let file = class_body_scope.file(db);
|
|
let index = semantic_index(db, file);
|
|
|
|
attribute_scopes(db, class_body_scope).filter_map(|function_scope_id| {
|
|
let place_table = index.place_table(function_scope_id);
|
|
let place = place_table.place_id_by_instance_attribute_name(name)?;
|
|
let use_def = &index.use_def_maps[function_scope_id];
|
|
Some((
|
|
use_def.inner.all_reachable_declarations(place),
|
|
function_scope_id,
|
|
))
|
|
})
|
|
}
|
|
|
|
/// Returns all attribute assignments as scope IDs for a specific class body scope.
|
|
///
|
|
/// Only call this when doing type inference on the same file as `class_body_scope`, otherwise it
|
|
/// introduces a direct dependency on that file's AST.
|
|
pub(crate) fn attribute_scopes<'db, 's>(
|
|
db: &'db dyn Db,
|
|
class_body_scope: ScopeId<'db>,
|
|
) -> impl Iterator<Item = FileScopeId> + use<'s, 'db> {
|
|
let file = class_body_scope.file(db);
|
|
let module = parsed_module(db, file).load(db);
|
|
let index = semantic_index(db, file);
|
|
let class_scope_id = class_body_scope.file_scope_id(db);
|
|
|
|
ChildrenIter::new(index, class_scope_id).filter_map(move |(child_scope_id, scope)| {
|
|
let (function_scope_id, function_scope) =
|
|
if scope.node().scope_kind() == ScopeKind::Annotation {
|
|
// This could be a generic method with a type-params scope.
|
|
// Go one level deeper to find the function scope. The first
|
|
// descendant is the (potential) function scope.
|
|
let function_scope_id = scope.descendants().start;
|
|
(function_scope_id, index.scope(function_scope_id))
|
|
} else {
|
|
(child_scope_id, scope)
|
|
};
|
|
|
|
function_scope.node().as_function(&module)?;
|
|
Some(function_scope_id)
|
|
})
|
|
}
|
|
|
|
/// Returns the module global scope of `file`.
|
|
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
|
|
pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
|
let _span = tracing::trace_span!("global_scope", ?file).entered();
|
|
|
|
FileScopeId::global().to_scope_id(db, file)
|
|
}
|
|
|
|
pub(crate) enum EagerSnapshotResult<'map, 'db> {
|
|
FoundConstraint(ScopedNarrowingConstraint),
|
|
FoundBindings(BindingWithConstraintsIterator<'map, 'db>),
|
|
NotFound,
|
|
NoLongerInEagerContext,
|
|
}
|
|
|
|
/// The place tables and use-def maps for all scopes in a file.
|
|
#[derive(Debug, Update, get_size2::GetSize)]
|
|
pub(crate) struct SemanticIndex<'db> {
|
|
/// List of all place tables in this file, indexed by scope.
|
|
place_tables: IndexVec<FileScopeId, Arc<PlaceTable>>,
|
|
|
|
/// List of all scopes in this file.
|
|
scopes: IndexVec<FileScopeId, Scope>,
|
|
|
|
/// Map expressions to their corresponding scope.
|
|
scopes_by_expression: ExpressionsScopeMap,
|
|
|
|
/// Map from a node creating a definition to its definition.
|
|
definitions_by_node: FxHashMap<DefinitionNodeKey, Definitions<'db>>,
|
|
|
|
/// Map from a standalone expression to its [`Expression`] ingredient.
|
|
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
|
|
|
/// Map from nodes that create a scope to the scope they create.
|
|
scopes_by_node: FxHashMap<NodeWithScopeKey, FileScopeId>,
|
|
|
|
/// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`].
|
|
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
|
|
|
/// Use-def map for each scope in this file.
|
|
use_def_maps: IndexVec<FileScopeId, ArcUseDefMap<'db>>,
|
|
|
|
/// Lookup table to map between node ids and ast nodes.
|
|
///
|
|
/// Note: We should not depend on this map when analysing other files or
|
|
/// changing a file invalidates all dependents.
|
|
ast_ids: IndexVec<FileScopeId, AstIds>,
|
|
|
|
/// The set of modules that are imported anywhere within this file.
|
|
imported_modules: Arc<FxHashSet<ModuleName>>,
|
|
|
|
/// Flags about the global scope (code usage impacting inference)
|
|
has_future_annotations: bool,
|
|
|
|
/// Map of all of the eager snapshots that appear in this file.
|
|
eager_snapshots: FxHashMap<EagerSnapshotKey, ScopedEagerSnapshotId>,
|
|
|
|
/// List of all semantic syntax errors in this file.
|
|
semantic_syntax_errors: Vec<SemanticSyntaxError>,
|
|
|
|
/// Set of all generator functions in this file.
|
|
generator_functions: FxHashSet<FileScopeId>,
|
|
}
|
|
|
|
impl<'db> SemanticIndex<'db> {
|
|
/// Returns the place table for a specific scope.
|
|
///
|
|
/// Use the Salsa cached [`place_table()`] query if you only need the
|
|
/// place table for a single scope.
|
|
#[track_caller]
|
|
pub(super) fn place_table(&self, scope_id: FileScopeId) -> Arc<PlaceTable> {
|
|
self.place_tables[scope_id].clone()
|
|
}
|
|
|
|
/// Returns the use-def map for a specific scope.
|
|
///
|
|
/// Use the Salsa cached [`use_def_map()`] query if you only need the
|
|
/// use-def map for a single scope.
|
|
#[track_caller]
|
|
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> ArcUseDefMap<'_> {
|
|
self.use_def_maps[scope_id].clone()
|
|
}
|
|
|
|
#[track_caller]
|
|
pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds {
|
|
&self.ast_ids[scope_id]
|
|
}
|
|
|
|
/// Returns the ID of the `expression`'s enclosing scope.
|
|
#[track_caller]
|
|
pub(crate) fn expression_scope_id<E>(&self, expression: &E) -> FileScopeId
|
|
where
|
|
E: HasTrackedScope,
|
|
{
|
|
self.try_expression_scope_id(expression)
|
|
.expect("Expression to be part of a scope if it is from the same module")
|
|
}
|
|
|
|
/// Returns the ID of the `expression`'s enclosing scope.
|
|
pub(crate) fn try_expression_scope_id<E>(&self, expression: &E) -> Option<FileScopeId>
|
|
where
|
|
E: HasTrackedScope,
|
|
{
|
|
self.scopes_by_expression.try_get(expression)
|
|
}
|
|
|
|
/// Returns the [`Scope`] of the `expression`'s enclosing scope.
|
|
#[allow(unused)]
|
|
#[track_caller]
|
|
pub(crate) fn expression_scope(&self, expression: &impl HasTrackedScope) -> &Scope {
|
|
&self.scopes[self.expression_scope_id(expression)]
|
|
}
|
|
|
|
/// Returns the [`Scope`] with the given id.
|
|
#[track_caller]
|
|
pub(crate) fn scope(&self, id: FileScopeId) -> &Scope {
|
|
&self.scopes[id]
|
|
}
|
|
|
|
pub(crate) fn scope_ids(&self) -> impl Iterator<Item = ScopeId> {
|
|
self.scope_ids_by_scope.iter().copied()
|
|
}
|
|
|
|
pub(crate) fn symbol_is_global_in_scope(
|
|
&self,
|
|
symbol: ScopedPlaceId,
|
|
scope: FileScopeId,
|
|
) -> bool {
|
|
self.place_table(scope)
|
|
.place_expr(symbol)
|
|
.is_marked_global()
|
|
}
|
|
|
|
pub(crate) fn symbol_is_nonlocal_in_scope(
|
|
&self,
|
|
symbol: ScopedPlaceId,
|
|
scope: FileScopeId,
|
|
) -> bool {
|
|
self.place_table(scope)
|
|
.place_expr(symbol)
|
|
.is_marked_nonlocal()
|
|
}
|
|
|
|
/// Returns the id of the parent scope.
|
|
pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option<FileScopeId> {
|
|
let scope = self.scope(scope_id);
|
|
scope.parent()
|
|
}
|
|
|
|
/// Returns the parent scope of `scope_id`.
|
|
#[expect(unused)]
|
|
#[track_caller]
|
|
pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> {
|
|
Some(&self.scopes[self.parent_scope_id(scope_id)?])
|
|
}
|
|
|
|
fn is_scope_reachable(&self, db: &'db dyn Db, scope_id: FileScopeId) -> bool {
|
|
self.parent_scope_id(scope_id)
|
|
.is_none_or(|parent_scope_id| {
|
|
if !self.is_scope_reachable(db, parent_scope_id) {
|
|
return false;
|
|
}
|
|
|
|
let parent_use_def = self.use_def_map(parent_scope_id);
|
|
let reachability = self.scope(scope_id).reachability();
|
|
|
|
parent_use_def.is_reachable(db, reachability)
|
|
})
|
|
}
|
|
|
|
/// Returns true if a given AST node is reachable from the start of the scope. For example,
|
|
/// in the following code, expression `2` is reachable, but expressions `1` and `3` are not:
|
|
/// ```py
|
|
/// def f():
|
|
/// x = 1
|
|
/// if False:
|
|
/// x # 1
|
|
/// x # 2
|
|
/// return
|
|
/// x # 3
|
|
/// ```
|
|
pub(crate) fn is_node_reachable(
|
|
&self,
|
|
db: &'db dyn crate::Db,
|
|
scope_id: FileScopeId,
|
|
node_key: NodeKey,
|
|
) -> bool {
|
|
self.is_scope_reachable(db, scope_id)
|
|
&& self.use_def_map(scope_id).is_node_reachable(db, node_key)
|
|
}
|
|
|
|
/// Returns an iterator over the descendent scopes of `scope`.
|
|
#[allow(unused)]
|
|
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendantsIter {
|
|
DescendantsIter::new(self, scope)
|
|
}
|
|
|
|
/// Returns an iterator over the direct child scopes of `scope`.
|
|
#[allow(unused)]
|
|
pub(crate) fn child_scopes(&self, scope: FileScopeId) -> ChildrenIter {
|
|
ChildrenIter::new(self, scope)
|
|
}
|
|
|
|
/// Returns an iterator over all ancestors of `scope`, starting with `scope` itself.
|
|
pub(crate) fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter {
|
|
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`
|
|
/// if the definition is created by a wildcard (`*`) import.
|
|
#[track_caller]
|
|
pub(crate) fn definitions(
|
|
&self,
|
|
definition_key: impl Into<DefinitionNodeKey>,
|
|
) -> &Definitions<'db> {
|
|
&self.definitions_by_node[&definition_key.into()]
|
|
}
|
|
|
|
/// Returns the [`definition::Definition`] salsa ingredient for `definition_key`.
|
|
///
|
|
/// ## Panics
|
|
///
|
|
/// If the number of definitions associated with the key is not exactly 1 and
|
|
/// the `debug_assertions` feature is enabled, this method will panic.
|
|
#[track_caller]
|
|
pub(crate) fn expect_single_definition(
|
|
&self,
|
|
definition_key: impl Into<DefinitionNodeKey> + std::fmt::Debug + Copy,
|
|
) -> Definition<'db> {
|
|
let definitions = self.definitions(definition_key);
|
|
debug_assert_eq!(
|
|
definitions.len(),
|
|
1,
|
|
"Expected exactly one definition to be associated with AST node {definition_key:?} but found {}",
|
|
definitions.len()
|
|
);
|
|
definitions[0]
|
|
}
|
|
|
|
/// Returns the [`Expression`] ingredient for an expression node.
|
|
/// Panics if we have no expression ingredient for that node. We can only call this method for
|
|
/// standalone-inferable expressions, which we call `add_standalone_expression` for in
|
|
/// [`SemanticIndexBuilder`].
|
|
#[track_caller]
|
|
pub(crate) fn expression(
|
|
&self,
|
|
expression_key: impl Into<ExpressionNodeKey>,
|
|
) -> Expression<'db> {
|
|
self.expressions_by_node[&expression_key.into()]
|
|
}
|
|
|
|
pub(crate) fn try_expression(
|
|
&self,
|
|
expression_key: impl Into<ExpressionNodeKey>,
|
|
) -> Option<Expression<'db>> {
|
|
self.expressions_by_node
|
|
.get(&expression_key.into())
|
|
.copied()
|
|
}
|
|
|
|
pub(crate) fn is_standalone_expression(
|
|
&self,
|
|
expression_key: impl Into<ExpressionNodeKey>,
|
|
) -> bool {
|
|
self.expressions_by_node
|
|
.contains_key(&expression_key.into())
|
|
}
|
|
|
|
/// Returns the id of the scope that `node` creates.
|
|
/// This is different from [`definition::Definition::scope`] which
|
|
/// returns the scope in which that definition is defined in.
|
|
#[track_caller]
|
|
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
|
self.scopes_by_node[&node.node_key()]
|
|
}
|
|
|
|
/// Checks if there is an import of `__future__.annotations` in the global scope, which affects
|
|
/// the logic for type inference.
|
|
pub(super) fn has_future_annotations(&self) -> bool {
|
|
self.has_future_annotations
|
|
}
|
|
|
|
/// Returns
|
|
/// * `NoLongerInEagerContext` if the nested scope is no longer in an eager context
|
|
/// (that is, not every scope that will be traversed is eager).
|
|
/// * an iterator of bindings for a particular nested eager scope reference if the bindings exist.
|
|
/// * a narrowing constraint if there are no bindings, but there is a narrowing constraint for an outer scope symbol.
|
|
/// * `NotFound` if the narrowing constraint / bindings do not exist in the nested eager scope.
|
|
pub(crate) fn eager_snapshot(
|
|
&self,
|
|
enclosing_scope: FileScopeId,
|
|
expr: &PlaceExpr,
|
|
nested_scope: FileScopeId,
|
|
) -> EagerSnapshotResult<'_, 'db> {
|
|
for (ancestor_scope_id, ancestor_scope) in self.ancestor_scopes(nested_scope) {
|
|
if ancestor_scope_id == enclosing_scope {
|
|
break;
|
|
}
|
|
if !ancestor_scope.is_eager() {
|
|
return EagerSnapshotResult::NoLongerInEagerContext;
|
|
}
|
|
}
|
|
let Some(place_id) = self.place_tables[enclosing_scope].place_id_by_expr(expr) else {
|
|
return EagerSnapshotResult::NotFound;
|
|
};
|
|
let key = EagerSnapshotKey {
|
|
enclosing_scope,
|
|
enclosing_place: place_id,
|
|
nested_scope,
|
|
};
|
|
let Some(id) = self.eager_snapshots.get(&key) else {
|
|
return EagerSnapshotResult::NotFound;
|
|
};
|
|
self.use_def_maps[enclosing_scope].inner.eager_snapshot(*id)
|
|
}
|
|
|
|
pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] {
|
|
&self.semantic_syntax_errors
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, salsa::Update, get_size2::GetSize)]
|
|
pub(crate) struct ArcUseDefMap<'db> {
|
|
#[get_size(size_fn = untracked_arc_size)]
|
|
inner: Arc<UseDefMap<'db>>,
|
|
}
|
|
|
|
impl<'db> std::ops::Deref for ArcUseDefMap<'db> {
|
|
type Target = UseDefMap<'db>;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.inner
|
|
}
|
|
}
|
|
|
|
impl<'db> ArcUseDefMap<'db> {
|
|
pub(crate) fn new(inner: UseDefMap<'db>) -> Self {
|
|
Self {
|
|
inner: Arc::new(inner),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct AncestorsIter<'a> {
|
|
scopes: &'a IndexSlice<FileScopeId, Scope>,
|
|
next_id: Option<FileScopeId>,
|
|
}
|
|
|
|
impl<'a> AncestorsIter<'a> {
|
|
fn new(module_table: &'a SemanticIndex, start: FileScopeId) -> Self {
|
|
Self {
|
|
scopes: &module_table.scopes,
|
|
next_id: Some(start),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for AncestorsIter<'a> {
|
|
type Item = (FileScopeId, &'a Scope);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let current_id = self.next_id?;
|
|
let current = &self.scopes[current_id];
|
|
self.next_id = current.parent();
|
|
|
|
Some((current_id, current))
|
|
}
|
|
}
|
|
|
|
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>,
|
|
}
|
|
|
|
impl<'a> DescendantsIter<'a> {
|
|
fn new(index: &'a SemanticIndex, scope_id: FileScopeId) -> Self {
|
|
let scope = &index.scopes[scope_id];
|
|
let scopes = &index.scopes[scope.descendants()];
|
|
|
|
Self {
|
|
next_id: scope_id + 1,
|
|
descendants: scopes.iter(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for DescendantsIter<'a> {
|
|
type Item = (FileScopeId, &'a Scope);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
let descendant = self.descendants.next()?;
|
|
let id = self.next_id;
|
|
self.next_id = self.next_id + 1;
|
|
|
|
Some((id, descendant))
|
|
}
|
|
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
self.descendants.size_hint()
|
|
}
|
|
}
|
|
|
|
impl FusedIterator for DescendantsIter<'_> {}
|
|
|
|
impl ExactSizeIterator for DescendantsIter<'_> {}
|
|
|
|
pub struct ChildrenIter<'a> {
|
|
parent: FileScopeId,
|
|
descendants: DescendantsIter<'a>,
|
|
}
|
|
|
|
impl<'a> ChildrenIter<'a> {
|
|
pub(crate) fn new(module_index: &'a SemanticIndex, parent: FileScopeId) -> Self {
|
|
let descendants = DescendantsIter::new(module_index, parent);
|
|
|
|
Self {
|
|
parent,
|
|
descendants,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Iterator for ChildrenIter<'a> {
|
|
type Item = (FileScopeId, &'a Scope);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.descendants
|
|
.find(|(_, scope)| scope.parent() == Some(self.parent))
|
|
}
|
|
}
|
|
|
|
impl FusedIterator for ChildrenIter<'_> {}
|
|
|
|
/// Interval map that maps a range of expression node ids to their corresponding scopes.
|
|
///
|
|
/// Lookups require `O(log n)` time, where `n` is roughly the number of scopes (roughly
|
|
/// because sub-scopes can be interleaved with expressions in the outer scope, e.g. function, some statements, a function).
|
|
#[derive(Eq, PartialEq, Debug, get_size2::GetSize, Default)]
|
|
struct ExpressionsScopeMap(Box<[(std::ops::RangeInclusive<NodeIndex>, FileScopeId)]>);
|
|
|
|
impl ExpressionsScopeMap {
|
|
fn try_get<E>(&self, node: &E) -> Option<FileScopeId>
|
|
where
|
|
E: HasTrackedScope,
|
|
{
|
|
let node_index = node.node_index().load();
|
|
|
|
let entry = self
|
|
.0
|
|
.binary_search_by_key(&node_index, |(range, _)| *range.start());
|
|
|
|
let index = match entry {
|
|
Ok(index) => index,
|
|
Err(index) => index.checked_sub(1)?,
|
|
};
|
|
|
|
let (range, scope) = &self.0[index];
|
|
if range.contains(&node_index) {
|
|
Some(*scope)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ruff_db::files::{File, system_path_to_file};
|
|
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
|
use ruff_python_ast::{self as ast};
|
|
use ruff_text_size::{Ranged, TextRange};
|
|
|
|
use crate::Db;
|
|
use crate::db::tests::{TestDb, TestDbBuilder};
|
|
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
|
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
|
use crate::semantic_index::place::{FileScopeId, PlaceTable, Scope, ScopeKind, ScopedPlaceId};
|
|
use crate::semantic_index::use_def::UseDefMap;
|
|
use crate::semantic_index::{global_scope, place_table, semantic_index, use_def_map};
|
|
|
|
impl UseDefMap<'_> {
|
|
fn first_public_binding(&self, symbol: ScopedPlaceId) -> Option<Definition<'_>> {
|
|
self.end_of_scope_bindings(symbol)
|
|
.find_map(|constrained_binding| constrained_binding.binding.definition())
|
|
}
|
|
|
|
fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
|
|
self.bindings_at_use(use_id)
|
|
.find_map(|constrained_binding| constrained_binding.binding.definition())
|
|
}
|
|
}
|
|
|
|
struct TestCase {
|
|
db: TestDb,
|
|
file: File,
|
|
}
|
|
|
|
fn test_case(content: &str) -> TestCase {
|
|
const FILENAME: &str = "test.py";
|
|
|
|
let db = TestDbBuilder::new()
|
|
.with_file(FILENAME, content)
|
|
.build()
|
|
.unwrap();
|
|
|
|
let file = system_path_to_file(&db, FILENAME).unwrap();
|
|
|
|
TestCase { db, file }
|
|
}
|
|
|
|
fn names(table: &PlaceTable) -> Vec<String> {
|
|
table
|
|
.places()
|
|
.filter_map(|expr| Some(expr.as_name()?.to_string()))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn empty() {
|
|
let TestCase { db, file } = test_case("");
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
let global_names = names(global_table);
|
|
|
|
assert_eq!(global_names, Vec::<&str>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn simple() {
|
|
let TestCase { db, file } = test_case("x");
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert_eq!(names(global_table), vec!["x"]);
|
|
}
|
|
|
|
#[test]
|
|
fn annotation_only() {
|
|
let TestCase { db, file } = test_case("x: int");
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert_eq!(names(global_table), vec!["int", "x"]);
|
|
// TODO record definition
|
|
}
|
|
|
|
#[test]
|
|
fn import() {
|
|
let TestCase { db, file } = test_case("import foo");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(names(global_table), vec!["foo"]);
|
|
let foo = global_table.place_id_by_name("foo").unwrap();
|
|
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def.first_public_binding(foo).unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Import(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn import_sub() {
|
|
let TestCase { db, file } = test_case("import foo.bar");
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert_eq!(names(global_table), vec!["foo"]);
|
|
}
|
|
|
|
#[test]
|
|
fn import_as() {
|
|
let TestCase { db, file } = test_case("import foo.bar as baz");
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert_eq!(names(global_table), vec!["baz"]);
|
|
}
|
|
|
|
#[test]
|
|
fn import_from() {
|
|
let TestCase { db, file } = test_case("from bar import foo");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(names(global_table), vec!["foo"]);
|
|
assert!(
|
|
global_table
|
|
.place_by_name("foo")
|
|
.is_some_and(|symbol| { symbol.is_bound() && !symbol.is_used() }),
|
|
"symbols that are defined get the defined flag"
|
|
);
|
|
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def
|
|
.first_public_binding(
|
|
global_table
|
|
.place_id_by_name("foo")
|
|
.expect("symbol to exist"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::ImportFrom(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn assign() {
|
|
let TestCase { db, file } = test_case("x = foo");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(names(global_table), vec!["foo", "x"]);
|
|
assert!(
|
|
global_table
|
|
.place_by_name("foo")
|
|
.is_some_and(|symbol| { !symbol.is_bound() && symbol.is_used() }),
|
|
"a symbol used but not bound in a scope should have only the used flag"
|
|
);
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name("x").expect("symbol exists"))
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn augmented_assignment() {
|
|
let TestCase { db, file } = test_case("x += 1");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(names(global_table), vec!["x"]);
|
|
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name("x").unwrap())
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
binding.kind(&db),
|
|
DefinitionKind::AugmentedAssignment(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn class_scope() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
class C:
|
|
x = 1
|
|
y = 2
|
|
",
|
|
);
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert_eq!(names(global_table), vec!["C", "y"]);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
|
|
let [(class_scope_id, class_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope")
|
|
};
|
|
assert_eq!(class_scope.kind(), ScopeKind::Class);
|
|
assert_eq!(
|
|
class_scope_id.to_scope_id(&db, file).name(&db, &module),
|
|
"C"
|
|
);
|
|
|
|
let class_table = index.place_table(class_scope_id);
|
|
assert_eq!(names(&class_table), vec!["x"]);
|
|
|
|
let use_def = index.use_def_map(class_scope_id);
|
|
let binding = use_def
|
|
.first_public_binding(class_table.place_id_by_name("x").expect("symbol exists"))
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn function_scope() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
def func():
|
|
x = 1
|
|
y = 2
|
|
",
|
|
);
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["func", "y"]);
|
|
|
|
let [(function_scope_id, function_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope")
|
|
};
|
|
assert_eq!(function_scope.kind(), ScopeKind::Function);
|
|
assert_eq!(
|
|
function_scope_id.to_scope_id(&db, file).name(&db, &module),
|
|
"func"
|
|
);
|
|
|
|
let function_table = index.place_table(function_scope_id);
|
|
assert_eq!(names(&function_table), vec!["x"]);
|
|
|
|
let use_def = index.use_def_map(function_scope_id);
|
|
let binding = use_def
|
|
.first_public_binding(function_table.place_id_by_name("x").expect("symbol exists"))
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn function_parameter_symbols() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
|
pass
|
|
",
|
|
);
|
|
|
|
let index = semantic_index(&db, file);
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert_eq!(names(global_table), vec!["str", "int", "f"]);
|
|
|
|
let [(function_scope_id, _function_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("Expected a function scope")
|
|
};
|
|
|
|
let function_table = index.place_table(function_scope_id);
|
|
assert_eq!(
|
|
names(&function_table),
|
|
vec!["a", "b", "c", "d", "args", "kwargs"],
|
|
);
|
|
|
|
let use_def = index.use_def_map(function_scope_id);
|
|
for name in ["a", "b", "c", "d"] {
|
|
let binding = use_def
|
|
.first_public_binding(
|
|
function_table
|
|
.place_id_by_name(name)
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
|
|
}
|
|
let args_binding = use_def
|
|
.first_public_binding(
|
|
function_table
|
|
.place_id_by_name("args")
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
args_binding.kind(&db),
|
|
DefinitionKind::VariadicPositionalParameter(_)
|
|
));
|
|
let kwargs_binding = use_def
|
|
.first_public_binding(
|
|
function_table
|
|
.place_id_by_name("kwargs")
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
kwargs_binding.kind(&db),
|
|
DefinitionKind::VariadicKeywordParameter(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_parameter_symbols() {
|
|
let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None");
|
|
|
|
let index = semantic_index(&db, file);
|
|
let global_table = place_table(&db, global_scope(&db, file));
|
|
|
|
assert!(names(global_table).is_empty());
|
|
|
|
let [(lambda_scope_id, _lambda_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("Expected a lambda scope")
|
|
};
|
|
|
|
let lambda_table = index.place_table(lambda_scope_id);
|
|
assert_eq!(
|
|
names(&lambda_table),
|
|
vec!["a", "b", "c", "d", "args", "kwargs"],
|
|
);
|
|
|
|
let use_def = index.use_def_map(lambda_scope_id);
|
|
for name in ["a", "b", "c", "d"] {
|
|
let binding = use_def
|
|
.first_public_binding(lambda_table.place_id_by_name(name).expect("symbol exists"))
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
|
|
}
|
|
let args_binding = use_def
|
|
.first_public_binding(
|
|
lambda_table
|
|
.place_id_by_name("args")
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
args_binding.kind(&db),
|
|
DefinitionKind::VariadicPositionalParameter(_)
|
|
));
|
|
let kwargs_binding = use_def
|
|
.first_public_binding(
|
|
lambda_table
|
|
.place_id_by_name("kwargs")
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
kwargs_binding.kind(&db),
|
|
DefinitionKind::VariadicKeywordParameter(_)
|
|
));
|
|
}
|
|
|
|
/// Test case to validate that the comprehension scope is correctly identified and that the target
|
|
/// variable is defined only in the comprehension scope and not in the global scope.
|
|
#[test]
|
|
fn comprehension_scope() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
[x for x, y in iter1]
|
|
",
|
|
);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["iter1"]);
|
|
|
|
let [(comprehension_scope_id, comprehension_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope")
|
|
};
|
|
|
|
assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension);
|
|
assert_eq!(
|
|
comprehension_scope_id
|
|
.to_scope_id(&db, file)
|
|
.name(&db, &module),
|
|
"<listcomp>"
|
|
);
|
|
|
|
let comprehension_symbol_table = index.place_table(comprehension_scope_id);
|
|
|
|
assert_eq!(names(&comprehension_symbol_table), vec!["x", "y"]);
|
|
|
|
let use_def = index.use_def_map(comprehension_scope_id);
|
|
for name in ["x", "y"] {
|
|
let binding = use_def
|
|
.first_public_binding(
|
|
comprehension_symbol_table
|
|
.place_id_by_name(name)
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(
|
|
binding.kind(&db),
|
|
DefinitionKind::Comprehension(_)
|
|
));
|
|
}
|
|
}
|
|
|
|
/// Test case to validate that the `x` variable used in the comprehension is referencing the
|
|
/// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one.
|
|
#[test]
|
|
fn multiple_generators() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
[x for x in iter1 for x in iter2]
|
|
",
|
|
);
|
|
|
|
let index = semantic_index(&db, file);
|
|
let [(comprehension_scope_id, _)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope")
|
|
};
|
|
|
|
let use_def = index.use_def_map(comprehension_scope_id);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let syntax = module.syntax();
|
|
let element = syntax.body[0]
|
|
.as_expr_stmt()
|
|
.unwrap()
|
|
.value
|
|
.as_list_comp_expr()
|
|
.unwrap()
|
|
.elt
|
|
.as_name_expr()
|
|
.unwrap();
|
|
let element_use_id =
|
|
element.scoped_use_id(&db, comprehension_scope_id.to_scope_id(&db, file));
|
|
|
|
let binding = use_def.first_binding_at_use(element_use_id).unwrap();
|
|
let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else {
|
|
panic!("expected generator definition")
|
|
};
|
|
let target = comprehension.target(&module);
|
|
let name = target.as_name_expr().unwrap().id().as_str();
|
|
|
|
assert_eq!(name, "x");
|
|
assert_eq!(target.range(), TextRange::new(23.into(), 24.into()));
|
|
}
|
|
|
|
/// Test case to validate that the nested comprehension creates a new scope which is a child of
|
|
/// the outer comprehension scope and the variables are correctly defined in the respective
|
|
/// scopes.
|
|
#[test]
|
|
fn nested_generators() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
[{x for x in iter2} for y in iter1]
|
|
",
|
|
);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["iter1"]);
|
|
|
|
let [(comprehension_scope_id, comprehension_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope")
|
|
};
|
|
|
|
assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension);
|
|
assert_eq!(
|
|
comprehension_scope_id
|
|
.to_scope_id(&db, file)
|
|
.name(&db, &module),
|
|
"<listcomp>"
|
|
);
|
|
|
|
let comprehension_symbol_table = index.place_table(comprehension_scope_id);
|
|
|
|
assert_eq!(names(&comprehension_symbol_table), vec!["y", "iter2"]);
|
|
|
|
let [(inner_comprehension_scope_id, inner_comprehension_scope)] = index
|
|
.child_scopes(comprehension_scope_id)
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one inner generator scope")
|
|
};
|
|
|
|
assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension);
|
|
assert_eq!(
|
|
inner_comprehension_scope_id
|
|
.to_scope_id(&db, file)
|
|
.name(&db, &module),
|
|
"<setcomp>"
|
|
);
|
|
|
|
let inner_comprehension_symbol_table = index.place_table(inner_comprehension_scope_id);
|
|
|
|
assert_eq!(names(&inner_comprehension_symbol_table), vec!["x"]);
|
|
}
|
|
|
|
#[test]
|
|
fn with_item_definition() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
with item1 as x, item2 as y:
|
|
pass
|
|
",
|
|
);
|
|
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["item1", "x", "item2", "y"]);
|
|
|
|
let use_def = index.use_def_map(FileScopeId::global());
|
|
for name in ["x", "y"] {
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name(name).expect("symbol exists"))
|
|
.expect("Expected with item definition for {name}");
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn with_item_unpacked_definition() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
with context() as (x, y):
|
|
pass
|
|
",
|
|
);
|
|
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["context", "x", "y"]);
|
|
|
|
let use_def = index.use_def_map(FileScopeId::global());
|
|
for name in ["x", "y"] {
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name(name).expect("symbol exists"))
|
|
.expect("Expected with item definition for {name}");
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dupes() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
def func():
|
|
x = 1
|
|
def func():
|
|
y = 2
|
|
",
|
|
);
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["func"]);
|
|
let [
|
|
(func_scope1_id, func_scope_1),
|
|
(func_scope2_id, func_scope_2),
|
|
] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected two child scopes");
|
|
};
|
|
|
|
assert_eq!(func_scope_1.kind(), ScopeKind::Function);
|
|
|
|
assert_eq!(
|
|
func_scope1_id.to_scope_id(&db, file).name(&db, &module),
|
|
"func"
|
|
);
|
|
assert_eq!(func_scope_2.kind(), ScopeKind::Function);
|
|
assert_eq!(
|
|
func_scope2_id.to_scope_id(&db, file).name(&db, &module),
|
|
"func"
|
|
);
|
|
|
|
let func1_table = index.place_table(func_scope1_id);
|
|
let func2_table = index.place_table(func_scope2_id);
|
|
assert_eq!(names(&func1_table), vec!["x"]);
|
|
assert_eq!(names(&func2_table), vec!["y"]);
|
|
|
|
let use_def = index.use_def_map(FileScopeId::global());
|
|
let binding = use_def
|
|
.first_public_binding(
|
|
global_table
|
|
.place_id_by_name("func")
|
|
.expect("symbol exists"),
|
|
)
|
|
.unwrap();
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::Function(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn generic_function() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
def func[T]():
|
|
x = 1
|
|
",
|
|
);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["func"]);
|
|
|
|
let [(ann_scope_id, ann_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope");
|
|
};
|
|
|
|
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
|
|
assert_eq!(
|
|
ann_scope_id.to_scope_id(&db, file).name(&db, &module),
|
|
"func"
|
|
);
|
|
let ann_table = index.place_table(ann_scope_id);
|
|
assert_eq!(names(&ann_table), vec!["T"]);
|
|
|
|
let [(func_scope_id, func_scope)] =
|
|
index.child_scopes(ann_scope_id).collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope");
|
|
};
|
|
assert_eq!(func_scope.kind(), ScopeKind::Function);
|
|
assert_eq!(
|
|
func_scope_id.to_scope_id(&db, file).name(&db, &module),
|
|
"func"
|
|
);
|
|
let func_table = index.place_table(func_scope_id);
|
|
assert_eq!(names(&func_table), vec!["x"]);
|
|
}
|
|
|
|
#[test]
|
|
fn generic_class() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
class C[T]:
|
|
x = 1
|
|
",
|
|
);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
let global_table = index.place_table(FileScopeId::global());
|
|
|
|
assert_eq!(names(&global_table), vec!["C"]);
|
|
|
|
let [(ann_scope_id, ann_scope)] = index
|
|
.child_scopes(FileScopeId::global())
|
|
.collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope");
|
|
};
|
|
|
|
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
|
|
assert_eq!(ann_scope_id.to_scope_id(&db, file).name(&db, &module), "C");
|
|
let ann_table = index.place_table(ann_scope_id);
|
|
assert_eq!(names(&ann_table), vec!["T"]);
|
|
assert!(
|
|
ann_table
|
|
.place_by_name("T")
|
|
.is_some_and(|s| s.is_bound() && !s.is_used()),
|
|
"type parameters are defined by the scope that introduces them"
|
|
);
|
|
|
|
let [(class_scope_id, class_scope)] =
|
|
index.child_scopes(ann_scope_id).collect::<Vec<_>>()[..]
|
|
else {
|
|
panic!("expected one child scope");
|
|
};
|
|
|
|
assert_eq!(class_scope.kind(), ScopeKind::Class);
|
|
assert_eq!(
|
|
class_scope_id.to_scope_id(&db, file).name(&db, &module),
|
|
"C"
|
|
);
|
|
assert_eq!(names(&index.place_table(class_scope_id)), vec!["x"]);
|
|
}
|
|
|
|
#[test]
|
|
fn reachability_trivial() {
|
|
let TestCase { db, file } = test_case("x = 1; x");
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let scope = global_scope(&db, file);
|
|
let ast = module.syntax();
|
|
let ast::Stmt::Expr(ast::StmtExpr {
|
|
value: x_use_expr, ..
|
|
}) = &ast.body[1]
|
|
else {
|
|
panic!("should be an expr")
|
|
};
|
|
let ast::Expr::Name(x_use_expr_name) = x_use_expr.as_ref() else {
|
|
panic!("expected a Name");
|
|
};
|
|
let x_use_id = x_use_expr_name.scoped_use_id(&db, scope);
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def.first_binding_at_use(x_use_id).unwrap();
|
|
let DefinitionKind::Assignment(assignment) = binding.kind(&db) else {
|
|
panic!("should be an assignment definition")
|
|
};
|
|
let ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
|
value: ast::Number::Int(num),
|
|
..
|
|
}) = assignment.value(&module)
|
|
else {
|
|
panic!("should be a number literal")
|
|
};
|
|
assert_eq!(*num, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn expression_scope() {
|
|
let TestCase { db, file } = test_case("x = 1;\ndef test():\n y = 4");
|
|
|
|
let index = semantic_index(&db, file);
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let ast = module.syntax();
|
|
|
|
let x_stmt = ast.body[0].as_assign_stmt().unwrap();
|
|
let x = &x_stmt.targets[0];
|
|
|
|
assert_eq!(index.expression_scope(x).kind(), ScopeKind::Module);
|
|
assert_eq!(index.expression_scope_id(x), FileScopeId::global());
|
|
|
|
let def = ast.body[1].as_function_def_stmt().unwrap();
|
|
let y_stmt = def.body[0].as_assign_stmt().unwrap();
|
|
let y = &y_stmt.targets[0];
|
|
|
|
assert_eq!(index.expression_scope(y).kind(), ScopeKind::Function);
|
|
}
|
|
|
|
#[test]
|
|
fn scope_iterators() {
|
|
fn scope_names<'a, 'db>(
|
|
scopes: impl Iterator<Item = (FileScopeId, &'db Scope)>,
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
module: &'a ParsedModuleRef,
|
|
) -> Vec<&'a str> {
|
|
scopes
|
|
.into_iter()
|
|
.map(|(scope_id, _)| scope_id.to_scope_id(db, file).name(db, module))
|
|
.collect()
|
|
}
|
|
|
|
let TestCase { db, file } = test_case(
|
|
r"
|
|
class Test:
|
|
def foo():
|
|
def bar():
|
|
...
|
|
def baz():
|
|
pass
|
|
|
|
def x():
|
|
pass",
|
|
);
|
|
|
|
let module = parsed_module(&db, file).load(&db);
|
|
let index = semantic_index(&db, file);
|
|
|
|
let descendants = index.descendent_scopes(FileScopeId::global());
|
|
assert_eq!(
|
|
scope_names(descendants, &db, file, &module),
|
|
vec!["Test", "foo", "bar", "baz", "x"]
|
|
);
|
|
|
|
let children = index.child_scopes(FileScopeId::global());
|
|
assert_eq!(scope_names(children, &db, file, &module), vec!["Test", "x"]);
|
|
|
|
let test_class = index.child_scopes(FileScopeId::global()).next().unwrap().0;
|
|
let test_child_scopes = index.child_scopes(test_class);
|
|
assert_eq!(
|
|
scope_names(test_child_scopes, &db, file, &module),
|
|
vec!["foo", "baz"]
|
|
);
|
|
|
|
let bar_scope = index
|
|
.descendent_scopes(FileScopeId::global())
|
|
.nth(2)
|
|
.unwrap()
|
|
.0;
|
|
let ancestors = index.ancestor_scopes(bar_scope);
|
|
|
|
assert_eq!(
|
|
scope_names(ancestors, &db, file, &module),
|
|
vec!["bar", "foo", "Test", "<module>"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn match_stmt() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
match subject:
|
|
case a: ...
|
|
case [b, c, *d]: ...
|
|
case e as f: ...
|
|
case {'x': g, **h}: ...
|
|
case Foo(i, z=j): ...
|
|
case k | l: ...
|
|
case _: ...
|
|
",
|
|
);
|
|
|
|
let global_scope_id = global_scope(&db, file);
|
|
let global_table = place_table(&db, global_scope_id);
|
|
|
|
assert!(global_table.place_by_name("Foo").unwrap().is_used());
|
|
assert_eq!(
|
|
names(global_table),
|
|
vec![
|
|
"subject", "a", "b", "c", "d", "e", "f", "g", "h", "Foo", "i", "j", "k", "l"
|
|
]
|
|
);
|
|
|
|
let use_def = use_def_map(&db, global_scope_id);
|
|
for (name, expected_index) in [
|
|
("a", 0),
|
|
("b", 0),
|
|
("c", 1),
|
|
("d", 2),
|
|
("e", 0),
|
|
("f", 1),
|
|
("g", 0),
|
|
("h", 1),
|
|
("i", 0),
|
|
("j", 1),
|
|
("k", 0),
|
|
("l", 1),
|
|
] {
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name(name).expect("symbol exists"))
|
|
.expect("Expected with item definition for {name}");
|
|
if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) {
|
|
assert_eq!(pattern.index(), expected_index);
|
|
} else {
|
|
panic!("Expected match pattern definition for {name}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn nested_match_case() {
|
|
let TestCase { db, file } = test_case(
|
|
"
|
|
match 1:
|
|
case first:
|
|
match 2:
|
|
case second:
|
|
pass
|
|
",
|
|
);
|
|
|
|
let global_scope_id = global_scope(&db, file);
|
|
let global_table = place_table(&db, global_scope_id);
|
|
|
|
assert_eq!(names(global_table), vec!["first", "second"]);
|
|
|
|
let use_def = use_def_map(&db, global_scope_id);
|
|
for (name, expected_index) in [("first", 0), ("second", 0)] {
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name(name).expect("symbol exists"))
|
|
.expect("Expected with item definition for {name}");
|
|
if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) {
|
|
assert_eq!(pattern.index(), expected_index);
|
|
} else {
|
|
panic!("Expected match pattern definition for {name}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn for_loops_single_assignment() {
|
|
let TestCase { db, file } = test_case("for x in a: pass");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(&names(global_table), &["a", "x"]);
|
|
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name("x").unwrap())
|
|
.unwrap();
|
|
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn for_loops_simple_unpacking() {
|
|
let TestCase { db, file } = test_case("for (x, y) in a: pass");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(&names(global_table), &["a", "x", "y"]);
|
|
|
|
let use_def = use_def_map(&db, scope);
|
|
let x_binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name("x").unwrap())
|
|
.unwrap();
|
|
let y_binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name("y").unwrap())
|
|
.unwrap();
|
|
|
|
assert!(matches!(x_binding.kind(&db), DefinitionKind::For(_)));
|
|
assert!(matches!(y_binding.kind(&db), DefinitionKind::For(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn for_loops_complex_unpacking() {
|
|
let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass");
|
|
let scope = global_scope(&db, file);
|
|
let global_table = place_table(&db, scope);
|
|
|
|
assert_eq!(&names(global_table), &["e", "a", "b", "c", "d"]);
|
|
|
|
let use_def = use_def_map(&db, scope);
|
|
let binding = use_def
|
|
.first_public_binding(global_table.place_id_by_name("a").unwrap())
|
|
.unwrap();
|
|
|
|
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
|
|
}
|
|
}
|