mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-27 12:29:48 +00:00
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:
parent
fd1dfc3bfa
commit
b3240dbfa2
10 changed files with 152 additions and 41 deletions
|
@ -164,3 +164,11 @@ def f():
|
||||||
)
|
)
|
||||||
|
|
||||||
x: DataFrame = 2
|
x: DataFrame = 2
|
||||||
|
|
||||||
|
|
||||||
|
def f():
|
||||||
|
global Member
|
||||||
|
|
||||||
|
from module import Member
|
||||||
|
|
||||||
|
x: Member = 1
|
||||||
|
|
|
@ -138,3 +138,12 @@ def f(provided: int) -> int:
|
||||||
match provided:
|
match provided:
|
||||||
case {**x}:
|
case {**x}:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
global CONSTANT
|
||||||
|
|
||||||
|
|
||||||
|
def f() -> None:
|
||||||
|
global CONSTANT
|
||||||
|
CONSTANT = 1
|
||||||
|
CONSTANT = 2
|
||||||
|
|
|
@ -73,3 +73,10 @@ def override_class():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
CLASS()
|
CLASS()
|
||||||
|
|
||||||
|
|
||||||
|
def multiple_assignment():
|
||||||
|
"""Should warn on every assignment."""
|
||||||
|
global CONSTANT # [global-statement]
|
||||||
|
CONSTANT = 1
|
||||||
|
CONSTANT = 2
|
||||||
|
|
|
@ -264,7 +264,7 @@ where
|
||||||
let binding_id = self.semantic.push_binding(
|
let binding_id = self.semantic.push_binding(
|
||||||
*range,
|
*range,
|
||||||
BindingKind::Global,
|
BindingKind::Global,
|
||||||
BindingFlags::empty(),
|
BindingFlags::GLOBAL,
|
||||||
);
|
);
|
||||||
let scope = self.semantic.scope_mut();
|
let scope = self.semantic.scope_mut();
|
||||||
scope.add(name, binding_id);
|
scope.add(name, binding_id);
|
||||||
|
@ -299,7 +299,7 @@ where
|
||||||
let binding_id = self.semantic.push_binding(
|
let binding_id = self.semantic.push_binding(
|
||||||
*range,
|
*range,
|
||||||
BindingKind::Nonlocal(scope_id),
|
BindingKind::Nonlocal(scope_id),
|
||||||
BindingFlags::empty(),
|
BindingFlags::NONLOCAL,
|
||||||
);
|
);
|
||||||
let scope = self.semantic.scope_mut();
|
let scope = self.semantic.scope_mut();
|
||||||
scope.add(name, binding_id);
|
scope.add(name, binding_id);
|
||||||
|
@ -4279,22 +4279,27 @@ impl<'a> Checker<'a> {
|
||||||
return binding_id;
|
return binding_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid shadowing builtins.
|
||||||
let shadowed = &self.semantic.bindings[shadowed_id];
|
let shadowed = &self.semantic.bindings[shadowed_id];
|
||||||
match &shadowed.kind {
|
if !matches!(
|
||||||
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException => {
|
shadowed.kind,
|
||||||
// Avoid overriding builtins.
|
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException,
|
||||||
}
|
) {
|
||||||
kind @ (BindingKind::Global | BindingKind::Nonlocal(_)) => {
|
|
||||||
// If the original binding was a global or nonlocal, then the new binding is
|
|
||||||
// too.
|
|
||||||
let references = shadowed.references.clone();
|
let references = shadowed.references.clone();
|
||||||
self.semantic.bindings[binding_id].kind = kind.clone();
|
let is_global = shadowed.is_global();
|
||||||
self.semantic.bindings[binding_id].references = references;
|
let is_nonlocal = shadowed.is_nonlocal();
|
||||||
|
|
||||||
|
// If the shadowed binding was global, then this one is too.
|
||||||
|
if is_global {
|
||||||
|
self.semantic.bindings[binding_id].flags |= BindingFlags::GLOBAL;
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
let references = shadowed.references.clone();
|
// If the shadowed binding was non-local, then this one is too.
|
||||||
self.semantic.bindings[binding_id].references = references;
|
if is_nonlocal {
|
||||||
|
self.semantic.bindings[binding_id].flags |= BindingFlags::NONLOCAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.semantic.bindings[binding_id].references = references;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4389,7 +4394,7 @@ impl<'a> Checker<'a> {
|
||||||
if self.semantic.scope().kind.is_any_function() {
|
if self.semantic.scope().kind.is_any_function() {
|
||||||
// Ignore globals.
|
// Ignore globals.
|
||||||
if !self.semantic.scope().get(id).map_or(false, |binding_id| {
|
if !self.semantic.scope().get(id).map_or(false, |binding_id| {
|
||||||
self.semantic.binding(binding_id).kind.is_global()
|
self.semantic.binding(binding_id).is_global()
|
||||||
}) {
|
}) {
|
||||||
pep8_naming::rules::non_lowercase_variable_in_function(self, expr, parent, id);
|
pep8_naming::rules::non_lowercase_variable_in_function(self, expr, parent, id);
|
||||||
}
|
}
|
||||||
|
@ -4839,9 +4844,6 @@ impl<'a> Checker<'a> {
|
||||||
for (name, binding_id) in scope.bindings() {
|
for (name, binding_id) in scope.bindings() {
|
||||||
let binding = self.semantic.binding(binding_id);
|
let binding = self.semantic.binding(binding_id);
|
||||||
if binding.kind.is_global() {
|
if binding.kind.is_global() {
|
||||||
if let Some(source) = binding.source {
|
|
||||||
let stmt = &self.semantic.stmts[source];
|
|
||||||
if stmt.is_global_stmt() {
|
|
||||||
diagnostics.push(Diagnostic::new(
|
diagnostics.push(Diagnostic::new(
|
||||||
pylint::rules::GlobalVariableNotAssigned {
|
pylint::rules::GlobalVariableNotAssigned {
|
||||||
name: (*name).to_string(),
|
name: (*name).to_string(),
|
||||||
|
@ -4851,8 +4853,6 @@ impl<'a> Checker<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Imports in classes are public members.
|
// Imports in classes are public members.
|
||||||
if scope.kind.is_class() {
|
if scope.kind.is_class() {
|
||||||
|
|
|
@ -221,4 +221,32 @@ TCH002.py:47:22: TCH002 [*] Move third-party import `pandas` into a type-checkin
|
||||||
49 52 | x = dict["pd.DataFrame", "pd.DataFrame"]
|
49 52 | x = dict["pd.DataFrame", "pd.DataFrame"]
|
||||||
50 53 |
|
50 53 |
|
||||||
|
|
||||||
|
TCH002.py:172:24: TCH002 [*] Move third-party import `module.Member` into a type-checking block
|
||||||
|
|
|
||||||
|
170 | global Member
|
||||||
|
171 |
|
||||||
|
172 | from module import Member
|
||||||
|
| ^^^^^^ TCH002
|
||||||
|
173 |
|
||||||
|
174 | x: Member = 1
|
||||||
|
|
|
||||||
|
= help: Move into type-checking block
|
||||||
|
|
||||||
|
ℹ Suggested fix
|
||||||
|
1 1 | """Tests to determine accurate detection of typing-only imports."""
|
||||||
|
2 |+from typing import TYPE_CHECKING
|
||||||
|
3 |+
|
||||||
|
4 |+if TYPE_CHECKING:
|
||||||
|
5 |+ from module import Member
|
||||||
|
2 6 |
|
||||||
|
3 7 |
|
||||||
|
4 8 | def f():
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
169 173 | def f():
|
||||||
|
170 174 | global Member
|
||||||
|
171 175 |
|
||||||
|
172 |- from module import Member
|
||||||
|
173 176 |
|
||||||
|
174 177 | x: Member = 1
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,11 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
|
||||||
for binding_id in scope.binding_ids() {
|
for binding_id in scope.binding_ids() {
|
||||||
let binding = checker.semantic().binding(binding_id);
|
let binding = checker.semantic().binding(binding_id);
|
||||||
|
|
||||||
if binding.is_used() || binding.is_explicit_export() {
|
if binding.is_used()
|
||||||
|
|| binding.is_explicit_export()
|
||||||
|
|| binding.is_nonlocal()
|
||||||
|
|| binding.is_global()
|
||||||
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -295,6 +295,8 @@ pub(crate) fn unused_variable(checker: &mut Checker, scope: ScopeId) {
|
||||||
.map(|(name, binding_id)| (name, checker.semantic().binding(binding_id)))
|
.map(|(name, binding_id)| (name, checker.semantic().binding(binding_id)))
|
||||||
.filter_map(|(name, binding)| {
|
.filter_map(|(name, binding)| {
|
||||||
if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment())
|
if (binding.kind.is_assignment() || binding.kind.is_named_expr_assignment())
|
||||||
|
&& !binding.is_nonlocal()
|
||||||
|
&& !binding.is_global()
|
||||||
&& !binding.is_used()
|
&& !binding.is_used()
|
||||||
&& !checker.settings.dummy_variable_rgx.is_match(name)
|
&& !checker.settings.dummy_variable_rgx.is_match(name)
|
||||||
&& name != "__tracebackhide__"
|
&& name != "__tracebackhide__"
|
||||||
|
|
|
@ -58,19 +58,18 @@ pub(crate) fn global_statement(checker: &mut Checker, name: &str) {
|
||||||
let scope = checker.semantic().scope();
|
let scope = checker.semantic().scope();
|
||||||
if let Some(binding_id) = scope.get(name) {
|
if let Some(binding_id) = scope.get(name) {
|
||||||
let binding = checker.semantic().binding(binding_id);
|
let binding = checker.semantic().binding(binding_id);
|
||||||
if binding.kind.is_global() {
|
if binding.is_global() {
|
||||||
let source = checker.semantic().stmts[binding
|
if let Some(source) = binding.source {
|
||||||
.source
|
let source = checker.semantic().stmts[source];
|
||||||
.expect("`global` bindings should always have a `source`")];
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
let diagnostic = Diagnostic::new(
|
|
||||||
GlobalStatement {
|
GlobalStatement {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
},
|
},
|
||||||
// Match Pylint's behavior by reporting on the `global` statement`, rather
|
// Match Pylint's behavior by reporting on the `global` statement`, rather
|
||||||
// than the variable usage.
|
// than the variable usage.
|
||||||
source.range(),
|
source.range(),
|
||||||
);
|
));
|
||||||
checker.diagnostics.push(diagnostic);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,4 +79,23 @@ global_statement.py:70:5: PLW0603 Using the global statement to update `CLASS` i
|
||||||
72 | class CLASS:
|
72 | class CLASS:
|
||||||
|
|
|
|
||||||
|
|
||||||
|
global_statement.py:80:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged
|
||||||
|
|
|
||||||
|
78 | def multiple_assignment():
|
||||||
|
79 | """Should warn on every assignment."""
|
||||||
|
80 | global CONSTANT # [global-statement]
|
||||||
|
| ^^^^^^^^^^^^^^^ PLW0603
|
||||||
|
81 | CONSTANT = 1
|
||||||
|
82 | CONSTANT = 2
|
||||||
|
|
|
||||||
|
|
||||||
|
global_statement.py:81:5: PLW0603 Using the global statement to update `CONSTANT` is discouraged
|
||||||
|
|
|
||||||
|
79 | """Should warn on every assignment."""
|
||||||
|
80 | global CONSTANT # [global-statement]
|
||||||
|
81 | CONSTANT = 1
|
||||||
|
| ^^^^^^^^^^^^ PLW0603
|
||||||
|
82 | CONSTANT = 2
|
||||||
|
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,19 @@ impl<'a> Binding<'a> {
|
||||||
self.flags.contains(BindingFlags::ALIAS)
|
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
|
/// Return `true` if this [`Binding`] represents an unbound variable
|
||||||
/// (e.g., `x` in `x = 1; del x`).
|
/// (e.g., `x` in `x = 1; del x`).
|
||||||
pub const fn is_unbound(&self) -> bool {
|
pub const fn is_unbound(&self) -> bool {
|
||||||
|
@ -193,6 +206,28 @@ bitflags! {
|
||||||
/// from fastapi import FastAPI as app
|
/// from fastapi import FastAPI as app
|
||||||
/// ```
|
/// ```
|
||||||
const ALIAS = 1 << 2;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue