Skip to content
This repository was archived by the owner on Jan 13, 2026. It is now read-only.

Commit 193193e

Browse files
Add RemoveTrailingWhitespaceVisitor (#100)
* Add RemoveTrailingWhitespaceVisitor * Add pre/post visit methods * Fix attribute * Call RemoveTrailingWhiteSpaceVisitor in AutoFormatVisitor * Update rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py Co-authored-by: Knut Wannheden <[email protected]> * Fix for trailing comma and clean --------- Co-authored-by: Knut Wannheden <[email protected]>
1 parent a4c9587 commit 193193e

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed

rewrite/rewrite/python/format/auto_format.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from .blank_lines import BlankLinesVisitor
44
from .normalize_format import NormalizeFormatVisitor
5+
from .remove_trailing_whitespace_visitor import RemoveTrailingWhitespaceVisitor
56
from .spaces_visitor import SpacesVisitor
67
from .normalize_tabs_or_spaces import NormalizeTabsOrSpacesVisitor
78
from .. import TabsAndIndentsStyle
@@ -32,4 +33,5 @@ def visit(self, tree: Optional[Tree], p: P, parent: Optional[Cursor] = None) ->
3233
cu.get_style(TabsAndIndentsStyle) or IntelliJ.tabs_and_indents(),
3334
self._stop_after
3435
).visit(tree, p, self._cursor.fork())
36+
tree = RemoveTrailingWhitespaceVisitor(self._stop_after).visit(tree, self._cursor.fork())
3537
return tree
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from typing import Optional, cast, Union
2+
3+
import rewrite.java as j
4+
import rewrite.python as p
5+
from rewrite import Tree, Marker
6+
from rewrite.java import Space, J, TrailingComma
7+
from rewrite.python import PythonVisitor, CompilationUnit, PySpace
8+
from rewrite.visitor import P, Cursor, T
9+
10+
11+
class RemoveTrailingWhitespaceVisitor(PythonVisitor):
12+
def __init__(self, stop_after: Optional[p.Tree] = None):
13+
self._stop_after = stop_after
14+
self._stop = False
15+
16+
def visit_compilation_unit(self, compilation_unit: CompilationUnit, p: P) -> J:
17+
if not compilation_unit.prefix.comments:
18+
compilation_unit = compilation_unit.with_prefix(Space.EMPTY)
19+
cu: j.CompilationUnit = cast(j.CompilationUnit, super().visit_compilation_unit(compilation_unit, p))
20+
21+
if cu.eof.whitespace:
22+
clean = "".join([_ for _ in cu.eof.whitespace if _ in ['\n', '\r']])
23+
cu = cu.with_eof(cu.eof.with_whitespace(clean))
24+
25+
return cu
26+
27+
def visit_space(self, space: Optional[Space], loc: Optional[Union[PySpace.Location, Space.Location]],
28+
p: P) -> Space:
29+
s = cast(Space, super().visit_space(space, loc, p))
30+
if not s or not s.whitespace:
31+
return s
32+
return self._normalize_whitespace(s)
33+
34+
def visit_marker(self, marker: Marker, p: P) -> Marker:
35+
m = cast(Marker, super().visit_marker(marker, p))
36+
if isinstance(m, TrailingComma):
37+
return m.with_suffix(self._normalize_whitespace(m.suffix))
38+
return m
39+
40+
@staticmethod
41+
def _normalize_whitespace(s):
42+
last_newline = s.whitespace.rfind('\n')
43+
if last_newline > 0:
44+
ws = [c for i, c in enumerate(s.whitespace) if i >= last_newline or c in {'\r', '\n'}]
45+
s = s.with_whitespace(''.join(ws))
46+
return s
47+
48+
def post_visit(self, tree: T, p: P) -> Optional[T]:
49+
if self._stop_after and tree == self._stop_after:
50+
self._stop = True
51+
return tree
52+
53+
def visit(self, tree: Optional[Tree], p: P, parent: Optional[Cursor] = None) -> Optional[T]:
54+
return tree if self._stop else super().visit(tree, p, parent)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import pytest
2+
3+
from rewrite.python.format.remove_trailing_whitespace_visitor import RemoveTrailingWhitespaceVisitor
4+
from rewrite.test import rewrite_run, RecipeSpec, from_visitor, python
5+
6+
7+
@pytest.mark.parametrize('n_spaces', list(range(0, 4)))
8+
@pytest.mark.parametrize('linebreaks', ['\n', '\r\n', '\r\n\n', '\n\n'])
9+
def test_tabs_to_spaces(n_spaces, linebreaks):
10+
# noinspection PyInconsistentIndentation
11+
spaces = ' ' * n_spaces
12+
rewrite_run(
13+
python(
14+
# language=python
15+
f"""\
16+
class Foo:{spaces}{linebreaks}\
17+
def bar(self):
18+
return 42{linebreaks}
19+
{spaces}{linebreaks}
20+
""",
21+
# language=python
22+
f"""\
23+
class Foo:{linebreaks}\
24+
def bar(self):
25+
return 42{linebreaks}
26+
{linebreaks}
27+
"""
28+
),
29+
spec=RecipeSpec()
30+
.with_recipes(
31+
from_visitor(RemoveTrailingWhitespaceVisitor())
32+
)
33+
)
34+
35+
36+
@pytest.mark.parametrize('n_spaces', list(range(0, 4)))
37+
@pytest.mark.parametrize('linebreaks', ['\n', '\r\n', '\r\n\n', '\n\n'])
38+
def test_tabs_to_spaces_with_trailing_comma(n_spaces, linebreaks):
39+
# noinspection PyInconsistentIndentation
40+
spaces = ' ' * n_spaces
41+
rewrite_run(
42+
python(
43+
# language=python
44+
f"""\
45+
def bar():{spaces}{linebreaks}\
46+
return [
47+
1,{spaces}{linebreaks}\
48+
2,
49+
3,{spaces}{linebreaks}\
50+
]
51+
{spaces}{linebreaks}
52+
""",
53+
# language=python
54+
f"""\
55+
def bar():{linebreaks}\
56+
return [
57+
1,{linebreaks}\
58+
2,
59+
3,{linebreaks}\
60+
]
61+
{linebreaks}
62+
"""
63+
),
64+
spec=RecipeSpec()
65+
.with_recipes(
66+
from_visitor(RemoveTrailingWhitespaceVisitor())
67+
)
68+
)

0 commit comments

Comments
 (0)