Avoid propagating BindingKind::Global and BindingKind::Nonlocal (#5136)

## Summary

This PR fixes a small quirk in the semantic model. Typically, when we
see an import, like `import foo`, we create a `BindingKind::Importation`
for it. However, if `foo` has been declared as a `global`, then we
propagate the kind forward. So given:

```python
global foo

import foo
```

We'd create two bindings for `foo`, both with type `global`.

This was originally borrowed from Pyflakes, and it exists to help avoid
false-positives like:

```python
def f():
    global foo

    # Don't mark `foo` as "assigned but unused"! It's a global!
    foo = 1
```

This PR removes that behavior, and instead tracks "Does this binding
refer to a global?" as a flag. This is much cleaner, since it means we
don't "lose" the identity of various bindings.

As a very strange example of why this matters, consider:

```python
def foo():
    global Member

    from module import Member

    x: Member = 1
```

`Member` is only used in a typing context, so we should flag it and say
"move it to a `TYPE_CHECKING` block". However, when we go to analyze
`from module import Member`, it has `BindingKind::Global`. So we don't
even know that it's an import!
This commit is contained in:
Charlie Marsh 2023-06-16 11:06:59 -04:00 committed by GitHub
parent fd1dfc3bfa
commit b3240dbfa2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 41 deletions

View file

@ -57,6 +57,19 @@ impl<'a> Binding<'a> {
self.flags.contains(BindingFlags::ALIAS)
}
/// Return `true` if this [`Binding`] represents a `nonlocal`. A [`Binding`] is a `nonlocal`
/// if it's declared by a `nonlocal` statement, or shadows a [`Binding`] declared by a
/// `nonlocal` statement.
pub const fn is_nonlocal(&self) -> bool {
self.flags.contains(BindingFlags::NONLOCAL)
}
/// Return `true` if this [`Binding`] represents a `global`. A [`Binding`] is a `global` if it's
/// declared by a `global` statement, or shadows a [`Binding`] declared by a `global` statement.
pub const fn is_global(&self) -> bool {
self.flags.contains(BindingFlags::GLOBAL)
}
/// Return `true` if this [`Binding`] represents an unbound variable
/// (e.g., `x` in `x = 1; del x`).
pub const fn is_unbound(&self) -> bool {
@ -193,6 +206,28 @@ bitflags! {
/// from fastapi import FastAPI as app
/// ```
const ALIAS = 1 << 2;
/// The binding is `nonlocal` to the declaring scope. This could be a binding created by
/// a `nonlocal` statement, or a binding that shadows such a binding.
///
/// For example, both of the bindings in the following function are `nonlocal`:
/// ```python
/// def f():
/// nonlocal x
/// x = 1
/// ```
const NONLOCAL = 1 << 3;
/// The binding is `global`. This could be a binding created by a `global` statement, or a
/// binding that shadows such a binding.
///
/// For example, both of the bindings in the following function are `global`:
/// ```python
/// def f():
/// global x
/// x = 1
/// ```
const GLOBAL = 1 << 4;
}
}