Skip to content

Conversation

@ntBre
Copy link
Contributor

@ntBre ntBre commented Apr 15, 2025

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:

# try.py
x = None
global x
> 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.

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
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
```

Test Plan
--

New PLE0118 test case.

We should also check the codspeed results carefully here. `Globals::from_body`
visits all of the statements in `body`, so this might be a really bad idea, but
it looked like the simplest fix.

[this comment]: #7633 (comment)
[load-before-global-declaration (PLE0118)]: https://docs.astral.sh/ruff/rules/load-before-global-declaration/#load-before-global-declaration-ple0118
@ntBre ntBre added the bug Something isn't working label Apr 15, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Apr 15, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@ntBre ntBre marked this pull request as ready for review April 15, 2025 17:34
@MichaReiser
Copy link
Member

Can you tell me a bit more about the motivation for adding this error? I'm not quiet convinced that we should emit the extra error at the module level (especially considering that it results in a 1% perf regression)

What's not quiet clear to me is how many errors will emit in the case where there's an assignment to a variable before its global declaration at the module level. What I understand from the PR summary is that will emit two errors:

  1. That the globals statement is invalid on the module level
  2. That x is used before the globals declaration

If that's the case, then I'd prefer not to make this change because the only relevant error for users to fix is to not use the globals statement and everything will then work as expected (at least for assignments, uses are a different story).

@ntBre
Copy link
Contributor Author

ntBre commented Apr 23, 2025

Can you tell me a bit more about the motivation for adding this error? I'm not quiet convinced that we should emit the extra error at the module level (especially considering that it results in a 1% perf regression)

The main motivation was just that it came up in the fuzzing results from #7633, and I noticed that we didn't raise any error for it.

What's not quiet clear to me is how many errors will emit in the case where there's an assignment to a variable before its global declaration at the module level. What I understand from the PR summary is that will emit two errors:

  1. That the globals statement is invalid on the module level
  2. That x is used before the globals declaration

If that's the case, then I'd prefer not to make this change because the only relevant error for users to fix is to not use the globals statement and everything will then work as expected (at least for assignments, uses are a different story).

I think the summary (and title) was a bit poorly worded. global is valid at the module level as far as CPython is concerned, only accessing the variable before the global statement is an error. This snippet runs fine in Python:

global x
x = 1

What I was trying to say in the title/description was more like "extend PLE0118 to detect load-before-global-declaration in the module scope," but I see now how it sounds very different. PLE0118 currently only applies in function and class scopes but should also apply in module scope to match CPython.

Ideally this would have been a rule-specific change, but instead it looked like I needed to modify the Checker/SemanticModel like I did here.

I don't feel strongly about this, though, especially with the perf hit, and am happy to close the PR. I only opened it for completeness of handling #7633. I think actual code like this would be pretty rare.

@MichaReiser
Copy link
Member

Thanks. So there's nothing special about the module level. It's just that Ruff didn't emit the diagnostic

Comment on lines 1507 to 1508
// also in the global scope, where we don't want these to count as definitions for rules
// like `undefined-name` (F821)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you maybe add an example here of what problem would arise if we did run this code at the global level too? It's not entirely clear to me and I'm wondering if not running it creates different problems at the root level (you could try to comment out this code and see what tests fail for the non-global scope and then decide if this is or isn't a problem for the global scope)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I did try deleting this code entirely before because that actually seemed more natural to me, but I didn't write down the results. This is the tricky F821 (F821_6.py) case that fails if we comment out the binding loop entirely:

"""Test: annotated global."""


n: int


def f():
    print(n)  # F821 false positive here


def g():
    global n
    n = 1


g()
f()

This shorter case also gets a false positive with the whole loop commented out:

def f(): global foo; import foo
def g(): foo.is_used()  # false positive here

I don't think we can run into something similar if the global is in the module scope, but this was all quite tricky, so I'm glad to get your eyes on these examples too.

If we run the binding loop in the global scope too, this case, also for F821, fails:

global x
def foo():
    print(x)  # F821 false negative

The second and third cases here look like nice additions to the docs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, the more I stare at these examples, the more I think I might have been right to consider deleting this code entirely. It's really the assignment and the import that should be adding the bindings, not the global statements. We can just coincidentally get the right answer by letting global handle it, but it's more obvious that a real binding is needed in the module scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is easily actionable though. I poked around a bit in Checker::add_binding and my attempts at checking for globals didn't work out. I've added the examples for now.

@ntBre ntBre merged commit 6d3b1d1 into main Apr 25, 2025
33 checks passed
@ntBre ntBre deleted the brent/ple0118-in-module-scope branch April 25, 2025 12:37
dcreager added a commit that referenced this pull request Apr 25, 2025
* main: (24 commits)
  [`airflow`] Extend `AIR301` rule (#17598)
  [`airflow`] update existing `AIR302` rules with better suggestions (#17542)
  red_knot_project: sort diagnostics from checking files
  [red-knot] fix detecting a metaclass on a not-explicitly-specialized generic base (#17621)
  [red-knot] fix inheritance-cycle detection for generic classes (#17620)
  [`pylint`] Detect `global` declarations in module scope (`PLE0118`) (#17411)
  Add Semantic Error Test for LateFutureImport (#17612)
  [red-knot] change TypeVarInstance to be interned, not tracked (#17616)
  [red-knot] Special case `@final`, `@override` (#17608)
  [red-knot] add TODO comment in specialization code (#17615)
  [`semantic-syntax-errors`] test for `LoadBeforeGlobalDeclaration` - ruff linter (#17592)
  [syntax-errors] `nonlocal` declaration at module level (#17559)
  [`airflow`] Apply auto fix to cases where name has been changed in Airflow 3 (`AIR311`) (#17571)
  [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific (#17460)
  Bump 0.11.7 (#17613)
  [red-knot] Use iterative approach to collect overloads (#17607)
  red_knot_python_semantic: avoid Rust's screaming snake case convention in mdtest
  red_knot_python_semantic: improve diagnostics for unsupported boolean conversions
  red_knot_python_semantic: add "return type span" helper method
  red_knot_python_semantic: move parameter span helper method
  ...
dcreager added a commit that referenced this pull request Apr 26, 2025
* main: (27 commits)
  [red-knot] Add new property tests for subtyping with "bottom" callable (#17635)
  [red-knot] Create generic context for generic classes lazily (#17617)
  ruff_db: add tests for annotations with no ranges
  [`airflow`] Extend `AIR301` rule (#17598)
  [`airflow`] update existing `AIR302` rules with better suggestions (#17542)
  red_knot_project: sort diagnostics from checking files
  [red-knot] fix detecting a metaclass on a not-explicitly-specialized generic base (#17621)
  [red-knot] fix inheritance-cycle detection for generic classes (#17620)
  [`pylint`] Detect `global` declarations in module scope (`PLE0118`) (#17411)
  Add Semantic Error Test for LateFutureImport (#17612)
  [red-knot] change TypeVarInstance to be interned, not tracked (#17616)
  [red-knot] Special case `@final`, `@override` (#17608)
  [red-knot] add TODO comment in specialization code (#17615)
  [`semantic-syntax-errors`] test for `LoadBeforeGlobalDeclaration` - ruff linter (#17592)
  [syntax-errors] `nonlocal` declaration at module level (#17559)
  [`airflow`] Apply auto fix to cases where name has been changed in Airflow 3 (`AIR311`) (#17571)
  [syntax-errors] Make `async-comprehension-in-sync-comprehension` more specific (#17460)
  Bump 0.11.7 (#17613)
  [red-knot] Use iterative approach to collect overloads (#17607)
  red_knot_python_semantic: avoid Rust's screaming snake case convention in mdtest
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants