Skip to content

Commit 7a67bbe

Browse files
committed
Issue #124: Implement recipe for NewlineAtEndOfFile
1 parent a0ae57d commit 7a67bbe

File tree

12 files changed

+290
-5
lines changed

12 files changed

+290
-5
lines changed

.gitattributes

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# All files default to text with LF eol
2+
* eol=lf
3+
4+
# Test resources that need specific eol
5+
/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/InputNewlineAtEndOfFileCrlf.java eol=crlf
6+
/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/DiffNewlineAtEndOfFileCrlf.diff binary

src/main/java/org/checkstyle/autofix/CheckstyleCheck.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
public enum CheckstyleCheck {
2424
FINAL_LOCAL_VARIABLE("com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"),
2525
HEADER("com.puppycrawl.tools.checkstyle.checks.header.HeaderCheck"),
26+
NEWLINE_AT_END_OF_FILE("com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck"),
2627
UPPER_ELL("com.puppycrawl.tools.checkstyle.checks.UpperEllCheck"),
2728
HEX_LITERAL_CASE("com.puppycrawl.tools.checkstyle.checks.HexLiteralCaseCheck"),
2829
REDUNDANT_IMPORT("com.puppycrawl.tools.checkstyle.checks.imports.RedundantImportCheck");

src/main/java/org/checkstyle/autofix/CheckstyleRecipeRegistry.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.checkstyle.autofix.recipe.FinalLocalVariable;
3131
import org.checkstyle.autofix.recipe.Header;
3232
import org.checkstyle.autofix.recipe.HexLiteralCase;
33+
import org.checkstyle.autofix.recipe.NewlineAtEndOfFile;
3334
import org.checkstyle.autofix.recipe.RedundantImport;
3435
import org.checkstyle.autofix.recipe.UpperEll;
3536
import org.openrewrite.Recipe;
@@ -48,6 +49,7 @@ public final class CheckstyleRecipeRegistry {
4849
RECIPE_MAP.put(CheckstyleCheck.HEX_LITERAL_CASE, HexLiteralCase::new);
4950
RECIPE_MAP.put(CheckstyleCheck.FINAL_LOCAL_VARIABLE, FinalLocalVariable::new);
5051
RECIPE_MAP_WITH_CONFIG.put(CheckstyleCheck.HEADER, Header::new);
52+
RECIPE_MAP_WITH_CONFIG.put(CheckstyleCheck.NEWLINE_AT_END_OF_FILE, NewlineAtEndOfFile::new);
5153
RECIPE_MAP.put(CheckstyleCheck.REDUNDANT_IMPORT, RedundantImport::new);
5254
}
5355

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
///////////////////////////////////////////////////////////////////////////////////////////////
2+
// checkstyle-openrewrite-recipes: Automatically fix Checkstyle violations with OpenRewrite.
3+
// Copyright (C) 2025 The Checkstyle OpenRewrite Recipes Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
///////////////////////////////////////////////////////////////////////////////////////////////
17+
18+
package org.checkstyle.autofix.recipe;
19+
20+
import java.nio.file.Path;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.function.UnaryOperator;
24+
25+
import org.checkstyle.autofix.parser.CheckConfiguration;
26+
import org.checkstyle.autofix.parser.CheckstyleViolation;
27+
import org.openrewrite.ExecutionContext;
28+
import org.openrewrite.Recipe;
29+
import org.openrewrite.Tree;
30+
import org.openrewrite.TreeVisitor;
31+
import org.openrewrite.java.JavaIsoVisitor;
32+
import org.openrewrite.java.format.AutodetectGeneralFormatStyle;
33+
import org.openrewrite.java.tree.Comment;
34+
import org.openrewrite.java.tree.J;
35+
import org.openrewrite.java.tree.JavaSourceFile;
36+
import org.openrewrite.java.tree.Space;
37+
import org.openrewrite.style.GeneralFormatStyle;
38+
import org.openrewrite.style.Style;
39+
40+
public class NewlineAtEndOfFile extends Recipe {
41+
42+
private final List<CheckstyleViolation> violations;
43+
private final CheckConfiguration config;
44+
45+
public NewlineAtEndOfFile(List<CheckstyleViolation> violations, CheckConfiguration config) {
46+
this.violations = violations;
47+
this.config = config;
48+
}
49+
50+
@Override
51+
public String getDisplayName() {
52+
return "End files with a single newline";
53+
}
54+
55+
@Override
56+
public String getDescription() {
57+
return "Some tools work better when files end with an empty line.";
58+
}
59+
60+
@Override
61+
public TreeVisitor<?, ExecutionContext> getVisitor() {
62+
final String lineSeparator = config.getProperty("lineSeparator");
63+
return new NewLineAtEndOfFileVisitor(violations, lineSeparator);
64+
}
65+
66+
private static class NewLineAtEndOfFileVisitor extends JavaIsoVisitor<ExecutionContext> {
67+
private final List<CheckstyleViolation> violations;
68+
private final String lineSeparatorConfig;
69+
70+
NewLineAtEndOfFileVisitor(List<CheckstyleViolation> violations,
71+
String lineSeparatorConfig) {
72+
this.violations = violations;
73+
this.lineSeparatorConfig = lineSeparatorConfig;
74+
}
75+
76+
@Override
77+
public J visit(Tree tree, ExecutionContext executionContext) {
78+
J result = (J) tree;
79+
80+
if (tree instanceof JavaSourceFile) {
81+
final JavaSourceFile sourceFile = (JavaSourceFile) tree;
82+
83+
final Path filePath = sourceFile.getSourcePath().toAbsolutePath();
84+
85+
if (hasViolation(filePath)) {
86+
final String lineEnding = determineLineEnding(sourceFile);
87+
88+
final Space eof = sourceFile.getEof();
89+
final String lastWhitespace = eof.getLastWhitespace();
90+
91+
if (!lineEnding.equals(lastWhitespace)) {
92+
final List<Comment> comments = eof.getComments();
93+
if (comments.isEmpty()) {
94+
result = sourceFile.withEof(Space.format(lineEnding));
95+
}
96+
else {
97+
result = sourceFile.withEof(sourceFile.getEof().withComments(
98+
mapLast(comments, comment -> comment.withSuffix(lineEnding))));
99+
}
100+
}
101+
}
102+
}
103+
104+
return result;
105+
}
106+
107+
private String determineLineEnding(JavaSourceFile sourceFile) {
108+
return switch (lineSeparatorConfig.toLowerCase()) {
109+
case "lf" -> "\n";
110+
case "crlf" -> "\r\n";
111+
case "cr" -> "\r";
112+
case "system" -> System.lineSeparator();
113+
case "lf_cr_crlf" -> getAutodetectedLineEnding(sourceFile);
114+
default -> {
115+
throw new IllegalStateException("Unexpected value: "
116+
+ lineSeparatorConfig.toLowerCase());
117+
}
118+
};
119+
}
120+
121+
private String getAutodetectedLineEnding(JavaSourceFile sourceFile) {
122+
final GeneralFormatStyle generalFormatStyle =
123+
Style.from(GeneralFormatStyle.class, sourceFile, () -> {
124+
return AutodetectGeneralFormatStyle
125+
.autodetectGeneralFormatStyle(sourceFile);
126+
});
127+
return generalFormatStyle.newLine();
128+
}
129+
130+
private static List<Comment> mapLast(List<Comment> comments,
131+
UnaryOperator<Comment> mapper) {
132+
List<Comment> result = comments;
133+
134+
if (comments != null && !comments.isEmpty()) {
135+
final int lastIndex = comments.size() - 1;
136+
final Comment last = comments.get(lastIndex);
137+
final Comment newLast = mapper.apply(last);
138+
139+
if (last != newLast) {
140+
result = new ArrayList<>(comments);
141+
result.set(lastIndex, newLast);
142+
}
143+
}
144+
145+
return result;
146+
}
147+
148+
private boolean hasViolation(Path filePath) {
149+
return violations.removeIf(violation -> {
150+
return violation.getFilePath().endsWith(filePath);
151+
});
152+
}
153+
}
154+
}

src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTestSupport.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ private void verify(Configuration config, Path reportPath, String inputPath, Str
100100
try {
101101
final Recipe mainRecipe = new CheckstyleAutoFix(reportPath.toString(),
102102
configPath.toString());
103-
final String beforeCode = readFile(getPath(inputPath));
104-
final String expectedAfterCode = readFile(getPath(outputPath));
103+
final String beforeCode = Files.readString(Path.of(getPath(inputPath)));
104+
final String expectedAfterCode = Files.readString(Path.of(getPath(outputPath)));
105+
105106
testRecipe(beforeCode, expectedAfterCode,
106107
getPath(inputPath), new InputClassRenamer(),
107108
mainRecipe, new RemoveViolationComments());
@@ -194,8 +195,14 @@ private Configuration getCheckConfigurations(String inputPath) throws Exception
194195
private String[] convertToExpectedMessages(List<CheckstyleViolation> violations) {
195196
return violations.stream()
196197
.map(violation -> {
197-
return violation.getLine() + ":"
198-
+ violation.getColumn() + ": " + violation.getMessage();
198+
String message = violation.getLine() + ":";
199+
if (violation.getColumn() != -1) {
200+
message += violation.getColumn() + ": ";
201+
}
202+
else {
203+
message += " ";
204+
}
205+
return message + violation.getMessage();
199206
})
200207
.toArray(String[]::new);
201208
}
@@ -205,7 +212,7 @@ private void testRecipe(String beforeCode, String expectedAfterCode,
205212
assertDoesNotThrow(() -> {
206213
rewriteRun(
207214
spec -> spec.recipes(recipes),
208-
java(beforeCode, expectedAfterCode, spec -> spec.path(filePath))
215+
java(beforeCode, expectedAfterCode, spec -> spec.path(filePath).noTrim())
209216
);
210217
});
211218
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
///////////////////////////////////////////////////////////////////////////////////////////////
2+
// checkstyle-openrewrite-recipes: Automatically fix Checkstyle violations with OpenRewrite.
3+
// Copyright (C) 2025 The Checkstyle OpenRewrite Recipes Authors
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
///////////////////////////////////////////////////////////////////////////////////////////////
17+
18+
package org.checkstyle.autofix.recipe;
19+
20+
import org.checkstyle.autofix.parser.ReportParser;
21+
22+
public class NewLineAtEndOfFileTest extends AbstractRecipeTestSupport {
23+
@Override
24+
protected String getSubpackage() {
25+
return "newlineatendoffile";
26+
}
27+
28+
@RecipeTest
29+
void newLineCrlf(ReportParser parser) throws Exception {
30+
verify(parser, "NewlineAtEndOfFileCrlf");
31+
}
32+
33+
@RecipeTest
34+
void noNewLine(ReportParser parser) throws Exception {
35+
verify(parser, "NewlineAtEndOfFileNoNewLine");
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
--- src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/InputNewlineAtEndOfFileCrlf.java
2+
+++ src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/OutputNewlineAtEndOfFileCrlf.java
3+
@@ -4,9 +4,8 @@
4+
fileExtensions = (default)""
5+
6+
*/
7+
-// violation 6 lines above 'Expected line ending for file is LF(\\n), but CRLF(\\r\\n) is detected.'
8+
9+
package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilecrlf;
10+
11+
-public class InputNewlineAtEndOfFileCrlf {
12+
-}
13+
+public class OutputNewlineAtEndOfFileCrlf {
14+
+}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/*
2+
com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck
3+
lineSeparator = lf
4+
fileExtensions = (default)""
5+
6+
*/
7+
// violation 6 lines above 'Expected line ending for file is LF(\\n), but CRLF(\\r\\n) is detected.'
8+
9+
package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilecrlf;
10+
11+
public class InputNewlineAtEndOfFileCrlf {
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck
3+
lineSeparator = lf
4+
fileExtensions = (default)""
5+
6+
*/
7+
8+
package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilecrlf;
9+
10+
public class OutputNewlineAtEndOfFileCrlf {
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
--- src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/InputNewlineAtEndOfFileNoNewLine.java
2+
+++ src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/OutputNewlineAtEndOfFileNoNewLine.java
3+
@@ -4,10 +4,9 @@
4+
fileExtensions = (default)""
5+
6+
*/
7+
-// violation 6 lines above 'File does not end with a newline.'
8+
9+
package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilenonewline;
10+
11+
-public interface InputNewlineAtEndOfFileNoNewLine
12+
+public interface OutputNewlineAtEndOfFileNoNewLine
13+
{
14+
-}
15+
\ No newline at end of file
16+
+}

0 commit comments

Comments
 (0)