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

@ -75,6 +75,11 @@ impl<'a> Binding<'a> {
self.flags.intersects(BindingFlags::GLOBAL)
}
/// Return `true` if this [`Binding`] was deleted.
pub const fn is_deleted(&self) -> bool {
self.flags.intersects(BindingFlags::DELETED)
}
/// Return `true` if this [`Binding`] represents an assignment to `__all__` with an invalid
/// value (e.g., `__all__ = "Foo"`).
pub const fn is_invalid_all_format(&self) -> bool {
@ -165,6 +170,7 @@ impl<'a> Binding<'a> {
// Deletions, annotations, `__future__` imports, and builtins are never considered
// redefinitions.
BindingKind::Deletion
| BindingKind::ConditionalDeletion(_)
| BindingKind::Annotation
| BindingKind::FutureImport
| BindingKind::Builtin => {
@ -265,6 +271,19 @@ bitflags! {
/// ```
const GLOBAL = 1 << 4;
/// The binding was deleted (i.e., the target of a `del` statement).
///
/// For example, the binding could be `x` in:
/// ```python
/// del x
/// ```
///
/// The semantic model will typically shadow a deleted binding via an additional binding
/// with [`BindingKind::Deletion`]; however, conditional deletions (e.g.,
/// `if condition: del x`) do _not_ generate a shadow binding. This flag is thus used to
/// detect whether a binding was _ever_ deleted, even conditionally.
const DELETED = 1 << 5;
/// The binding represents an export via `__all__`, but the assigned value uses an invalid
/// expression (i.e., a non-container type).
///
@ -272,7 +291,7 @@ bitflags! {
/// ```python
/// __all__ = 1
/// ```
const INVALID_ALL_FORMAT = 1 << 5;
const INVALID_ALL_FORMAT = 1 << 6;
/// The binding represents an export via `__all__`, but the assigned value contains an
/// invalid member (i.e., a non-string).
@ -281,7 +300,7 @@ bitflags! {
/// ```python
/// __all__ = [1]
/// ```
const INVALID_ALL_OBJECT = 1 << 6;
const INVALID_ALL_OBJECT = 1 << 7;
/// The binding represents a private declaration.
///
@ -289,7 +308,7 @@ bitflags! {
/// ```python
/// _T = "This is a private variable"
/// ```
const PRIVATE_DECLARATION = 1 << 7;
const PRIVATE_DECLARATION = 1 << 8;
/// The binding represents an unpacked assignment.
///
@ -297,7 +316,7 @@ bitflags! {
/// ```python
/// (x, y) = 1, 2
/// ```
const UNPACKED_ASSIGNMENT = 1 << 8;
const UNPACKED_ASSIGNMENT = 1 << 9;
}
}
@ -512,6 +531,13 @@ pub enum BindingKind<'a> {
/// ```
Deletion,
/// A binding for a deletion, like `x` in:
/// ```python
/// if x > 0:
/// del x
/// ```
ConditionalDeletion(BindingId),
/// A binding to bind an exception to a local variable, like `x` in:
/// ```python
/// try: