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_parser::semantic_errors::SemanticSyntaxError; use rustc_hash::{FxBuildHasher, 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::symbol::{ FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId, SymbolTable, }; use crate::semantic_index::use_def::{EagerSnapshotKey, ScopedEagerSnapshotId, UseDefMap}; pub mod ast_ids; mod builder; pub mod definition; pub mod expression; pub(crate) mod narrowing_constraints; pub(crate) mod predicate; mod re_exports; pub mod symbol; mod use_def; mod visibility_constraints; pub(crate) use self::use_def::{ BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint, DeclarationsIterator, }; type SymbolMap = hashbrown::HashMap; /// Returns the semantic index for `file`. /// /// Prefer using [`symbol_table`] when working with symbols from a single scope. #[salsa::tracked(returns(ref), no_eq)] pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { let _span = tracing::trace_span!("semantic_index", ?file).entered(); let parsed = parsed_module(db.upcast(), file); SemanticIndexBuilder::new(db, file, parsed).build() } /// Returns the symbol table for a specific `scope`. /// /// Using [`symbol_table`] over [`semantic_index`] has the advantage that /// Salsa can avoid invalidating dependent queries if this scope's symbol table /// is unchanged. #[salsa::tracked(returns(deref))] pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc { let file = scope.file(db); let _span = tracing::trace_span!("symbol_table", scope=?scope.as_id(), ?file).entered(); let index = semantic_index(db, file); index.symbol_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))] pub(crate) fn imported_modules<'db>(db: &'db dyn Db, file: File) -> Arc> { 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))] pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc> { 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, 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 attribute_table = index.instance_attribute_table(function_scope_id); let symbol = attribute_table.symbol_id_by_name(name)?; let use_def = &index.use_def_maps[function_scope_id]; Some(( use_def.instance_attribute_bindings(symbol), 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 + use<'s, 'db> { let file = class_body_scope.file(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(|(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()?; Some(function_scope_id) }) } /// Returns the module global scope of `file`. #[salsa::tracked] 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 symbol tables and use-def maps for all scopes in a file. #[derive(Debug, Update)] pub(crate) struct SemanticIndex<'db> { /// List of all symbol tables in this file, indexed by scope. symbol_tables: IndexVec>, /// List of all instance attribute tables in this file, indexed by scope. instance_attribute_tables: IndexVec, /// List of all scopes in this file. scopes: IndexVec, /// Map expressions to their corresponding scope. scopes_by_expression: FxHashMap, /// Map from a node creating a definition to its definition. definitions_by_node: FxHashMap>, /// Map from a standalone expression to its [`Expression`] ingredient. expressions_by_node: FxHashMap>, /// Map from nodes that create a scope to the scope they create. scopes_by_node: FxHashMap, /// Map from the file-local [`FileScopeId`] to the salsa-ingredient [`ScopeId`]. scope_ids_by_scope: IndexVec>, /// Map from the file-local [`FileScopeId`] to the set of explicit-global symbols it contains. globals_by_scope: FxHashMap>, /// Use-def map for each scope in this file. use_def_maps: IndexVec>>, /// 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, /// The set of modules that are imported anywhere within this file. imported_modules: Arc>, /// 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, /// List of all semantic syntax errors in this file. semantic_syntax_errors: Vec, /// Set of all generator functions in this file. generator_functions: FxHashSet, } impl<'db> SemanticIndex<'db> { /// Returns the symbol table for a specific scope. /// /// Use the Salsa cached [`symbol_table()`] query if you only need the /// symbol table for a single scope. #[track_caller] pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc { self.symbol_tables[scope_id].clone() } pub(super) fn instance_attribute_table(&self, scope_id: FileScopeId) -> &SymbolTable { &self.instance_attribute_tables[scope_id] } /// 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) -> Arc { 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( &self, expression: impl Into, ) -> FileScopeId { self.scopes_by_expression[&expression.into()] } /// Returns the [`Scope`] of the `expression`'s enclosing scope. #[allow(unused)] #[track_caller] pub(crate) fn expression_scope(&self, expression: impl Into) -> &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 { self.scope_ids_by_scope.iter().copied() } pub(crate) fn symbol_is_global_in_scope( &self, symbol: ScopedSymbolId, scope: FileScopeId, ) -> bool { self.globals_by_scope .get(&scope) .is_some_and(|globals| globals.contains(&symbol)) } /// Returns the id of the parent scope. pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option { 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 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, ) -> &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 + 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, ) -> Expression<'db> { self.expressions_by_node[&expression_key.into()] } pub(crate) fn try_expression( &self, expression_key: impl Into, ) -> Option> { self.expressions_by_node .get(&expression_key.into()) .copied() } pub(crate) fn is_standalone_expression( &self, expression_key: impl Into, ) -> 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, symbol: &str, 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(symbol_id) = self.symbol_tables[enclosing_scope].symbol_id_by_name(symbol) else { return EagerSnapshotResult::NotFound; }; let key = EagerSnapshotKey { enclosing_scope, enclosing_symbol: symbol_id, nested_scope, }; let Some(id) = self.eager_snapshots.get(&key) else { return EagerSnapshotResult::NotFound; }; self.use_def_maps[enclosing_scope].eager_snapshot(*id) } pub(crate) fn semantic_syntax_errors(&self) -> &[SemanticSyntaxError] { &self.semantic_syntax_errors } } pub struct AncestorsIter<'a> { scopes: &'a IndexSlice, next_id: Option, } impl<'a> AncestorsIter<'a> { fn new(module_symbol_table: &'a SemanticIndex, start: FileScopeId) -> Self { Self { scopes: &module_symbol_table.scopes, next_id: Some(start), } } } impl<'a> Iterator for AncestorsIter<'a> { type Item = (FileScopeId, &'a Scope); fn next(&mut self) -> Option { 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 DescendantsIter<'a> { next_id: FileScopeId, descendants: std::slice::Iter<'a, Scope>, } impl<'a> DescendantsIter<'a> { fn new(symbol_table: &'a SemanticIndex, scope_id: FileScopeId) -> Self { let scope = &symbol_table.scopes[scope_id]; let scopes = &symbol_table.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 { 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) { 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_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self { let descendants = DescendantsIter::new(module_symbol_table, parent); Self { parent, descendants, } } } impl<'a> Iterator for ChildrenIter<'a> { type Item = (FileScopeId, &'a Scope); fn next(&mut self) -> Option { self.descendants .find(|(_, scope)| scope.parent() == Some(self.parent)) } } impl FusedIterator for ChildrenIter<'_> {} #[cfg(test)] mod tests { use ruff_db::files::{File, system_path_to_file}; use ruff_db::parsed::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::symbol::{ FileScopeId, Scope, ScopeKind, ScopedSymbolId, SymbolTable, }; use crate::semantic_index::use_def::UseDefMap; use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map}; impl UseDefMap<'_> { fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option> { self.public_bindings(symbol) .find_map(|constrained_binding| constrained_binding.binding) } fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option> { self.bindings_at_use(use_id) .find_map(|constrained_binding| constrained_binding.binding) } } 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: &SymbolTable) -> Vec { table .symbols() .map(|symbol| symbol.name().to_string()) .collect() } #[test] fn empty() { let TestCase { db, file } = test_case(""); let global_table = symbol_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 = symbol_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 = symbol_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 = symbol_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); let foo = global_table.symbol_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 = symbol_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 = symbol_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 = symbol_table(&db, scope); assert_eq!(names(global_table), vec!["foo"]); assert!( global_table .symbol_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 .symbol_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 = symbol_table(&db, scope); assert_eq!(names(global_table), vec!["foo", "x"]); assert!( global_table .symbol_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.symbol_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 = symbol_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.symbol_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 = symbol_table(&db, global_scope(&db, file)); assert_eq!(names(global_table), vec!["C", "y"]); let index = semantic_index(&db, file); let [(class_scope_id, class_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] 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), "C"); let class_table = index.symbol_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.symbol_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 index = semantic_index(&db, file); let global_table = index.symbol_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["func", "y"]); let [(function_scope_id, function_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] 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), "func"); let function_table = index.symbol_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 .symbol_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 = symbol_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::>()[..] else { panic!("Expected a function scope") }; let function_table = index.symbol_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 .symbol_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 .symbol_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 .symbol_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 = symbol_table(&db, global_scope(&db, file)); assert!(names(global_table).is_empty()); let [(lambda_scope_id, _lambda_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] else { panic!("Expected a lambda scope") }; let lambda_table = index.symbol_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.symbol_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 .symbol_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 .symbol_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 index = semantic_index(&db, file); let global_table = index.symbol_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["iter1"]); let [(comprehension_scope_id, comprehension_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] 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), "" ); let comprehension_symbol_table = index.symbol_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 .symbol_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::>()[..] else { panic!("expected one child scope") }; let use_def = index.use_def_map(comprehension_scope_id); let module = parsed_module(&db, file).syntax(); let element = module.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(); 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 index = semantic_index(&db, file); let global_table = index.symbol_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["iter1"]); let [(comprehension_scope_id, comprehension_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] 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), "" ); let comprehension_symbol_table = index.symbol_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::>()[..] 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), "" ); let inner_comprehension_symbol_table = index.symbol_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.symbol_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.symbol_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.symbol_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.symbol_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 index = semantic_index(&db, file); let global_table = index.symbol_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::>()[..] 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), "func"); assert_eq!(func_scope_2.kind(), ScopeKind::Function); assert_eq!(func_scope2_id.to_scope_id(&db, file).name(&db), "func"); let func1_table = index.symbol_table(func_scope1_id); let func2_table = index.symbol_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 .symbol_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 index = semantic_index(&db, file); let global_table = index.symbol_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["func"]); let [(ann_scope_id, ann_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] 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), "func"); let ann_table = index.symbol_table(ann_scope_id); assert_eq!(names(&ann_table), vec!["T"]); let [(func_scope_id, func_scope)] = index.child_scopes(ann_scope_id).collect::>()[..] 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), "func"); let func_table = index.symbol_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 index = semantic_index(&db, file); let global_table = index.symbol_table(FileScopeId::global()); assert_eq!(names(&global_table), vec!["C"]); let [(ann_scope_id, ann_scope)] = index .child_scopes(FileScopeId::global()) .collect::>()[..] 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), "C"); let ann_table = index.symbol_table(ann_scope_id); assert_eq!(names(&ann_table), vec!["T"]); assert!( ann_table .symbol_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::>()[..] 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), "C"); assert_eq!(names(&index.symbol_table(class_scope_id)), vec!["x"]); } #[test] fn reachability_trivial() { let TestCase { db, file } = test_case("x = 1; x"); let parsed = parsed_module(&db, file); let scope = global_scope(&db, file); let ast = parsed.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() 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 parsed = parsed_module(&db, file); let ast = parsed.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>( scopes: impl Iterator, db: &'a dyn Db, file: File, ) -> Vec<&'a str> { scopes .into_iter() .map(|(scope_id, _)| scope_id.to_scope_id(db, file).name(db)) .collect() } let TestCase { db, file } = test_case( r" class Test: def foo(): def bar(): ... def baz(): pass def x(): pass", ); let index = semantic_index(&db, file); let descendants = index.descendent_scopes(FileScopeId::global()); assert_eq!( scope_names(descendants, &db, file), vec!["Test", "foo", "bar", "baz", "x"] ); let children = index.child_scopes(FileScopeId::global()); assert_eq!(scope_names(children, &db, file), 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), 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), vec!["bar", "foo", "Test", ""] ); } #[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 = symbol_table(&db, global_scope_id); assert!(global_table.symbol_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.symbol_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 = symbol_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.symbol_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 = symbol_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.symbol_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 = symbol_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.symbol_id_by_name("x").unwrap()) .unwrap(); let y_binding = use_def .first_public_binding(global_table.symbol_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 = symbol_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.symbol_id_by_name("a").unwrap()) .unwrap(); assert!(matches!(binding.kind(&db), DefinitionKind::For(_))); } }