From 694e4d296d740f87f9e2df5636a88670864f73c7 Mon Sep 17 00:00:00 2001 From: Yashvardhan Thanvi Date: Thu, 15 Oct 2026 08:56:05 +0000 Subject: [PATCH] Preserve whitespace before `:` in except handlers without `as` The native parser only read the whitespace before the colon of an ExceptHandler or ExceptStarHandler when the handler had an `as` name. Without a name that whitespace was not stored anywhere, so `except A :` and `except* A :` came back as `except A:` and `except* A:` after a parse and codegen round trip. Always parse whitespace_before_colon from the colon token. A bare `except :` is unchanged: its whitespace is shared with the colon token and is still consumed first by whitespace_after_except. Add round-trip fixtures for both handler types, and node tests that check whitespace_before_colon is populated when there is no name. Fixes #1457 --- libcst/_nodes/tests/test_try.py | 94 +++++++++++++++++++ native/libcst/src/nodes/statement.rs | 21 ++--- native/libcst/tests/fixtures/starry_tries.py | 11 +++ .../libcst/tests/fixtures/terrible_tries.py | 13 +++ 4 files changed, 126 insertions(+), 13 deletions(-) diff --git a/libcst/_nodes/tests/test_try.py b/libcst/_nodes/tests/test_try.py index c5ae24624..8c96f0609 100644 --- a/libcst/_nodes/tests/test_try.py +++ b/libcst/_nodes/tests/test_try.py @@ -372,6 +372,60 @@ class TryTest(CSTNodeTest): ), "code": "try: pass\nexcept ValueError, RuntimeError: pass\n", }, + # Whitespace before the colon without an as name + { + "node": cst.Try( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + type=cst.Name("Exception"), + whitespace_before_colon=cst.SimpleWhitespace(" "), + ), + ), + ), + "code": "try: pass\nexcept Exception : pass\n", + "parser": parse_statement, + }, + # Whitespace before the colon after a parenthesized tuple + { + "node": cst.Try( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + type=cst.Tuple( + [ + cst.Element( + cst.Name("IOError"), + comma=cst.Comma( + whitespace_after=cst.SimpleWhitespace(" ") + ), + ), + cst.Element(cst.Name("ImportError")), + ] + ), + whitespace_before_colon=cst.SimpleWhitespace(" "), + ), + ), + ), + "code": "try: pass\nexcept (IOError, ImportError) : pass\n", + "parser": parse_statement, + }, + # A bare except keeps its space in whitespace_after_except + { + "node": cst.Try( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + whitespace_after_except=cst.SimpleWhitespace(" "), + ), + ), + ), + "code": "try: pass\nexcept : pass\n", + "parser": parse_statement, + }, ) ) def test_valid(self, **kwargs: Any) -> None: @@ -636,6 +690,46 @@ class TryStarTest(CSTNodeTest): "code": "try: pass\nexcept* ValueError, RuntimeError: pass\n", "parser": native_parse_statement, }, + # Whitespace before the colon without an as name + { + "node": cst.TryStar( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptStarHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + type=cst.Name("Exception"), + whitespace_before_colon=cst.SimpleWhitespace(" "), + ), + ), + ), + "code": "try: pass\nexcept* Exception : pass\n", + "parser": native_parse_statement, + }, + # Whitespace before the colon after a parenthesized tuple + { + "node": cst.TryStar( + cst.SimpleStatementSuite((cst.Pass(),)), + handlers=( + cst.ExceptStarHandler( + cst.SimpleStatementSuite((cst.Pass(),)), + type=cst.Tuple( + [ + cst.Element( + cst.Name("IOError"), + comma=cst.Comma( + whitespace_after=cst.SimpleWhitespace(" ") + ), + ), + cst.Element(cst.Name("ImportError")), + ] + ), + whitespace_before_colon=cst.SimpleWhitespace(" "), + ), + ), + ), + "code": "try: pass\nexcept* (IOError, ImportError) : pass\n", + "parser": native_parse_statement, + }, ) ) def test_valid(self, **kwargs: Any) -> None: diff --git a/native/libcst/src/nodes/statement.rs b/native/libcst/src/nodes/statement.rs index 8ed32a6bd..ec39cce16 100644 --- a/native/libcst/src/nodes/statement.rs +++ b/native/libcst/src/nodes/statement.rs @@ -2100,14 +2100,12 @@ impl<'r, 'a> Inflate<'a> for DeflatedExceptHandler<'r, 'a> { let r#type = self.r#type.inflate(config)?; let name = self.name.inflate(config)?; - let whitespace_before_colon = if name.is_some() { - parse_simple_whitespace( - config, - &mut (*self.colon_tok).whitespace_before.borrow_mut(), - )? - } else { - Default::default() - }; + // In a bare `except :`, whitespace_after_except has already consumed this + // whitespace, so it parses as empty here. + let whitespace_before_colon = parse_simple_whitespace( + config, + &mut (*self.colon_tok).whitespace_before.borrow_mut(), + )?; let body = self.body.inflate(config)?; Ok(Self::Inflated { @@ -2172,11 +2170,8 @@ impl<'r, 'a> Inflate<'a> for DeflatedExceptStarHandler<'r, 'a> { let r#type = self.r#type.inflate(config)?; let name = self.name.inflate(config)?; - let whitespace_before_colon = if name.is_some() { - parse_simple_whitespace(config, &mut self.colon_tok.whitespace_before.borrow_mut())? - } else { - Default::default() - }; + let whitespace_before_colon = + parse_simple_whitespace(config, &mut self.colon_tok.whitespace_before.borrow_mut())?; let body = self.body.inflate(config)?; Ok(Self::Inflated { diff --git a/native/libcst/tests/fixtures/starry_tries.py b/native/libcst/tests/fixtures/starry_tries.py index 2749a0315..51f7927d0 100644 --- a/native/libcst/tests/fixtures/starry_tries.py +++ b/native/libcst/tests/fixtures/starry_tries.py @@ -30,6 +30,17 @@ finally: pass +try: + pass +except* foo : + pass +except* (foo, bar) : + pass +except* foo, bar : + pass +except* foo as bar : + pass + try: diff --git a/native/libcst/tests/fixtures/terrible_tries.py b/native/libcst/tests/fixtures/terrible_tries.py index eb5429ccb..aa11052ca 100644 --- a/native/libcst/tests/fixtures/terrible_tries.py +++ b/native/libcst/tests/fixtures/terrible_tries.py @@ -75,6 +75,19 @@ except (foo, bar): pass +try: + pass +except foo : + pass +except (foo, bar) : + pass +except foo, bar : + pass +except foo as bar : + pass +except : + pass + try: pass except foo, bar: