Skip to content

Commit 82d6e55

Browse files
Python: Fix auto_format adding extra indentation on sub-trees (#6842)
When auto_format is called on a sub-tree (e.g., an If node inside a method body) with a cursor, TabsAndIndentsVisitor.visit() called pre_visit() on the root element during setup. This set indent_type=INDENT on the cursor, causing visit_space() to add an extra indent_size to the node's own prefix — doubling the indentation. The pre_visit setup was intended to establish context for children, but it incorrectly affected the root element itself. In normal full-tree traversal, pre_visit is called as the visitor descends, so the node's own prefix uses the parent's indent_type (not its own). Remove the setup pre_visit call. The normal traversal pre_visit calls handle child indentation correctly without it.
1 parent 33ac75a commit 82d6e55

2 files changed

Lines changed: 105 additions & 5 deletions

File tree

rewrite-python/rewrite/src/rewrite/python/format/tabs_and_indents_visitor.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,6 @@ def visit(self, tree: Optional[Tree], p: P, parent: Optional[Cursor] = None) ->
6363
if indent != 0:
6464
c.put_message("last_indent", indent)
6565

66-
for next_parent in parent.get_path():
67-
if isinstance(next_parent, J):
68-
self.pre_visit(next_parent, p)
69-
break
70-
7166
return super().visit(tree, p)
7267

7368
def pre_visit(self, tree: T, p: P) -> Optional[T]:
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Copyright 2025 the original author or authors.
2+
# <p>
3+
# Licensed under the Moderne Source Available License (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
# <p>
7+
# https://docs.moderne.io/licensing/moderne-source-available-license
8+
# <p>
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Tests for auto_format when applied to sub-trees with a cursor.
16+
17+
When a recipe calls auto_format(node, p, cursor=self.cursor) on a node
18+
that is not the root CompilationUnit, the formatter must preserve the
19+
node's own indentation level and only adjust its children. Previously,
20+
TabsAndIndentsVisitor.visit() called pre_visit() on the root element
21+
during setup, which set indent_type=INDENT on the cursor and caused an
22+
extra indentation level to be added to the node's own prefix.
23+
"""
24+
25+
from typing import Any, Optional
26+
27+
from rewrite import ExecutionContext, Recipe, TreeVisitor
28+
from rewrite.java.tree import (
29+
Block,
30+
If as JIf,
31+
)
32+
from rewrite.python import tree as py_tree
33+
from rewrite.python.format import auto_format
34+
from rewrite.python.visitor import PythonVisitor
35+
from rewrite.test import RecipeSpec, python
36+
37+
38+
class _AutoFormatIfRecipe(Recipe):
39+
"""Calls auto_format on the If node to test sub-tree formatting."""
40+
41+
@property
42+
def name(self) -> str:
43+
return "test.AutoFormatIf"
44+
45+
@property
46+
def display_name(self) -> str:
47+
return "Test auto_format on If sub-tree"
48+
49+
@property
50+
def description(self) -> str:
51+
return "Calls auto_format on an If node to verify indentation is preserved."
52+
53+
def editor(self) -> TreeVisitor[Any, ExecutionContext]:
54+
class Visitor(PythonVisitor[ExecutionContext]):
55+
def visit_if(
56+
self, if_stmt: JIf, p: ExecutionContext
57+
) -> Optional[JIf]:
58+
if_stmt = super().visit_if(if_stmt, p)
59+
# Only process if-statements with a pass body
60+
if isinstance(if_stmt.then_part, Block):
61+
stmts = if_stmt.then_part.statements
62+
if len(stmts) == 1 and isinstance(stmts[0], py_tree.Pass):
63+
return auto_format(if_stmt, p, cursor=self.cursor)
64+
return if_stmt
65+
66+
return Visitor()
67+
68+
69+
def test_auto_format_if_in_method_preserves_indent():
70+
"""auto_format on an If inside a method must not add extra indentation.
71+
72+
The If at 4-space indent should stay at 4-space indent, not shift to 8.
73+
"""
74+
spec = RecipeSpec(recipe=_AutoFormatIfRecipe())
75+
spec.rewrite_run(
76+
python(
77+
"def resolve(self):\n"
78+
" if cond:\n"
79+
" pass\n",
80+
)
81+
)
82+
83+
84+
def test_auto_format_if_in_nested_method_preserves_indent():
85+
"""auto_format on a deeply nested If should preserve its indentation."""
86+
spec = RecipeSpec(recipe=_AutoFormatIfRecipe())
87+
spec.rewrite_run(
88+
python(
89+
"class Foo:\n"
90+
" def resolve(self):\n"
91+
" if cond:\n"
92+
" pass\n",
93+
)
94+
)
95+
96+
97+
def test_auto_format_if_at_top_level_preserves_indent():
98+
"""auto_format on a top-level If should preserve zero indentation."""
99+
spec = RecipeSpec(recipe=_AutoFormatIfRecipe())
100+
spec.rewrite_run(
101+
python(
102+
"if cond:\n"
103+
" pass\n",
104+
)
105+
)

0 commit comments

Comments
 (0)