Track conditional deletions in the semantic model (#10415)

## Summary

Given `del X`, we'll typically add a `BindingKind::Deletion` to `X` to
shadow the current binding. However, if the deletion is inside of a
conditional operation, we _won't_, as in:

```python
def f():
    global X

    if X > 0:
        del X
```

We will, however, track it as a reference to the binding. This PR adds
the expression context to those resolved references, so that we can
detect that the `X` in `global X` was "assigned to".

Closes https://github.com/astral-sh/ruff/issues/10397.
This commit is contained in:
Charlie Marsh 2024-03-14 17:45:46 -07:00 committed by GitHub
parent a8e50a7f40
commit 10ace88e9a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 133 additions and 45 deletions

View file

@ -3,10 +3,10 @@ use std::ops::Deref;
use bitflags::bitflags;
use ruff_index::{newtype_index, IndexSlice, IndexVec};
use ruff_python_ast::ExprContext;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::context::ExecutionContext;
use crate::scope::ScopeId;
use crate::{Exceptions, NodeId, SemanticModelFlags};
@ -18,10 +18,12 @@ pub struct ResolvedReference {
node_id: Option<NodeId>,
/// The scope in which the reference is defined.
scope_id: ScopeId,
/// The range of the reference in the source code.
range: TextRange,
/// The expression context in which the reference occurs (e.g., `Load`, `Store`, `Del`).
ctx: ExprContext,
/// The model state in which the reference occurs.
flags: SemanticModelFlags,
/// The range of the reference in the source code.
range: TextRange,
}
impl ResolvedReference {
@ -35,13 +37,19 @@ impl ResolvedReference {
self.scope_id
}
/// The [`ExecutionContext`] of the reference.
pub const fn context(&self) -> ExecutionContext {
if self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT) {
ExecutionContext::Typing
} else {
ExecutionContext::Runtime
}
/// Return `true` if the reference occurred in a `Load` operation.
pub const fn is_load(&self) -> bool {
self.ctx.is_load()
}
/// Return `true` if the context is in a typing context.
pub const fn in_typing_context(&self) -> bool {
self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT)
}
/// Return `true` if the context is in a runtime context.
pub const fn in_runtime_context(&self) -> bool {
!self.flags.intersects(SemanticModelFlags::TYPING_CONTEXT)
}
/// Return `true` if the context is in a typing-only type annotation.
@ -108,14 +116,16 @@ impl ResolvedReferences {
&mut self,
scope_id: ScopeId,
node_id: Option<NodeId>,
range: TextRange,
ctx: ExprContext,
flags: SemanticModelFlags,
range: TextRange,
) -> ResolvedReferenceId {
self.0.push(ResolvedReference {
node_id,
scope_id,
range,
ctx,
flags,
range,
})
}
}