mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:16 +00:00
allow reads of "free" variables to refer to a global
declaration
Previously this worked if there was also a binding in the same scope as the `global` declaration (probably almost always the case), but CPython doesn't require this. This change surfaced an error in an existing test, where a global variable was only ever declared and bound using the `global` keyword, and never mentioned explicitly in the global scope. @AlexWaygood suggested we probably want to keep that requirement, so I'm adding an a new test for that on top of fixing the failing test.
This commit is contained in:
parent
3b4667ec32
commit
5f2e855c29
2 changed files with 28 additions and 2 deletions
|
@ -44,6 +44,24 @@ def f():
|
||||||
reveal_type(x) # revealed: str
|
reveal_type(x) # revealed: str
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Reads terminate at the `global` keyword in an enclosing scope, even if there's no binding in that scope
|
||||||
|
|
||||||
|
_Unlike_ variables that are explicitly declared `nonlocal` (below), implicitly nonlocal ("free")
|
||||||
|
reads can come from a variable that's declared `global` in an enclosing scope. It doesn't matter
|
||||||
|
whether the variable is bound in that scope:
|
||||||
|
|
||||||
|
```py
|
||||||
|
x: int = 1
|
||||||
|
|
||||||
|
def f():
|
||||||
|
x: str = "hello"
|
||||||
|
def g():
|
||||||
|
global x
|
||||||
|
def h():
|
||||||
|
# allowed: this loads the global `x` variable due to the `global` declaration in the immediate enclosing scope
|
||||||
|
y: int = x
|
||||||
|
```
|
||||||
|
|
||||||
## The `nonlocal` keyword
|
## The `nonlocal` keyword
|
||||||
|
|
||||||
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
|
Without the `nonlocal` keyword, bindings in an inner scope shadow variables of the same name in
|
||||||
|
@ -264,8 +282,8 @@ def f1():
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def f3():
|
def f3():
|
||||||
# This scope declares `x` nonlocal and `y` as global, and it shadows `z` without
|
# This scope declares `x` nonlocal, shadows `y` without a type declaration, and
|
||||||
# giving it a type declaration.
|
# declares `z` global.
|
||||||
nonlocal x
|
nonlocal x
|
||||||
x = 4
|
x = 4
|
||||||
y = 5
|
y = 5
|
||||||
|
|
|
@ -6079,6 +6079,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||||
let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else {
|
let Some(enclosing_place) = enclosing_place_table.place_by_expr(expr) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if enclosing_place.is_marked_global() {
|
||||||
|
// Reads of "free" variables can terminate at an enclosing scope that marks the
|
||||||
|
// variable `global` but doesn't actually bind it. In that case, stop walking
|
||||||
|
// scopes and proceed to the global handling below. (But note that it's a
|
||||||
|
// semantic syntax error for the `nonlocal` keyword to do this. See
|
||||||
|
// `infer_nonlocal_statement`.)
|
||||||
|
break;
|
||||||
|
}
|
||||||
if enclosing_place.is_bound() || enclosing_place.is_declared() {
|
if enclosing_place.is_bound() || enclosing_place.is_declared() {
|
||||||
// We can return early here, because the nearest function-like scope that
|
// We can return early here, because the nearest function-like scope that
|
||||||
// defines a name must be the only source for the nonlocal reference (at
|
// defines a name must be the only source for the nonlocal reference (at
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue