From 1f7c9a4a27372b50e6a72d3a5d631e64e92ff0ae Mon Sep 17 00:00:00 2001 From: Niels de Bruin Date: Wed, 11 Dec 2024 16:41:57 +0100 Subject: [PATCH 1/6] Add RemoveTrailingWhitespaceVisitor --- .../remove_trailing_whitespace_visitor.py | 36 ++++++++++++++++++ .../all/format/remove_trailing_whitespace.py | 37 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py create mode 100644 rewrite/tests/python/all/format/remove_trailing_whitespace.py diff --git a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py new file mode 100644 index 00000000..3c2a88a2 --- /dev/null +++ b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py @@ -0,0 +1,36 @@ +from typing import Optional, cast, Union + +import rewrite.java as j +import rewrite.python as p +from rewrite.java import Space, J +from rewrite.python import PythonVisitor, CompilationUnit, PySpace +from rewrite.visitor import P + + +class RemoveTrailingWhitespaceVisitor(PythonVisitor): + def __init__(self, stop_after: Optional[p.Tree] = None): + self._stop_after = stop_after + + def visit_compilation_unit(self, compilation_unit: CompilationUnit, p: P) -> J: + if not compilation_unit.prefix.comments: + compilation_unit = compilation_unit.with_prefix(Space.EMPTY) + cu: j.CompilationUnit = cast(j.CompilationUnit, super().visit_compilation_unit(compilation_unit, p)) + + if cu.eof.whitespace: + clean = "".join([_ for _ in cu.eof.whitespace if _ in ['\n', '\r']]) + cu = cu.with_eof(cu.eof.with_whitespace(clean)) + + return cu + + def visit_space(self, space: Optional[Space], loc: Optional[Union[PySpace.Location, Space.Location]], + p: P) -> Space: + s = cast(Space, super().visit_space(space, loc, p)) + + if not s or not s.whitespace: + return s + + last_newline = s.whitespace.rfind('\n') + if last_newline > 0: + ws = [c for i, c in enumerate(s.whitespace) if i >= last_newline or c in {',', '\r', '\n'}] + s = s.with_whitespace(''.join(ws)) + return s diff --git a/rewrite/tests/python/all/format/remove_trailing_whitespace.py b/rewrite/tests/python/all/format/remove_trailing_whitespace.py new file mode 100644 index 00000000..1770ecba --- /dev/null +++ b/rewrite/tests/python/all/format/remove_trailing_whitespace.py @@ -0,0 +1,37 @@ +import pytest + +from rewrite.python.format.remove_trailing_whitespace_visitor import RemoveTrailingWhitespaceVisitor +from rewrite.test import rewrite_run, RecipeSpec, from_visitor, python + + +class Foo: + pass + + +@pytest.mark.parametrize('n_spaces', list(range(0, 4))) +@pytest.mark.parametrize('linebreaks', ['\n', '\r\n', '\r\n\n', '\n\n']) +def test_tabs_to_spaces(n_spaces, linebreaks): + # noinspection PyInconsistentIndentation + spaces = ' ' * n_spaces + rewrite_run( + python( + # language=python + f"""\ + class Foo:{spaces}{linebreaks}\ + def bar(self): + return 42{linebreaks} + {spaces}{linebreaks} + """, + # language=python + f"""\ + class Foo:{linebreaks}\ + def bar(self): + return 42{linebreaks} + {linebreaks} + """ + ), + spec=RecipeSpec() + .with_recipes( + from_visitor(RemoveTrailingWhitespaceVisitor()) + ) + ) From ddf4c3318d0069b7d354132a9ff9e41767600533 Mon Sep 17 00:00:00 2001 From: Niels de Bruin Date: Wed, 11 Dec 2024 16:56:49 +0100 Subject: [PATCH 2/6] Add pre/post visit methods --- .../format/remove_trailing_whitespace_visitor.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py index 3c2a88a2..8eab1b4d 100644 --- a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py +++ b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py @@ -2,9 +2,10 @@ import rewrite.java as j import rewrite.python as p +from rewrite import Tree from rewrite.java import Space, J from rewrite.python import PythonVisitor, CompilationUnit, PySpace -from rewrite.visitor import P +from rewrite.visitor import P, Cursor, T class RemoveTrailingWhitespaceVisitor(PythonVisitor): @@ -34,3 +35,11 @@ def visit_space(self, space: Optional[Space], loc: Optional[Union[PySpace.Locati ws = [c for i, c in enumerate(s.whitespace) if i >= last_newline or c in {',', '\r', '\n'}] s = s.with_whitespace(''.join(ws)) return s + + def post_visit(self, tree: T, p: P) -> Optional[T]: + if self._stop_after and tree == self._stop_after: + self._stop = True + return tree + + def visit(self, tree: Optional[Tree], p: P, parent: Optional[Cursor] = None) -> Optional[T]: + return tree if self._stop else super().visit(tree, p, parent) From c5b5da58d93b92492443c308ea89fcc515a7d34d Mon Sep 17 00:00:00 2001 From: Niels de Bruin Date: Wed, 11 Dec 2024 16:57:57 +0100 Subject: [PATCH 3/6] Fix attribute --- .../rewrite/python/format/remove_trailing_whitespace_visitor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py index 8eab1b4d..5ac7772f 100644 --- a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py +++ b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py @@ -11,6 +11,7 @@ class RemoveTrailingWhitespaceVisitor(PythonVisitor): def __init__(self, stop_after: Optional[p.Tree] = None): self._stop_after = stop_after + self._stop = False def visit_compilation_unit(self, compilation_unit: CompilationUnit, p: P) -> J: if not compilation_unit.prefix.comments: From 59d92d5a99fd3462904572ab7dae4940be16f328 Mon Sep 17 00:00:00 2001 From: Niels de Bruin Date: Thu, 12 Dec 2024 16:51:51 +0100 Subject: [PATCH 4/6] Call RemoveTrailingWhiteSpaceVisitor in AutoFormatVisitor --- rewrite/rewrite/python/format/auto_format.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rewrite/rewrite/python/format/auto_format.py b/rewrite/rewrite/python/format/auto_format.py index d3e94c53..c4f68aed 100644 --- a/rewrite/rewrite/python/format/auto_format.py +++ b/rewrite/rewrite/python/format/auto_format.py @@ -2,6 +2,7 @@ from .blank_lines import BlankLinesVisitor from .normalize_format import NormalizeFormatVisitor +from .remove_trailing_whitespace_visitor import RemoveTrailingWhitespaceVisitor from .spaces_visitor import SpacesVisitor from .normalize_tabs_or_spaces import NormalizeTabsOrSpacesVisitor from .. import TabsAndIndentsStyle @@ -32,4 +33,5 @@ def visit(self, tree: Optional[Tree], p: P, parent: Optional[Cursor] = None) -> cu.get_style(TabsAndIndentsStyle) or IntelliJ.tabs_and_indents(), self._stop_after ).visit(tree, p, self._cursor.fork()) + tree = RemoveTrailingWhitespaceVisitor(self._stop_after).visit(tree, self._cursor.fork()) return tree From 163a9c93ea969246c50ef471cafc5e9e367a4582 Mon Sep 17 00:00:00 2001 From: Niels de Bruin Date: Fri, 13 Dec 2024 11:20:25 +0100 Subject: [PATCH 5/6] Update rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py Co-authored-by: Knut Wannheden --- .../rewrite/python/format/remove_trailing_whitespace_visitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py index 5ac7772f..c25dd6c0 100644 --- a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py +++ b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py @@ -33,7 +33,7 @@ def visit_space(self, space: Optional[Space], loc: Optional[Union[PySpace.Locati last_newline = s.whitespace.rfind('\n') if last_newline > 0: - ws = [c for i, c in enumerate(s.whitespace) if i >= last_newline or c in {',', '\r', '\n'}] + ws = [c for i, c in enumerate(s.whitespace) if i >= last_newline or c in {'\r', '\n'}] s = s.with_whitespace(''.join(ws)) return s From b457e3581cc61edd3543b0a266bc102280d297f9 Mon Sep 17 00:00:00 2001 From: Niels de Bruin Date: Fri, 13 Dec 2024 11:53:25 +0100 Subject: [PATCH 6/6] Fix for trailing comma and clean --- .../remove_trailing_whitespace_visitor.py | 14 +++++-- ...emove_trailing_whitespace_visitor_test.py} | 39 +++++++++++++++++-- 2 files changed, 46 insertions(+), 7 deletions(-) rename rewrite/tests/python/all/format/{remove_trailing_whitespace.py => remove_trailing_whitespace_visitor_test.py} (50%) diff --git a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py index c25dd6c0..81e4e0e4 100644 --- a/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py +++ b/rewrite/rewrite/python/format/remove_trailing_whitespace_visitor.py @@ -2,8 +2,8 @@ import rewrite.java as j import rewrite.python as p -from rewrite import Tree -from rewrite.java import Space, J +from rewrite import Tree, Marker +from rewrite.java import Space, J, TrailingComma from rewrite.python import PythonVisitor, CompilationUnit, PySpace from rewrite.visitor import P, Cursor, T @@ -27,10 +27,18 @@ def visit_compilation_unit(self, compilation_unit: CompilationUnit, p: P) -> J: def visit_space(self, space: Optional[Space], loc: Optional[Union[PySpace.Location, Space.Location]], p: P) -> Space: s = cast(Space, super().visit_space(space, loc, p)) - if not s or not s.whitespace: return s + return self._normalize_whitespace(s) + + def visit_marker(self, marker: Marker, p: P) -> Marker: + m = cast(Marker, super().visit_marker(marker, p)) + if isinstance(m, TrailingComma): + return m.with_suffix(self._normalize_whitespace(m.suffix)) + return m + @staticmethod + def _normalize_whitespace(s): last_newline = s.whitespace.rfind('\n') if last_newline > 0: ws = [c for i, c in enumerate(s.whitespace) if i >= last_newline or c in {'\r', '\n'}] diff --git a/rewrite/tests/python/all/format/remove_trailing_whitespace.py b/rewrite/tests/python/all/format/remove_trailing_whitespace_visitor_test.py similarity index 50% rename from rewrite/tests/python/all/format/remove_trailing_whitespace.py rename to rewrite/tests/python/all/format/remove_trailing_whitespace_visitor_test.py index 1770ecba..d3ac82d3 100644 --- a/rewrite/tests/python/all/format/remove_trailing_whitespace.py +++ b/rewrite/tests/python/all/format/remove_trailing_whitespace_visitor_test.py @@ -4,10 +4,6 @@ from rewrite.test import rewrite_run, RecipeSpec, from_visitor, python -class Foo: - pass - - @pytest.mark.parametrize('n_spaces', list(range(0, 4))) @pytest.mark.parametrize('linebreaks', ['\n', '\r\n', '\r\n\n', '\n\n']) def test_tabs_to_spaces(n_spaces, linebreaks): @@ -35,3 +31,38 @@ def bar(self): from_visitor(RemoveTrailingWhitespaceVisitor()) ) ) + + +@pytest.mark.parametrize('n_spaces', list(range(0, 4))) +@pytest.mark.parametrize('linebreaks', ['\n', '\r\n', '\r\n\n', '\n\n']) +def test_tabs_to_spaces_with_trailing_comma(n_spaces, linebreaks): + # noinspection PyInconsistentIndentation + spaces = ' ' * n_spaces + rewrite_run( + python( + # language=python + f"""\ + def bar():{spaces}{linebreaks}\ + return [ + 1,{spaces}{linebreaks}\ + 2, + 3,{spaces}{linebreaks}\ + ] + {spaces}{linebreaks} + """, + # language=python + f"""\ + def bar():{linebreaks}\ + return [ + 1,{linebreaks}\ + 2, + 3,{linebreaks}\ + ] + {linebreaks} + """ + ), + spec=RecipeSpec() + .with_recipes( + from_visitor(RemoveTrailingWhitespaceVisitor()) + ) + )