Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.staticanalysis;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.text.PlainText;
import org.openrewrite.text.PlainTextVisitor;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

@EqualsAndHashCode(callSuper = false)
@Value
public class RemoveTrailingWhitespace extends Recipe {

private static final Pattern TRAILING_WHITESPACE = Pattern.compile("[ \\t]+(?=\\r?\\n|$)", Pattern.MULTILINE);

@Override
public String getDisplayName() {
return "Remove trailing whitespace from text files";
}

@Override
public String getDescription() {
return "Removes trailing whitespace (spaces and tabs) from the end of lines in text files. " +
"This helps maintain clean code formatting and prevents unnecessary whitespace in version control.";
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new PlainTextVisitor<ExecutionContext>() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A PlainTextVisitor does not act on Java files. If you would want to "downcast" the Java lst as a text lst, you could build a generic TreeVisitor that does that conversion.

In this case though you do not want to do that as you will loose all rich information of the lst and every recipe that should run on java files after this one will not make any changes as the Lst's at that moment are no longer JavaSourceFiles but PlainText Sourcefiles causing the isAcceptable of the JavaVisitor to skip the converted files.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Override
public PlainText visitText(PlainText text, ExecutionContext ctx) {
String content = text.getText();
Matcher matcher = TRAILING_WHITESPACE.matcher(content);
if (matcher.find()) {
String cleanedContent = matcher.replaceAll("");
return text.withText(cleanedContent);
}
return text;
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* Copyright 2025 the original author or authors.
* <p>
* Licensed under the Moderne Source Available License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* https://docs.moderne.io/licensing/moderne-source-available-license
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openrewrite.staticanalysis;

import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;

import static org.openrewrite.test.SourceSpecs.text;

class RemoveTrailingWhitespaceTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new RemoveTrailingWhitespace());
}

@DocumentExample
@Test
void removeTrailingSpaces() {
rewriteRun(
text(
"Line with trailing spaces \n" +
"Line without trailing spaces\n" +
"Another line with spaces \n",
"Line with trailing spaces\n" +
"Line without trailing spaces\n" +
"Another line with spaces\n"
Comment on lines +37 to +42
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Line with trailing spaces \n" +
"Line without trailing spaces\n" +
"Another line with spaces \n",
"Line with trailing spaces\n" +
"Line without trailing spaces\n" +
"Another line with spaces\n"
"""
Line with trailing spaces \s
Line without trailing spaces
Another line with spaces \s
""",
"""
Line with trailing spaces
Line without trailing spaces
Another line with spaces
"""

)
);
}

@Test
void removeTrailingTabs() {
rewriteRun(
text(
"Line with trailing tabs\t\t\n" +
"Line without trailing tabs\n" +
"Mixed tabs and spaces\t \n",
"Line with trailing tabs\n" +
"Line without trailing tabs\n" +
"Mixed tabs and spaces\n"
Comment on lines +51 to +56
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Line with trailing tabs\t\t\n" +
"Line without trailing tabs\n" +
"Mixed tabs and spaces\t \n",
"Line with trailing tabs\n" +
"Line without trailing tabs\n" +
"Mixed tabs and spaces\n"
"""
Line with trailing tabs
Line without trailing tabs
Mixed tabs and spaces \s
""",
"""
Line with trailing tabs
Line without trailing tabs
Mixed tabs and spaces
"""

)
);
}

@Test
void removeTrailingWhitespaceFromLastLine() {
rewriteRun(
text(
"First line\n" +
"Second line ",
"First line\n" +
"Second line"
Comment on lines +65 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"First line\n" +
"Second line ",
"First line\n" +
"Second line"
"""
First line
Second line """,
"""
First line
Second line"""

)
);
}

@Test
void preserveEmptyLines() {
rewriteRun(
text(
"Line 1 \n" +
"\n" +
"Line 3\t\n" +
"\n" +
"Line 5",
"Line 1\n" +
"\n" +
"Line 3\n" +
"\n" +
"Line 5"
Comment on lines +77 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Line 1 \n" +
"\n" +
"Line 3\t\n" +
"\n" +
"Line 5",
"Line 1\n" +
"\n" +
"Line 3\n" +
"\n" +
"Line 5"
"""
Line 1 \s
Line 3
Line 5""",
"""
Line 1
Line 3
Line 5"""

)
);
}

@Test
void handleMultipleConsecutiveWhitespaceTypes() {
rewriteRun(
text(
"Spaces then tabs \t\t\n" +
"Tabs then spaces\t\t \n" +
"Mixed whitespace \t \t \n",
"Spaces then tabs\n" +
"Tabs then spaces\n" +
"Mixed whitespace\n"
Comment on lines +95 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Spaces then tabs \t\t\n" +
"Tabs then spaces\t\t \n" +
"Mixed whitespace \t \t \n",
"Spaces then tabs\n" +
"Tabs then spaces\n" +
"Mixed whitespace\n"
"""
Spaces then tabs
Tabs then spaces \s
Mixed whitespace \s
""",
"""
Spaces then tabs
Tabs then spaces
Mixed whitespace
"""

)
);
}

@Test
void noChangesWhenNoTrailingWhitespace() {
rewriteRun(
text(
"Clean line 1\n" +
"Clean line 2\n" +
"Clean line 3"
Comment on lines +109 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Clean line 1\n" +
"Clean line 2\n" +
"Clean line 3"
"""
Clean line 1
Clean line 2
Clean line 3"""

)
);
}

@Test
void handleEmptyFile() {
rewriteRun(
text("")
);
}

@Test
void handleLinesWithOnlyWhitespace() {
rewriteRun(
text(
"line with content\n" +
" \n" +
"another line",
"line with content\n" +
"\n" +
"another line"
Comment on lines +127 to +132
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"line with content\n" +
" \n" +
"another line",
"line with content\n" +
"\n" +
"another line"
"""
line with content
\s
another line""",
"""
line with content
another line"""

)
);
}

@Test
void preserveIndentationWhitespace() {
rewriteRun(
text(
" Indented line \n" +
"\tTab indented line\t\n" +
" Mixed indentation\t ",
" Indented line\n" +
"\tTab indented line\n" +
" Mixed indentation"
Comment on lines +141 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
" Indented line \n" +
"\tTab indented line\t\n" +
" Mixed indentation\t ",
" Indented line\n" +
"\tTab indented line\n" +
" Mixed indentation"
"""
Indented line \s
Tab indented line
Mixed indentation """,
"""
Indented line
Tab indented line
Mixed indentation"""

)
);
}

@Test
void handleWindowsLineEndings() {
rewriteRun(
text(
"Windows line 1 \r\n" +
"Windows line 2\t\r\n" +
"Windows line 3",
"Windows line 1\r\n" +
"Windows line 2\r\n" +
"Windows line 3"
)
);
}

@Test
void handleMacLineEndings() {
rewriteRun(
text(
"Mac line 1 \r" +
"Mac line 2\t\r" +
"Mac line 3",
"Mac line 1\r" +
"Mac line 2\r" +
"Mac line 3"
)
);
}

@Test
void handleLargeAmountOfTrailingWhitespace() {
StringBuilder input = new StringBuilder();
StringBuilder expected = new StringBuilder();

// Create lines with varying amounts of trailing whitespace
for (int i = 1; i <= 10; i++) {
input.append("Line ").append(i).repeat(" ", i);
expected.append("Line ").append(i);

if (i < 10) {
input.append("\n");
expected.append("\n");
}
}

rewriteRun(
text(
input.toString(),
expected.toString()
)
);
}
}