From 2fbc4d577e35e1ff957f3dafd776a1436fa5486c Mon Sep 17 00:00:00 2001 From: Brent Westbrook <36778786+ntBre@users.noreply.github.com> Date: Wed, 9 Apr 2025 12:54:21 -0400 Subject: [PATCH] [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 "", 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 "", 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 --- .../ruff_python_parser/src/semantic_errors.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/ruff_python_parser/src/semantic_errors.rs b/crates/ruff_python_parser/src/semantic_errors.rs index e651354820..4bed95c45c 100644 --- a/crates/ruff_python_parser/src/semantic_errors.rs +++ b/crates/ruff_python_parser/src/semantic_errors.rs @@ -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