[syntax-errors] Document behavior of global declarations in try nodes before 3.13 (#17285)

Summary
--

This PR extends the documentation of the `LoadBeforeGlobalDeclaration`
check to specify the behavior on versions of Python before 3.13. Namely,
on Python 3.12, the `else` clause of a `try` statement is visited before
the `except` handlers:

```pycon
Python 3.12.9 (main, Feb 12 2025, 14:50:50) [Clang 19.1.6 ] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 10
>>> def g():
...     try:
...         1 / 0
...     except:
...         a = 1
...     else:
...         global a
...
>>> def f():
...     try:
...         pass
...     except:
...         global a
...     else:
...         print(a)
...
  File "<stdin>", line 5
SyntaxError: name 'a' is used prior to global declaration

```

The order is swapped on 3.13 (see
[CPython#111123](https://github.com/python/cpython/issues/111123)):

```pycon
Python 3.13.2 (main, Feb  5 2025, 08:05:21) [GCC 14.2.1 20250128] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 10
... def g():
...     try:
...         1 / 0
...     except:
...         a = 1
...     else:
...         global a
...
  File "<python-input-0>", line 8
    global a
    ^^^^^^^^
SyntaxError: name 'a' is assigned to before global declaration
>>> def f():
...     try:
...         pass
...     except:
...         global a
...     else:
...         print(a)
...
>>>
```

The current implementation of PLE0118 is correct for 3.13 but not 3.12:
[playground](https://play.ruff.rs/d7467ea6-f546-4a76-828f-8e6b800694c9)
(it flags the first case regardless of Python version).

We decided to maintain this incorrect diagnostic for Python versions
before 3.13 because the pre-3.13 behavior is very unintuitive and
confirmed to be a bug, although the bug fix was not backported to
earlier versions. This can lead to false positives and false negatives
for pre-3.13 code, but we also expect that to be very rare, as
demonstrated by the ecosystem check (before the version-dependent check
was reverted here).

Test Plan
--

N/a
This commit is contained in:
Brent Westbrook 2025-04-09 12:54:21 -04:00 committed by GitHub
parent 73399029b2
commit 2fbc4d577e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1011,6 +1011,43 @@ pub enum SemanticSyntaxErrorKind {
/// global counter
/// counter += 1
/// ```
///
/// ## Known Issues
///
/// Note that the order in which the parts of a `try` statement are visited was changed in 3.13,
/// as tracked in Python issue [#111123]. For example, this code was valid on Python 3.12:
///
/// ```python
/// a = 10
/// def g():
/// try:
/// 1 / 0
/// except:
/// a = 1
/// else:
/// global a
/// ```
///
/// While this more intuitive behavior aligned with the textual order was a syntax error:
///
/// ```python
/// a = 10
/// def f():
/// try:
/// pass
/// except:
/// global a
/// else:
/// a = 1 # SyntaxError: name 'a' is assigned to before global declaration
/// ```
///
/// This was reversed in version 3.13 to make the second case valid and the first case a syntax
/// error. We intentionally enforce the 3.13 ordering, regardless of the Python version, which
/// will lead to both false positives and false negatives on 3.12 code that takes advantage of
/// the old behavior. However, as mentioned in the Python issue, we expect code relying on this
/// to be very rare and not worth the additional complexity to detect.
///
/// [#111123]: https://github.com/python/cpython/issues/111123
LoadBeforeGlobalDeclaration { name: String, start: TextSize },
/// Represents the use of a starred expression in an invalid location, such as a `return` or