diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0fa6c49 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Test resources that need specific eol +src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/** -text + +# Force LF line endings for diff test files +*.diff text eol=lf \ No newline at end of file diff --git a/src/main/java/org/checkstyle/autofix/CheckstyleCheck.java b/src/main/java/org/checkstyle/autofix/CheckstyleCheck.java index fef6b72..946a2d3 100644 --- a/src/main/java/org/checkstyle/autofix/CheckstyleCheck.java +++ b/src/main/java/org/checkstyle/autofix/CheckstyleCheck.java @@ -23,6 +23,7 @@ public enum CheckstyleCheck { FINAL_LOCAL_VARIABLE("com.puppycrawl.tools.checkstyle.checks.coding.FinalLocalVariableCheck"), HEADER("com.puppycrawl.tools.checkstyle.checks.header.HeaderCheck"), + NEWLINE_AT_END_OF_FILE("com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck"), UPPER_ELL("com.puppycrawl.tools.checkstyle.checks.UpperEllCheck"), HEX_LITERAL_CASE("com.puppycrawl.tools.checkstyle.checks.HexLiteralCaseCheck"), REDUNDANT_IMPORT("com.puppycrawl.tools.checkstyle.checks.imports.RedundantImportCheck"); diff --git a/src/main/java/org/checkstyle/autofix/CheckstyleRecipeRegistry.java b/src/main/java/org/checkstyle/autofix/CheckstyleRecipeRegistry.java index 997be2b..5d2c299 100644 --- a/src/main/java/org/checkstyle/autofix/CheckstyleRecipeRegistry.java +++ b/src/main/java/org/checkstyle/autofix/CheckstyleRecipeRegistry.java @@ -30,6 +30,7 @@ import org.checkstyle.autofix.recipe.FinalLocalVariable; import org.checkstyle.autofix.recipe.Header; import org.checkstyle.autofix.recipe.HexLiteralCase; +import org.checkstyle.autofix.recipe.NewlineAtEndOfFile; import org.checkstyle.autofix.recipe.RedundantImport; import org.checkstyle.autofix.recipe.UpperEll; import org.openrewrite.Recipe; @@ -48,6 +49,7 @@ public final class CheckstyleRecipeRegistry { RECIPE_MAP.put(CheckstyleCheck.HEX_LITERAL_CASE, HexLiteralCase::new); RECIPE_MAP.put(CheckstyleCheck.FINAL_LOCAL_VARIABLE, FinalLocalVariable::new); RECIPE_MAP_WITH_CONFIG.put(CheckstyleCheck.HEADER, Header::new); + RECIPE_MAP_WITH_CONFIG.put(CheckstyleCheck.NEWLINE_AT_END_OF_FILE, NewlineAtEndOfFile::new); RECIPE_MAP.put(CheckstyleCheck.REDUNDANT_IMPORT, RedundantImport::new); } diff --git a/src/main/java/org/checkstyle/autofix/recipe/Header.java b/src/main/java/org/checkstyle/autofix/recipe/Header.java index 0b07f94..06fdc41 100644 --- a/src/main/java/org/checkstyle/autofix/recipe/Header.java +++ b/src/main/java/org/checkstyle/autofix/recipe/Header.java @@ -39,7 +39,7 @@ public class Header extends Recipe { private static final String HEADER_PROPERTY = "header"; private static final String HEADER_FILE_PROPERTY = "headerFile"; private static final String CHARSET_PROPERTY = "charset"; - private static final String LINE_SEPARATOR = "\n"; + private static final String LINE_SEPARATOR = System.lineSeparator(); private final List violations; private final CheckConfiguration config; @@ -75,7 +75,7 @@ private static String extractLicenseHeader(CheckConfiguration config) { .getPropertyOrDefault(CHARSET_PROPERTY, Charset.defaultCharset().name())); final String headerFilePath = config.getProperty(HEADER_FILE_PROPERTY); try { - header = toLfLineEnding(Files.readString(Path.of(headerFilePath), charsetToUse)); + header = Files.readString(Path.of(headerFilePath), charsetToUse); } catch (IOException exception) { throw new IllegalArgumentException("Failed to extract header from config", @@ -85,10 +85,6 @@ private static String extractLicenseHeader(CheckConfiguration config) { return header; } - private static String toLfLineEnding(String text) { - return text.replaceAll("(?x)\\\\r(?=\\\\n)|\\r(?=\\n)", ""); - } - private static class HeaderVisitor extends JavaIsoVisitor { private final List violations; private final String licenseHeader; @@ -121,8 +117,7 @@ public J visit(Tree tree, ExecutionContext executionContext) { private String extractCurrentHeader(JavaSourceFile sourceFile) { return sourceFile.getComments().stream() .map(comment -> { - return comment.printComment(getCursor()) - + toLfLineEnding(comment.getSuffix()); + return comment.printComment(getCursor()) + comment.getSuffix(); }) .collect(Collectors.joining("")); } diff --git a/src/main/java/org/checkstyle/autofix/recipe/NewlineAtEndOfFile.java b/src/main/java/org/checkstyle/autofix/recipe/NewlineAtEndOfFile.java new file mode 100644 index 0000000..68e7367 --- /dev/null +++ b/src/main/java/org/checkstyle/autofix/recipe/NewlineAtEndOfFile.java @@ -0,0 +1,154 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// checkstyle-openrewrite-recipes: Automatically fix Checkstyle violations with OpenRewrite. +// Copyright (C) 2025 The Checkstyle OpenRewrite Recipes Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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.checkstyle.autofix.recipe; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import org.checkstyle.autofix.parser.CheckConfiguration; +import org.checkstyle.autofix.parser.CheckstyleViolation; +import org.openrewrite.ExecutionContext; +import org.openrewrite.Recipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.format.AutodetectGeneralFormatStyle; +import org.openrewrite.java.tree.Comment; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.java.tree.Space; +import org.openrewrite.style.GeneralFormatStyle; +import org.openrewrite.style.Style; + +public class NewlineAtEndOfFile extends Recipe { + + private final List violations; + private final CheckConfiguration config; + + public NewlineAtEndOfFile(List violations, CheckConfiguration config) { + this.violations = violations; + this.config = config; + } + + @Override + public String getDisplayName() { + return "End files with a single newline"; + } + + @Override + public String getDescription() { + return "Some tools work better when files end with an empty line."; + } + + @Override + public TreeVisitor getVisitor() { + final String lineSeparator = config.getProperty("lineSeparator"); + return new NewLineAtEndOfFileVisitor(violations, lineSeparator); + } + + private static class NewLineAtEndOfFileVisitor extends JavaIsoVisitor { + private final List violations; + private final String lineSeparatorConfig; + + NewLineAtEndOfFileVisitor(List violations, + String lineSeparatorConfig) { + this.violations = violations; + this.lineSeparatorConfig = lineSeparatorConfig; + } + + @Override + public J visit(Tree tree, ExecutionContext executionContext) { + J result = (J) tree; + + if (tree instanceof JavaSourceFile) { + final JavaSourceFile sourceFile = (JavaSourceFile) tree; + + final Path filePath = sourceFile.getSourcePath().toAbsolutePath(); + + if (hasViolation(filePath)) { + final String lineEnding = determineLineEnding(sourceFile); + + final Space eof = sourceFile.getEof(); + final String lastWhitespace = eof.getLastWhitespace(); + + if (!lineEnding.equals(lastWhitespace)) { + final List comments = eof.getComments(); + if (comments.isEmpty()) { + result = sourceFile.withEof(Space.format(lineEnding)); + } + else { + result = sourceFile.withEof(sourceFile.getEof().withComments( + mapLast(comments, comment -> comment.withSuffix(lineEnding)))); + } + } + } + } + + return result; + } + + private String determineLineEnding(JavaSourceFile sourceFile) { + return switch (lineSeparatorConfig.toLowerCase()) { + case "lf" -> "\n"; + case "crlf" -> "\r\n"; + case "cr" -> "\r"; + case "system" -> System.lineSeparator(); + case "lf_cr_crlf" -> getAutodetectedLineEnding(sourceFile); + default -> { + throw new IllegalStateException("Unexpected value: " + + lineSeparatorConfig.toLowerCase()); + } + }; + } + + private String getAutodetectedLineEnding(JavaSourceFile sourceFile) { + final GeneralFormatStyle generalFormatStyle = + Style.from(GeneralFormatStyle.class, sourceFile, () -> { + return AutodetectGeneralFormatStyle + .autodetectGeneralFormatStyle(sourceFile); + }); + return generalFormatStyle.newLine(); + } + + private static List mapLast(List comments, + UnaryOperator mapper) { + List result = comments; + + if (comments != null && !comments.isEmpty()) { + final int lastIndex = comments.size() - 1; + final Comment last = comments.get(lastIndex); + final Comment newLast = mapper.apply(last); + + if (last != newLast) { + result = new ArrayList<>(comments); + result.set(lastIndex, newLast); + } + } + + return result; + } + + private boolean hasViolation(Path filePath) { + return violations.removeIf(violation -> { + return violation.getFilePath().endsWith(filePath); + }); + } + } +} diff --git a/src/test/java/org/checkstyle/autofix/generator/GenerateDiffFilesTest.java b/src/test/java/org/checkstyle/autofix/generator/GenerateDiffFilesTest.java index a0d3d90..90dea49 100644 --- a/src/test/java/org/checkstyle/autofix/generator/GenerateDiffFilesTest.java +++ b/src/test/java/org/checkstyle/autofix/generator/GenerateDiffFilesTest.java @@ -84,6 +84,7 @@ private void createDiff(Path inputFile) throws IOException, InterruptedException } final String diff = out.toString(); - Files.writeString(diffFile, diff); + final String normalizedDiff = diff.replaceAll("\\r\\n?", "\n"); + Files.writeString(diffFile, normalizedDiff); } } diff --git a/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTestSupport.java b/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTestSupport.java index 8fcb4e0..5d4509a 100644 --- a/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTestSupport.java +++ b/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTestSupport.java @@ -100,8 +100,9 @@ private void verify(Configuration config, Path reportPath, String inputPath, Str try { final Recipe mainRecipe = new CheckstyleAutoFix(reportPath.toString(), configPath.toString()); - final String beforeCode = readFile(getPath(inputPath)); - final String expectedAfterCode = readFile(getPath(outputPath)); + final String beforeCode = Files.readString(Path.of(getPath(inputPath))); + final String expectedAfterCode = Files.readString(Path.of(getPath(outputPath))); + testRecipe(beforeCode, expectedAfterCode, getPath(inputPath), new InputClassRenamer(), mainRecipe, new RemoveViolationComments()); @@ -194,8 +195,14 @@ private Configuration getCheckConfigurations(String inputPath) throws Exception private String[] convertToExpectedMessages(List violations) { return violations.stream() .map(violation -> { - return violation.getLine() + ":" - + violation.getColumn() + ": " + violation.getMessage(); + String message = violation.getLine() + ":"; + if (violation.getColumn() != -1) { + message += violation.getColumn() + ": "; + } + else { + message += " "; + } + return message + violation.getMessage(); }) .toArray(String[]::new); } @@ -205,7 +212,7 @@ private void testRecipe(String beforeCode, String expectedAfterCode, assertDoesNotThrow(() -> { rewriteRun( spec -> spec.recipes(recipes), - java(beforeCode, expectedAfterCode, spec -> spec.path(filePath)) + java(beforeCode, expectedAfterCode, spec -> spec.path(filePath).noTrim()) ); }); } diff --git a/src/test/java/org/checkstyle/autofix/recipe/NewLineAtEndOfFileTest.java b/src/test/java/org/checkstyle/autofix/recipe/NewLineAtEndOfFileTest.java new file mode 100644 index 0000000..80ad045 --- /dev/null +++ b/src/test/java/org/checkstyle/autofix/recipe/NewLineAtEndOfFileTest.java @@ -0,0 +1,37 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// checkstyle-openrewrite-recipes: Automatically fix Checkstyle violations with OpenRewrite. +// Copyright (C) 2025 The Checkstyle OpenRewrite Recipes Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// 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.checkstyle.autofix.recipe; + +import org.checkstyle.autofix.parser.ReportParser; + +public class NewLineAtEndOfFileTest extends AbstractRecipeTestSupport { + @Override + protected String getSubpackage() { + return "newlineatendoffile"; + } + + @RecipeTest + void newLineCrlf(ReportParser parser) throws Exception { + verify(parser, "NewlineAtEndOfFileCrlf"); + } + + @RecipeTest + void noNewLine(ReportParser parser) throws Exception { + verify(parser, "NewlineAtEndOfFileNoNewLine"); + } +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/DiffNewlineAtEndOfFileCrlf.diff b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/DiffNewlineAtEndOfFileCrlf.diff new file mode 100644 index 0000000..dc06e3d --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/DiffNewlineAtEndOfFileCrlf.diff @@ -0,0 +1,14 @@ +--- src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/InputNewlineAtEndOfFileCrlf.java ++++ src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/OutputNewlineAtEndOfFileCrlf.java +@@ -4,9 +4,8 @@ + fileExtensions = (default)"" + + */ +-// violation 6 lines above 'Expected line ending for file is LF(\\n), but CRLF(\\r\\n) is detected.' + + package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilecrlf; + +-public class InputNewlineAtEndOfFileCrlf { +-} ++public class OutputNewlineAtEndOfFileCrlf { ++} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/InputNewlineAtEndOfFileCrlf.java b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/InputNewlineAtEndOfFileCrlf.java new file mode 100644 index 0000000..9efa4c3 --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/InputNewlineAtEndOfFileCrlf.java @@ -0,0 +1,12 @@ +/* +com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck +lineSeparator = lf +fileExtensions = (default)"" + +*/ +// violation 6 lines above 'Expected line ending for file is LF(\\n), but CRLF(\\r\\n) is detected.' + +package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilecrlf; + +public class InputNewlineAtEndOfFileCrlf { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/OutputNewlineAtEndOfFileCrlf.java b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/OutputNewlineAtEndOfFileCrlf.java new file mode 100644 index 0000000..e5eab49 --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilecrlf/OutputNewlineAtEndOfFileCrlf.java @@ -0,0 +1,11 @@ +/* +com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck +lineSeparator = lf +fileExtensions = (default)"" + +*/ + +package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilecrlf; + +public class OutputNewlineAtEndOfFileCrlf { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/DiffNewlineAtEndOfFileNoNewLine.diff b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/DiffNewlineAtEndOfFileNoNewLine.diff new file mode 100644 index 0000000..2cf2734 --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/DiffNewlineAtEndOfFileNoNewLine.diff @@ -0,0 +1,16 @@ +--- src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/InputNewlineAtEndOfFileNoNewLine.java ++++ src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/OutputNewlineAtEndOfFileNoNewLine.java +@@ -4,10 +4,9 @@ + fileExtensions = (default)"" + + */ +-// violation 6 lines above 'File does not end with a newline.' + + package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilenonewline; + +-public interface InputNewlineAtEndOfFileNoNewLine ++public interface OutputNewlineAtEndOfFileNoNewLine + { +-} +\ No newline at end of file ++} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/InputNewlineAtEndOfFileNoNewLine.java b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/InputNewlineAtEndOfFileNoNewLine.java new file mode 100644 index 0000000..b011ee1 --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/InputNewlineAtEndOfFileNoNewLine.java @@ -0,0 +1,13 @@ +/* +com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck +lineSeparator = LF +fileExtensions = (default)"" + +*/ +// violation 6 lines above 'File does not end with a newline.' + +package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilenonewline; + +public interface InputNewlineAtEndOfFileNoNewLine +{ +} \ No newline at end of file diff --git a/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/OutputNewlineAtEndOfFileNoNewLine.java b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/OutputNewlineAtEndOfFileNoNewLine.java new file mode 100644 index 0000000..b384b3a --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/newlineatendoffile/newlineatendoffilenonewline/OutputNewlineAtEndOfFileNoNewLine.java @@ -0,0 +1,12 @@ +/* +com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck +lineSeparator = LF +fileExtensions = (default)"" + +*/ + +package org.checkstyle.autofix.recipe.newlineatendoffile.newlineatendoffilenonewline; + +public interface OutputNewlineAtEndOfFileNoNewLine +{ +}