[pylint] Detect global declarations in module scope (PLE0118) (#17411)

Summary
--

While going through the syntax errors in [this comment], I was surprised
to see the error `name 'x' is assigned to before global declaration`,
which corresponds to [load-before-global-declaration (PLE0118)] and has
also been reimplemented as a syntax error (#17135). However, it looks
like neither of the implementations consider `global` declarations in
the top-level module scope, which is a syntax error in CPython:

```python
# try.py
x = None
global x
```

```shell
> python -m compileall -f try.py
Compiling 'try.py'...
***   File "try.py", line 2
    global x
    ^^^^^^^^
SyntaxError: name 'x' is assigned to before global declaration
```

I'm not sure this is the best or most elegant solution, but it was a
quick fix that passed all of our tests.

Test Plan
--

New PLE0118 test case.

[this comment]:
https://github.com/astral-sh/ruff/issues/7633#issuecomment-1740424031
[load-before-global-declaration (PLE0118)]:
https://docs.astral.sh/ruff/rules/load-before-global-declaration/#load-before-global-declaration-ple0118
This commit is contained in:
Brent Westbrook 2025-04-25 08:37:16 -04:00 committed by GitHub
parent 3f84e75e20
commit 6d3b1d13d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 60 additions and 19 deletions

View file

@ -1503,24 +1503,48 @@ impl<'a> SemanticModel<'a> {
/// Set the [`Globals`] for the current [`Scope`].
pub fn set_globals(&mut self, globals: Globals<'a>) {
// If any global bindings don't already exist in the global scope, add them.
for (name, range) in globals.iter() {
if self
.global_scope()
.get(name)
.is_none_or(|binding_id| self.bindings[binding_id].is_unbound())
{
let id = self.bindings.push(Binding {
kind: BindingKind::Assignment,
range: *range,
references: Vec::new(),
scope: ScopeId::global(),
source: self.node_id,
context: self.execution_context(),
exceptions: self.exceptions(),
flags: BindingFlags::empty(),
});
self.global_scope_mut().add(name, id);
// If any global bindings don't already exist in the global scope, add them, unless we are
// also in the global scope, where we don't want these to count as definitions for rules
// like `undefined-name` (F821). For example, adding bindings in the top-level scope causes
// a false negative in cases like this:
//
// ```python
// global x
//
// def f():
// print(x) # F821 false negative
// ```
//
// On the other hand, failing to add bindings in non-top-level scopes causes false
// positives:
//
// ```python
// def f():
// global foo
// import foo
//
// def g():
// foo.is_used() # F821 false positive
// ```
if !self.at_top_level() {
for (name, range) in globals.iter() {
if self
.global_scope()
.get(name)
.is_none_or(|binding_id| self.bindings[binding_id].is_unbound())
{
let id = self.bindings.push(Binding {
kind: BindingKind::Assignment,
range: *range,
references: Vec::new(),
scope: ScopeId::global(),
source: self.node_id,
context: self.execution_context(),
exceptions: self.exceptions(),
flags: BindingFlags::empty(),
});
self.global_scope_mut().add(name, id);
}
}
}