diff --git a/crates/ruff/src/checkers/ast/deferred.rs b/crates/ruff/src/checkers/ast/deferred.rs index 44d607fbc3..61f2135f9f 100644 --- a/crates/ruff/src/checkers/ast/deferred.rs +++ b/crates/ruff/src/checkers/ast/deferred.rs @@ -1,14 +1,14 @@ -use ruff_python_semantic::scope::ScopeStack; use ruff_text_size::TextRange; use rustpython_parser::ast::{Expr, Stmt}; use ruff_python_ast::types::RefEquality; use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope}; +use ruff_python_semantic::scope::ScopeId; use crate::checkers::ast::AnnotationContext; use crate::docstrings::definition::Definition; -type Context<'a> = (ScopeStack, Vec>); +type Context<'a> = (ScopeId, Vec>); /// A collection of AST nodes that are deferred for later analysis. /// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index 0a20f2a566..8594d58dd8 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -1,4 +1,3 @@ -use std::iter; use std::path::Path; use itertools::Itertools; @@ -26,9 +25,7 @@ use ruff_python_semantic::binding::{ Importation, StarImportation, SubmoduleImportation, }; use ruff_python_semantic::context::Context; -use ruff_python_semantic::scope::{ - ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind, ScopeStack, -}; +use ruff_python_semantic::scope::{ClassDef, FunctionDef, Lambda, Scope, ScopeId, ScopeKind}; use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS}; use ruff_python_stdlib::path::is_python_stub_file; @@ -211,8 +208,7 @@ where &stmt.node, StmtKind::Import { .. } | StmtKind::ImportFrom { .. } ) { - let scope_index = self.ctx.scope_id(); - if scope_index.is_global() && self.ctx.current_stmt_parent().is_none() { + if self.ctx.scope_id.is_global() && self.ctx.current_stmt_parent().is_none() { self.importer.visit_import(stmt); } } @@ -220,14 +216,13 @@ where // Pre-visit. match &stmt.node { StmtKind::Global { names } => { - let scope_index = self.ctx.scope_id(); let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !scope_index.is_global() { + if !self.ctx.scope_id.is_global() { // Add the binding to the current scope. let context = self.ctx.execution_context(); let exceptions = self.ctx.exceptions(); - let scope = &mut self.ctx.scopes[scope_index]; - let usage = Some((scope.id, stmt.range())); + let scope = &mut self.ctx.scopes[self.ctx.scope_id]; + let usage = Some((self.ctx.scope_id, stmt.range())); for (name, range) in names.iter().zip(ranges.iter()) { let id = self.ctx.bindings.push(Binding { kind: BindingKind::Global, @@ -251,13 +246,12 @@ where } } StmtKind::Nonlocal { names } => { - let scope_index = self.ctx.scope_id(); let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); - if !scope_index.is_global() { + if !self.ctx.scope_id.is_global() { let context = self.ctx.execution_context(); let exceptions = self.ctx.exceptions(); - let scope = &mut self.ctx.scopes[scope_index]; - let usage = Some((scope.id, stmt.range())); + let scope = &mut self.ctx.scopes[self.ctx.scope_id]; + let usage = Some((self.ctx.scope_id, stmt.range())); for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. let id = self.ctx.bindings.push(Binding { @@ -276,20 +270,18 @@ where // Mark the binding in the defining scopes as used too. (Skip the global scope // and the current scope.) for (name, range) in names.iter().zip(ranges.iter()) { - let mut exists = false; - let mut scopes_iter = self.ctx.scope_stack.iter(); - // Skip the global scope - scopes_iter.next_back(); + let binding_id = self + .ctx + .scopes + .ancestors(self.ctx.scope_id) + .skip(1) + .take_while(|scope| !scope.kind.is_module()) + .find_map(|scope| scope.get(name.as_str())); - for index in scopes_iter.skip(1) { - if let Some(index) = self.ctx.scopes[*index].get(name.as_str()) { - exists = true; - self.ctx.bindings[*index].runtime_usage = usage; - } - } - - // Ensure that every nonlocal has an existing binding from a parent scope. - if !exists { + if let Some(binding_id) = binding_id { + self.ctx.bindings[*binding_id].runtime_usage = usage; + } else { + // Ensure that every nonlocal has an existing binding from a parent scope. if self.settings.rules.enabled(Rule::NonlocalWithoutBinding) { self.diagnostics.push(Diagnostic::new( pylint::rules::NonlocalWithoutBinding { @@ -909,7 +901,7 @@ where kind: BindingKind::FutureImportation, runtime_usage: None, // Always mark `__future__` imports as used. - synthetic_usage: Some((self.ctx.scope_id(), alias.range())), + synthetic_usage: Some((self.ctx.scope_id, alias.range())), typing_usage: None, range: alias.range(), source: Some(*self.ctx.current_stmt()), @@ -964,7 +956,7 @@ where kind: BindingKind::Importation(Importation { name, full_name }), runtime_usage: None, synthetic_usage: if is_explicit_reexport { - Some((self.ctx.scope_id(), alias.range())) + Some((self.ctx.scope_id, alias.range())) } else { None }, @@ -1220,7 +1212,7 @@ where kind: BindingKind::FutureImportation, runtime_usage: None, // Always mark `__future__` imports as used. - synthetic_usage: Some((self.ctx.scope_id(), alias.range())), + synthetic_usage: Some((self.ctx.scope_id, alias.range())), typing_usage: None, range: alias.range(), source: Some(*self.ctx.current_stmt()), @@ -1313,7 +1305,7 @@ where }), runtime_usage: None, synthetic_usage: if is_explicit_reexport { - Some((self.ctx.scope_id(), alias.range())) + Some((self.ctx.scope_id, alias.range())) } else { None }, @@ -1702,10 +1694,9 @@ where .. } => { if self.settings.rules.enabled(Rule::UnusedLoopControlVariable) { - self.deferred.for_loops.push(( - stmt, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), - )); + self.deferred + .for_loops + .push((stmt, (self.ctx.scope_id, self.ctx.parents.clone()))); } if self .settings @@ -1986,7 +1977,7 @@ where self.deferred.definitions.push(( definition, scope.visibility, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); self.ctx.visible_scope = scope; @@ -2024,7 +2015,7 @@ where self.deferred.functions.push(( stmt, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), self.ctx.visible_scope, )); } @@ -2049,7 +2040,7 @@ where self.deferred.definitions.push(( definition, scope.visibility, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); self.ctx.visible_scope = scope; @@ -2264,13 +2255,13 @@ where expr.range(), value, (self.ctx.in_annotation, self.ctx.in_type_checking_block), - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); } else { self.deferred.type_definitions.push(( expr, (self.ctx.in_annotation, self.ctx.in_type_checking_block), - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); } return; @@ -3498,7 +3489,7 @@ where expr.range(), value, (self.ctx.in_annotation, self.ctx.in_type_checking_block), - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); } if self @@ -3619,10 +3610,9 @@ where // Recurse. match &expr.node { ExprKind::Lambda { .. } => { - self.deferred.lambdas.push(( - expr, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), - )); + self.deferred + .lambdas + .push((expr, (self.ctx.scope_id, self.ctx.parents.clone()))); } ExprKind::IfExp { test, body, orelse } => { visit_boolean_test!(self, test); @@ -4183,18 +4173,16 @@ where impl<'a> Checker<'a> { fn add_binding(&mut self, name: &'a str, binding: Binding<'a>) { let binding_id = self.ctx.bindings.next_id(); - if let Some((stack_index, existing_binding_index)) = self + if let Some((stack_index, existing_binding_id)) = self .ctx - .scope_stack - .iter() + .scopes + .ancestors(self.ctx.scope_id) .enumerate() - .find_map(|(stack_index, scope_index)| { - self.ctx.scopes[*scope_index] - .get(name) - .map(|binding_id| (stack_index, *binding_id)) + .find_map(|(stack_index, scope)| { + scope.get(name).map(|binding_id| (stack_index, *binding_id)) }) { - let existing = &self.ctx.bindings[existing_binding_index]; + let existing = &self.ctx.bindings[existing_binding_id]; let in_current_scope = stack_index == 0; if !existing.kind.is_builtin() && existing.source.map_or(true, |left| { @@ -4271,7 +4259,7 @@ impl<'a> Checker<'a> { } else if existing_is_import && binding.redefines(existing) { self.ctx .shadowed_bindings - .entry(existing_binding_index) + .entry(existing_binding_id) .or_insert_with(Vec::new) .push(binding_id); } @@ -4319,8 +4307,7 @@ impl<'a> Checker<'a> { } fn bind_builtins(&mut self) { - let scope = - &mut self.ctx.scopes[self.ctx.scope_stack.top().expect("No current scope found")]; + let scope = &mut self.ctx.scopes[self.ctx.scope_id]; for builtin in BUILTINS .iter() @@ -4346,16 +4333,13 @@ impl<'a> Checker<'a> { let ExprKind::Name { id, .. } = &expr.node else { return; }; - let scope_id = self.ctx.scope_id(); let mut first_iter = true; let mut in_generator = false; let mut import_starred = false; - for scope_index in self.ctx.scope_stack.iter() { - let scope = &self.ctx.scopes[*scope_index]; - - if matches!(scope.kind, ScopeKind::Class(_)) { + for scope in self.ctx.scopes.ancestors(self.ctx.scope_id) { + if scope.kind.is_class() { if id == "__class__" { return; } else if !first_iter && !in_generator { @@ -4366,7 +4350,7 @@ impl<'a> Checker<'a> { if let Some(index) = scope.get(id.as_str()) { // Mark the binding as used. let context = self.ctx.execution_context(); - self.ctx.bindings[*index].mark_used(scope_id, expr.range(), context); + self.ctx.bindings[*index].mark_used(self.ctx.scope_id, expr.range(), context); if self.ctx.bindings[*index].kind.is_annotation() && self.ctx.in_deferred_string_type_definition.is_none() @@ -4396,7 +4380,7 @@ impl<'a> Checker<'a> { // Mark the sub-importation as used. if let Some(index) = scope.get(full_name) { self.ctx.bindings[*index].mark_used( - scope_id, + self.ctx.scope_id, expr.range(), context, ); @@ -4413,7 +4397,7 @@ impl<'a> Checker<'a> { // Mark the sub-importation as used. if let Some(index) = scope.get(full_name.as_str()) { self.ctx.bindings[*index].mark_used( - scope_id, + self.ctx.scope_id, expr.range(), context, ); @@ -4494,18 +4478,7 @@ impl<'a> Checker<'a> { let parent = self.ctx.current_stmt().0; if self.settings.rules.enabled(Rule::UndefinedLocal) { - let scopes: Vec<&Scope> = self - .ctx - .scope_stack - .iter() - .rev() - .map(|index| &self.ctx.scopes[*index]) - .collect(); - if let Some(diagnostic) = - pyflakes::rules::undefined_local(id, &scopes, &self.ctx.bindings) - { - self.diagnostics.push(diagnostic); - } + pyflakes::rules::undefined_local(self, id); } if self @@ -4743,7 +4716,7 @@ impl<'a> Checker<'a> { docstring, }, self.ctx.visible_scope.visibility, - (self.ctx.scope_stack.clone(), self.ctx.parents.clone()), + (self.ctx.scope_id, self.ctx.parents.clone()), )); docstring.is_some() } @@ -4751,10 +4724,10 @@ impl<'a> Checker<'a> { fn check_deferred_type_definitions(&mut self) { while !self.deferred.type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.type_definitions); - for (expr, (in_annotation, in_type_checking_block), (scopes, parents)) in + for (expr, (in_annotation, in_type_checking_block), (scope_id, parents)) in type_definitions { - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; self.ctx.in_annotation = in_annotation; self.ctx.in_type_checking_block = in_type_checking_block; @@ -4770,7 +4743,7 @@ impl<'a> Checker<'a> { fn check_deferred_string_type_definitions(&mut self, allocator: &'a typed_arena::Arena) { while !self.deferred.string_type_definitions.is_empty() { let type_definitions = std::mem::take(&mut self.deferred.string_type_definitions); - for (range, value, (in_annotation, in_type_checking_block), (scopes, parents)) in + for (range, value, (in_annotation, in_type_checking_block), (scope_id, parents)) in type_definitions { if let Ok((expr, kind)) = parse_type_annotation(value, range, self.locator) { @@ -4782,7 +4755,7 @@ impl<'a> Checker<'a> { let expr = allocator.alloc(expr); - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; self.ctx.in_annotation = in_annotation; self.ctx.in_type_checking_block = in_type_checking_block; @@ -4812,10 +4785,9 @@ impl<'a> Checker<'a> { fn check_deferred_functions(&mut self) { while !self.deferred.functions.is_empty() { let deferred_functions = std::mem::take(&mut self.deferred.functions); - for (stmt, (scopes, parents), visibility) in deferred_functions { - let scope_snapshot = scopes.snapshot(); + for (stmt, (scope_id, parents), visibility) in deferred_functions { let parents_snapshot = parents.len(); - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; self.ctx.visible_scope = visibility; @@ -4830,13 +4802,10 @@ impl<'a> Checker<'a> { } } - let mut scopes = std::mem::take(&mut self.ctx.scope_stack); - scopes.restore(scope_snapshot); - let mut parents = std::mem::take(&mut self.ctx.parents); parents.truncate(parents_snapshot); - self.deferred.assignments.push((scopes, parents)); + self.deferred.assignments.push((scope_id, parents)); } } } @@ -4844,11 +4813,10 @@ impl<'a> Checker<'a> { fn check_deferred_lambdas(&mut self) { while !self.deferred.lambdas.is_empty() { let lambdas = std::mem::take(&mut self.deferred.lambdas); - for (expr, (scopes, parents)) in lambdas { - let scope_snapshot = scopes.snapshot(); + for (expr, (scope_id, parents)) in lambdas { let parents_snapshot = parents.len(); - self.ctx.scope_stack = scopes; + self.ctx.scope_id = scope_id; self.ctx.parents = parents; if let ExprKind::Lambda { args, body } = &expr.node { @@ -4858,12 +4826,9 @@ impl<'a> Checker<'a> { unreachable!("Expected ExprKind::Lambda"); } - let mut scopes = std::mem::take(&mut self.ctx.scope_stack); - scopes.restore(scope_snapshot); - let mut parents = std::mem::take(&mut self.ctx.parents); parents.truncate(parents_snapshot); - self.deferred.assignments.push((scopes, parents)); + self.deferred.assignments.push((scope_id, parents)); } } } @@ -4871,17 +4836,13 @@ impl<'a> Checker<'a> { fn check_deferred_assignments(&mut self) { while !self.deferred.assignments.is_empty() { let assignments = std::mem::take(&mut self.deferred.assignments); - for (scopes, ..) in assignments { - let mut scopes_iter = scopes.iter(); - let scope_index = *scopes_iter.next().unwrap(); - let parent_scope_index = *scopes_iter.next().unwrap(); - + for (scope_id, ..) in assignments { // pyflakes if self.settings.rules.enabled(Rule::UnusedVariable) { - pyflakes::rules::unused_variable(self, scope_index); + pyflakes::rules::unused_variable(self, scope_id); } if self.settings.rules.enabled(Rule::UnusedAnnotation) { - pyflakes::rules::unused_annotation(self, scope_index); + pyflakes::rules::unused_annotation(self, scope_id); } if !self.is_stub { @@ -4893,11 +4854,13 @@ impl<'a> Checker<'a> { Rule::UnusedStaticMethodArgument, Rule::UnusedLambdaArgument, ]) { + let scope = &self.ctx.scopes[scope_id]; + let parent = &self.ctx.scopes[scope.parent.unwrap()]; self.diagnostics .extend(flake8_unused_arguments::rules::unused_arguments( self, - &self.ctx.scopes[parent_scope_index], - &self.ctx.scopes[scope_index], + parent, + scope, &self.ctx.bindings, )); } @@ -4910,8 +4873,8 @@ impl<'a> Checker<'a> { while !self.deferred.for_loops.is_empty() { let for_loops = std::mem::take(&mut self.deferred.for_loops); - for (stmt, (scopes, parents)) in for_loops { - self.ctx.scope_stack = scopes; + for (stmt, (scope_id, parents)) in for_loops { + self.ctx.scope_id = scope_id; self.ctx.parents = parents; if let StmtKind::For { target, body, .. } @@ -5019,10 +4982,10 @@ impl<'a> Checker<'a> { }; let mut diagnostics: Vec = vec![]; - for (index, stack) in self.ctx.dead_scopes.iter().rev() { - let scope = &self.ctx.scopes[*index]; + for scope_id in self.ctx.dead_scopes.iter().rev() { + let scope = &self.ctx.scopes[*scope_id]; - if index.is_global() { + if scope.kind.is_module() { // F822 if self.settings.rules.enabled(Rule::UndefinedExport) { if !self.path.ends_with("__init__.py") { @@ -5148,11 +5111,10 @@ impl<'a> Checker<'a> { let runtime_imports: Vec<&Binding> = if self.settings.flake8_type_checking.strict { vec![] } else { - stack - .iter() - .rev() - .chain(iter::once(index)) - .flat_map(|index| runtime_imports[usize::from(*index)].iter()) + self.ctx + .scopes + .ancestor_ids(*scope_id) + .flat_map(|scope_id| runtime_imports[usize::from(scope_id)].iter()) .copied() .collect() }; @@ -5405,8 +5367,8 @@ impl<'a> Checker<'a> { let mut overloaded_name: Option = None; while !self.deferred.definitions.is_empty() { let definitions = std::mem::take(&mut self.deferred.definitions); - for (definition, visibility, (scopes, parents)) in definitions { - self.ctx.scope_stack = scopes; + for (definition, visibility, (scope_id, parents)) in definitions { + self.ctx.scope_id = scope_id; self.ctx.parents = parents; // flake8-annotations @@ -5677,8 +5639,8 @@ pub fn check_ast( checker.check_definitions(); // Reset the scope to module-level, and check all consumed scopes. - checker.ctx.scope_stack = ScopeStack::default(); - checker.ctx.pop_scope(); + checker.ctx.scope_id = ScopeId::global(); + checker.ctx.dead_scopes.push(ScopeId::global()); checker.check_dead_scopes(); checker.diagnostics diff --git a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs index 43f11928bf..262c7163de 100644 --- a/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs +++ b/crates/ruff/src/rules/pyflakes/rules/return_outside_function.rs @@ -17,14 +17,12 @@ impl Violation for ReturnOutsideFunction { } pub fn return_outside_function(checker: &mut Checker, stmt: &Stmt) { - if let Some(index) = checker.ctx.scope_stack.top() { - if matches!( - checker.ctx.scopes[index].kind, - ScopeKind::Class(_) | ScopeKind::Module - ) { - checker - .diagnostics - .push(Diagnostic::new(ReturnOutsideFunction, stmt.range())); - } + if matches!( + checker.ctx.scope().kind, + ScopeKind::Class(_) | ScopeKind::Module + ) { + checker + .diagnostics + .push(Diagnostic::new(ReturnOutsideFunction, stmt.range())); } } diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs index 456fd3841a..d489c83ed5 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_local.rs @@ -2,8 +2,8 @@ use std::string::ToString; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::Bindings; -use ruff_python_semantic::scope::{Scope, ScopeKind}; + +use crate::checkers::ast::Checker; #[violation] pub struct UndefinedLocal { @@ -18,26 +18,39 @@ impl Violation for UndefinedLocal { } } -/// F821 -pub fn undefined_local(name: &str, scopes: &[&Scope], bindings: &Bindings) -> Option { - let current = &scopes.last().expect("No current scope found"); - if matches!(current.kind, ScopeKind::Function(_)) && !current.defines(name) { - for scope in scopes.iter().rev().skip(1) { - if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) { - if let Some(binding) = scope.get(name).map(|index| &bindings[*index]) { - if let Some((scope_id, location)) = binding.runtime_usage { - if scope_id == current.id { - return Some(Diagnostic::new( - UndefinedLocal { - name: name.to_string(), - }, - location, - )); - } - } +/// F823 +pub fn undefined_local(checker: &mut Checker, name: &str) { + // If the name hasn't already been defined in the current scope... + let current = checker.ctx.scope(); + if !current.kind.is_function() || current.defines(name) { + return; + } + + let Some(parent) = current.parent else { + return; + }; + + // For every function and module scope above us... + for scope in checker.ctx.scopes.ancestors(parent) { + if !(scope.kind.is_function() || scope.kind.is_module()) { + continue; + } + + // If the name was defined in that scope... + if let Some(binding) = scope.get(name).map(|index| &checker.ctx.bindings[*index]) { + // And has already been accessed in the current scope... + if let Some((scope_id, location)) = binding.runtime_usage { + if scope_id == checker.ctx.scope_id { + // Then it's probably an error. + checker.diagnostics.push(Diagnostic::new( + UndefinedLocal { + name: name.to_string(), + }, + location, + )); + return; } } } } - None } diff --git a/crates/ruff/src/rules/pylint/helpers.rs b/crates/ruff/src/rules/pylint/helpers.rs index b33965b129..ca3e84daa6 100644 --- a/crates/ruff/src/rules/pylint/helpers.rs +++ b/crates/ruff/src/rules/pylint/helpers.rs @@ -16,7 +16,7 @@ pub fn in_dunder_init(checker: &Checker) -> bool { if name != "__init__" { return false; } - let Some(parent) = checker.ctx.parent_scope() else { + let Some(parent) = scope.parent.map(|scope_id| &checker.ctx.scopes[scope_id]) else { return false; }; diff --git a/crates/ruff_python_semantic/src/context.rs b/crates/ruff_python_semantic/src/context.rs index f4eb2f2b2c..661bd7a88b 100644 --- a/crates/ruff_python_semantic/src/context.rs +++ b/crates/ruff_python_semantic/src/context.rs @@ -1,23 +1,23 @@ use std::path::Path; use nohash_hasher::{BuildNoHashHasher, IntMap}; -use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; -use ruff_python_ast::helpers::from_relative_import; -use ruff_python_ast::types::RefEquality; -use ruff_python_ast::typing::AnnotationKind; use rustc_hash::FxHashMap; use rustpython_parser::ast::{Expr, Stmt}; use smallvec::smallvec; -use crate::analyze::visibility::{module_visibility, Modifier, VisibleScope}; +use ruff_python_ast::call_path::{collect_call_path, from_unqualified_name, CallPath}; +use ruff_python_ast::helpers::from_relative_import; +use ruff_python_ast::types::RefEquality; +use ruff_python_ast::typing::AnnotationKind; use ruff_python_stdlib::path::is_python_stub_file; use ruff_python_stdlib::typing::TYPING_EXTENSIONS; +use crate::analyze::visibility::{module_visibility, Modifier, VisibleScope}; use crate::binding::{ Binding, BindingId, BindingKind, Bindings, Exceptions, ExecutionContext, FromImportation, Importation, SubmoduleImportation, }; -use crate::scope::{Scope, ScopeId, ScopeKind, ScopeStack, Scopes}; +use crate::scope::{Scope, ScopeId, ScopeKind, Scopes}; #[allow(clippy::struct_excessive_bools)] pub struct Context<'a> { @@ -35,8 +35,8 @@ pub struct Context<'a> { std::collections::HashMap, BuildNoHashHasher>, pub exprs: Vec>, pub scopes: Scopes<'a>, - pub scope_stack: ScopeStack, - pub dead_scopes: Vec<(ScopeId, ScopeStack)>, + pub scope_id: ScopeId, + pub dead_scopes: Vec, // Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, @@ -75,7 +75,7 @@ impl<'a> Context<'a> { shadowed_bindings: IntMap::default(), exprs: Vec::default(), scopes: Scopes::default(), - scope_stack: ScopeStack::default(), + scope_id: ScopeId::global(), dead_scopes: Vec::default(), body: &[], body_index: 0, @@ -330,19 +330,18 @@ impl<'a> Context<'a> { .expect("Attempted to pop without expression"); } - pub fn push_scope(&mut self, kind: ScopeKind<'a>) -> ScopeId { - let id = self.scopes.push_scope(kind); - self.scope_stack.push(id); - id + /// Push a [`Scope`] with the given [`ScopeKind`] onto the stack. + pub fn push_scope(&mut self, kind: ScopeKind<'a>) { + let id = self.scopes.push_scope(kind, self.scope_id); + self.scope_id = id; } + /// Pop the current [`Scope`] off the stack. pub fn pop_scope(&mut self) { - self.dead_scopes.push(( - self.scope_stack - .pop() - .expect("Attempted to pop without scope"), - self.scope_stack.clone(), - )); + self.dead_scopes.push(self.scope_id); + self.scope_id = self.scopes[self.scope_id] + .parent + .expect("Attempted to pop without scope"); } /// Return the current `Stmt`. @@ -387,31 +386,20 @@ impl<'a> Context<'a> { /// Returns the current top most scope. pub fn scope(&self) -> &Scope<'a> { - &self.scopes[self.scope_stack.top().expect("No current scope found")] - } - - /// Returns the id of the top-most scope - pub fn scope_id(&self) -> ScopeId { - self.scope_stack.top().expect("No current scope found") + &self.scopes[self.scope_id] } /// Returns a mutable reference to the current top most scope. pub fn scope_mut(&mut self) -> &mut Scope<'a> { - let top_id = self.scope_stack.top().expect("No current scope found"); - &mut self.scopes[top_id] - } - - pub fn parent_scope(&self) -> Option<&Scope> { - self.scope_stack - .iter() - .nth(1) - .map(|index| &self.scopes[*index]) + &mut self.scopes[self.scope_id] } + /// Returns an iterator over all scopes, starting from the current scope. pub fn scopes(&self) -> impl Iterator { - self.scope_stack.iter().map(|index| &self.scopes[*index]) + self.scopes.ancestors(self.scope_id) } + /// Returns `true` if the context is in an exception handler. pub const fn in_exception_handler(&self) -> bool { self.in_exception_handler } diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 075148977b..e1366de65a 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -8,8 +8,9 @@ use crate::binding::{BindingId, StarImportation}; #[derive(Debug)] pub struct Scope<'a> { - pub id: ScopeId, pub kind: ScopeKind<'a>, + pub parent: Option, + /// Whether this scope uses the `locals()` builtin. pub uses_locals: bool, /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). @@ -22,13 +23,20 @@ pub struct Scope<'a> { impl<'a> Scope<'a> { pub fn global() -> Self { - Scope::local(ScopeId::global(), ScopeKind::Module) + Scope { + kind: ScopeKind::Module, + parent: None, + uses_locals: false, + star_imports: Vec::default(), + bindings: FxHashMap::default(), + shadowed_bindings: FxHashMap::default(), + } } - pub fn local(id: ScopeId, kind: ScopeKind<'a>) -> Self { + pub fn local(kind: ScopeKind<'a>, parent: ScopeId) -> Self { Scope { - id, kind, + parent: Some(parent), uses_locals: false, star_imports: Vec::default(), bindings: FxHashMap::default(), @@ -189,11 +197,23 @@ impl<'a> Scopes<'a> { } /// Pushes a new scope and returns its unique id - pub fn push_scope(&mut self, kind: ScopeKind<'a>) -> ScopeId { + pub fn push_scope(&mut self, kind: ScopeKind<'a>, parent: ScopeId) -> ScopeId { let next_id = ScopeId::try_from(self.0.len()).unwrap(); - self.0.push(Scope::local(next_id, kind)); + self.0.push(Scope::local(kind, parent)); next_id } + + /// Returns an iterator over all [`ScopeId`] ancestors, starting from the given [`ScopeId`]. + pub fn ancestor_ids(&self, scope_id: ScopeId) -> impl Iterator + '_ { + std::iter::successors(Some(scope_id), |&scope_id| self[scope_id].parent) + } + + /// Returns an iterator over all [`Scope`] ancestors, starting from the given [`ScopeId`]. + pub fn ancestors(&self, scope_id: ScopeId) -> impl Iterator + '_ { + std::iter::successors(Some(&self[scope_id]), |&scope| { + scope.parent.map(|scope_id| &self[scope_id]) + }) + } } impl Default for Scopes<'_> { @@ -222,45 +242,3 @@ impl<'a> Deref for Scopes<'a> { &self.0 } } - -#[derive(Debug, Clone)] -pub struct ScopeStack(Vec); - -impl ScopeStack { - /// Pushes a new scope on the stack - pub fn push(&mut self, id: ScopeId) { - self.0.push(id); - } - - /// Pops the top most scope - pub fn pop(&mut self) -> Option { - self.0.pop() - } - - /// Returns the id of the top-most - pub fn top(&self) -> Option { - self.0.last().copied() - } - - /// Returns an iterator from the current scope to the top scope (reverse iterator) - pub fn iter(&self) -> std::iter::Rev> { - self.0.iter().rev() - } - - pub fn snapshot(&self) -> ScopeStackSnapshot { - ScopeStackSnapshot(self.0.len()) - } - - #[allow(clippy::needless_pass_by_value)] - pub fn restore(&mut self, snapshot: ScopeStackSnapshot) { - self.0.truncate(snapshot.0); - } -} - -pub struct ScopeStackSnapshot(usize); - -impl Default for ScopeStack { - fn default() -> Self { - Self(vec![ScopeId::global()]) - } -}