[ty] Implement global handling and load-before-global-declaration syntax error (#17637)

Summary
--

This PR resolves both the typing-related and syntax error TODOs added in
#17563 by tracking a set of `global` bindings for each scope. As
discussed below, we avoid the additional AST traversal from ruff by
collecting `Name`s from `global` statements while building the semantic
index and emit a syntax error if the `Name` is already bound in the
current scope at the point of the `global` statement. This has the
downside of separating the error from the `SemanticSyntaxChecker`, but I
plan to explore using this approach in the `SemanticSyntaxChecker`
itself as a follow-up. It seems like this may be a better approach for
ruff as well.

Test Plan
--

Updated all of the related mdtests to remove the TODOs (and add quotes I
forgot on the messages).

There is one remaining TODO, but it requires `nonlocal` support, which
isn't even incorporated into the `SemanticSyntaxChecker` yet.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Brent Westbrook 2025-05-08 10:30:04 -04:00 committed by GitHub
parent 67cd94ed64
commit 57bf7dfbd9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 174 additions and 48 deletions

View file

@ -32,8 +32,14 @@ def f():
y = ""
global x
# TODO: error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
x = ""
global z
# error: [invalid-assignment] "Object of type `Literal[""]` is not assignable to `int`"
z = ""
z: int
```
## Nested intervening scope
@ -48,8 +54,7 @@ def outer():
def inner():
global x
# TODO: revealed: int
reveal_type(x) # revealed: str
reveal_type(x) # revealed: int
```
## Narrowing
@ -87,8 +92,7 @@ def f():
```py
def f():
global x
# TODO this should also not be an error
y = x # error: [unresolved-reference] "Name `x` used when not defined"
y = x
x = 1 # No error.
x = 2
@ -99,79 +103,111 @@ x = 2
Using a name prior to its `global` declaration in the same scope is a syntax error.
```py
x = 1
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x)
def f():
global x
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(x)
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x)
def f():
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x)
def f():
global x, y
print(x) # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
print(x)
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
print(x)
def f():
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
x = 1
def f():
global x
x = 1 # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = 1
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
x = 1
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x
def f():
global x
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
del x
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x
def f():
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x
def f():
global x, y
del x # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x, y
del x
global x, y # error: [invalid-syntax] "name `x` is used prior to global declaration"
del x
def f():
print(f"{x=}") # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
print(f"{x=}")
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
# still an error in module scope
x = None # TODO: error: [invalid-syntax] name `x` is used prior to global declaration
global x
x = None
global x # error: [invalid-syntax] "name `x` is used prior to global declaration"
```
## Local bindings override preceding `global` bindings
```py
x = 42
def f():
global x
reveal_type(x) # revealed: Unknown | Literal[42]
x = "56"
reveal_type(x) # revealed: Literal["56"]
```
## Local assignment prevents falling back to the outer scope
```py
x = 42
def f():
# error: [unresolved-reference] "Name `x` used when not defined"
reveal_type(x) # revealed: Unknown
x = "56"
reveal_type(x) # revealed: Literal["56"]
```
## Annotating a `global` binding is a syntax error
```py
x: int = 1
def f():
global x
x: str = "foo" # TODO: error: [invalid-syntax] "annotated name 'x' can't be global"
```