Skip to content

Commit 6494028

Browse files
Python: Fix tuple parsing for parenthesized element with trailing comma outside (#6811)
The parser incorrectly identified `(` as the tuple's opening paren in expressions like `("a"),` where the parentheses wrap the element and the trailing comma outside creates an implicit single-element tuple. This caused the token index to fall behind the AST position, eventually crashing with `'NoneType' object has no attribute 'value_source'` when parsing string literals in type annotations like `Literal["..."]` later in the file.
1 parent f2f4bd9 commit 6494028

3 files changed

Lines changed: 46 additions & 0 deletions

File tree

rewrite-python/rewrite/src/rewrite/python/_parser_visitor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2691,6 +2691,25 @@ def visit_Tuple(self, node):
26912691
close_line == second_elt.lineno and close_col < second_elt_char_col
26922692
):
26932693
maybe_parens = False
2694+
elif close_paren_idx is not None and len(node.elts) == 1:
2695+
# For single-element tuples like `("a"),`, check if the '('
2696+
# wraps just the element (grouping parens) vs the tuple.
2697+
# If the token after ')' is ',' AND there is no ',' inside
2698+
# the parens, then '(' is a grouping paren and the trailing
2699+
# ',' outside creates the tuple.
2700+
# Compare with `("a",),` where ',' is inside the parens
2701+
# (making a proper tuple) and the outer ',' belongs to an
2702+
# enclosing context.
2703+
next_idx = close_paren_idx + 1
2704+
while next_idx < len(self._tokens) and self._tokens[next_idx].type in _SKIP_TOKEN_TYPES:
2705+
next_idx += 1
2706+
if next_idx < len(self._tokens) and self._tokens[next_idx].string == ',':
2707+
# Check if there's a comma inside the parens
2708+
prev_idx = close_paren_idx - 1
2709+
while prev_idx > self._token_idx and self._tokens[prev_idx].type in _SKIP_TOKEN_TYPES:
2710+
prev_idx -= 1
2711+
if prev_idx > self._token_idx and self._tokens[prev_idx].string != ',':
2712+
maybe_parens = False
26942713

26952714
if maybe_parens:
26962715
self._token_idx += 1 # consume '('

rewrite-python/rewrite/tests/python/all/tree/collection_literal_test.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ def test_single_element_tuple_with_trailing_comma():
4444
RecipeSpec().rewrite_run(python("t = (1 , )"))
4545

4646

47+
def test_single_element_tuple_with_trailing_comma_outside_parens():
48+
# language=python - parenthesized expression with trailing comma outside
49+
RecipeSpec().rewrite_run(python('("a"),\n'))
50+
51+
52+
def test_single_element_tuple_with_trailing_comma_outside_parens_multiline():
53+
# language=python - multi-line parenthesized string with trailing comma outside
54+
RecipeSpec().rewrite_run(python(
55+
"""\
56+
(
57+
"Part 1"
58+
" Part 2"
59+
),
60+
"""
61+
))
62+
63+
4764
def test_tuple_with_first_element_in_parens():
4865
# language=python
4966
RecipeSpec().rewrite_run(python("x = (1) // 2, 0"))

rewrite-python/rewrite/tests/python/all/tree/type_hint_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,16 @@ def test_variable_with_parameterized_type_hint_in_quotes():
8888
RecipeSpec().rewrite_run(python("""foo: Dict["Foo", str] = None"""))
8989

9090

91+
def test_literal_string_type_hint_with_assignment():
92+
# language=python - parenthesized string with trailing comma (tuple) before Literal type hint
93+
RecipeSpec().rewrite_run(python(
94+
"""\
95+
("a"),
96+
y: Literal["test"] = "value"
97+
"""
98+
))
99+
100+
91101
def test_variable_with_quoted_type_hint():
92102
# language=python
93103
RecipeSpec().rewrite_run(python("""foo: 'Foo' = None"""))

0 commit comments

Comments
 (0)