mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-10 13:48:35 +00:00
Add support for global and nonlocal symbol renames (#5134)
## Summary In #5074, we introduced an abstraction to support local symbol renames ("local" here refers to "within a module"). However, that abstraction didn't support `global` and `nonlocal` symbols. This PR extends it to those cases. Broadly, there are considerations. First, if we're renaming a symbol in a scope in which it is declared `global` or `nonlocal`. For example, given: ```python x = 1 def foo(): global x ``` Then when renaming `x` in `foo`, we need to detect that it's `global` and instead perform the rename starting from the module scope. Second, when renaming a symbol, we need to determine the scopes in which it is declared `global` or `nonlocal`. This is effectively the inverse of the above: when renaming `x` in the module scope, we need to detect that we should _also_ rename `x` in `foo`. To support these cases, the renaming algorithm was adjusted as follows: - When we start a rename in a scope, determine whether the symbol is declared `global` or `nonlocal` by looking for a `global` or `nonlocal` binding. If it is, start the rename in the defining scope. (This requires storing the defining scope on the `nonlocal` binding, which is new.) - We then perform the rename in the defining scope. - We then check whether the symbol was declared as `global` or `nonlocal` in any scopes, and perform the rename in those scopes too. (Thankfully, this doesn't need to be done recursively.) Closes #5092. ## Test Plan Added some additional snapshot tests.
This commit is contained in:
parent
b9754bd5c5
commit
fd1dfc3bfa
7 changed files with 305 additions and 81 deletions
|
@ -10,6 +10,7 @@ use crate::context::ExecutionContext;
|
|||
use crate::model::SemanticModel;
|
||||
use crate::node::NodeId;
|
||||
use crate::reference::ReferenceId;
|
||||
use crate::ScopeId;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Binding<'a> {
|
||||
|
@ -336,7 +337,7 @@ pub enum BindingKind<'a> {
|
|||
/// def foo():
|
||||
/// nonlocal x
|
||||
/// ```
|
||||
Nonlocal,
|
||||
Nonlocal(ScopeId),
|
||||
|
||||
/// A binding for a builtin, like `print` or `bool`.
|
||||
Builtin,
|
||||
|
|
|
@ -73,7 +73,7 @@ pub struct SemanticModel<'a> {
|
|||
|
||||
/// Map from binding index to indexes of bindings that annotate it (in the same scope).
|
||||
///
|
||||
/// For example:
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
/// x: int
|
||||
|
@ -94,6 +94,21 @@ pub struct SemanticModel<'a> {
|
|||
/// first binding in a scope; any annotations that follow are treated as "delayed" annotations.
|
||||
delayed_annotations: HashMap<BindingId, Vec<BindingId>, BuildNoHashHasher<BindingId>>,
|
||||
|
||||
/// Map from binding ID to the IDs of all scopes in which it is declared a `global` or
|
||||
/// `nonlocal`.
|
||||
///
|
||||
/// For example, given:
|
||||
/// ```python
|
||||
/// x = 1
|
||||
///
|
||||
/// def f():
|
||||
/// global x
|
||||
/// ```
|
||||
///
|
||||
/// In this case, the binding created by `x = 1` is rebound within the scope created by `f`
|
||||
/// by way of the `global x` statement.
|
||||
rebinding_scopes: HashMap<BindingId, Vec<ScopeId>, BuildNoHashHasher<BindingId>>,
|
||||
|
||||
/// Body iteration; used to peek at siblings.
|
||||
pub body: &'a [Stmt],
|
||||
pub body_index: usize,
|
||||
|
@ -123,6 +138,7 @@ impl<'a> SemanticModel<'a> {
|
|||
globals: GlobalsArena::default(),
|
||||
shadowed_bindings: IntMap::default(),
|
||||
delayed_annotations: IntMap::default(),
|
||||
rebinding_scopes: IntMap::default(),
|
||||
body: &[],
|
||||
body_index: 0,
|
||||
flags: SemanticModelFlags::new(path),
|
||||
|
@ -699,6 +715,26 @@ impl<'a> SemanticModel<'a> {
|
|||
self.globals[global_id].get(name).copied()
|
||||
}
|
||||
|
||||
/// Given a `name` that has been declared `nonlocal`, return the [`ScopeId`] and [`BindingId`]
|
||||
/// to which it refers.
|
||||
///
|
||||
/// Unlike `global` declarations, for which the scope is unambiguous, Python requires that
|
||||
/// `nonlocal` declarations refer to the closest enclosing scope that contains a binding for
|
||||
/// the given name.
|
||||
pub fn nonlocal(&self, name: &str) -> Option<(ScopeId, BindingId)> {
|
||||
self.scopes
|
||||
.ancestor_ids(self.scope_id)
|
||||
.skip(1)
|
||||
.find_map(|scope_id| {
|
||||
let scope = &self.scopes[scope_id];
|
||||
if scope.kind.is_module() || scope.kind.is_class() {
|
||||
None
|
||||
} else {
|
||||
scope.get(name).map(|binding_id| (scope_id, binding_id))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Return `true` if the given [`ScopeId`] matches that of the current scope.
|
||||
pub fn is_current_scope(&self, scope_id: ScopeId) -> bool {
|
||||
self.scope_id == scope_id
|
||||
|
@ -766,6 +802,21 @@ impl<'a> SemanticModel<'a> {
|
|||
self.delayed_annotations.get(&binding_id).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Mark the given [`BindingId`] as rebound in the given [`ScopeId`] (i.e., declared as
|
||||
/// `global` or `nonlocal`).
|
||||
pub fn add_rebinding_scope(&mut self, binding_id: BindingId, scope_id: ScopeId) {
|
||||
self.rebinding_scopes
|
||||
.entry(binding_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(scope_id);
|
||||
}
|
||||
|
||||
/// Return the list of [`ScopeId`]s in which the given [`BindingId`] is rebound (i.e., declared
|
||||
/// as `global` or `nonlocal`).
|
||||
pub fn rebinding_scopes(&self, binding_id: BindingId) -> Option<&[ScopeId]> {
|
||||
self.rebinding_scopes.get(&binding_id).map(Vec::as_slice)
|
||||
}
|
||||
|
||||
/// Return the [`ExecutionContext`] of the current scope.
|
||||
pub const fn execution_context(&self) -> ExecutionContext {
|
||||
if self.in_type_checking_block()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue