diff --git a/README.md b/README.md index 6501edf..07e621f 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ _No checks analyzed yet_ ### Headers -| Status | Check | Recipe | Coverage Notes | -|--------|---------------------------------------------------------------------------------|------------------|----------------| -| 🟢 | [`Header`](https://checkstyle.sourceforge.io/checks/header/header.html#Header ) | `TBD` | | +| Status | Check | Recipe | Coverage Notes | +|--------|---------------------------------------------------------------------------------|------------------|----------------------------| +| 🟡 | [`Header`](https://checkstyle.sourceforge.io/checks/header/header.html#Header ) | `TBD` | only java files are fixed. | diff --git a/config/import-control-test.xml b/config/import-control-test.xml index 5a21cb9..587b42a 100644 --- a/config/import-control-test.xml +++ b/config/import-control-test.xml @@ -12,4 +12,5 @@ + diff --git a/config/import-control.xml b/config/import-control.xml index 483abd1..adf5ddb 100644 --- a/config/import-control.xml +++ b/config/import-control.xml @@ -11,4 +11,5 @@ + diff --git a/src/main/java/org/checkstyle/autofix/parser/CheckstyleViolation.java b/src/main/java/org/checkstyle/autofix/parser/CheckstyleViolation.java index 2b3fc7f..ac27b5f 100644 --- a/src/main/java/org/checkstyle/autofix/parser/CheckstyleViolation.java +++ b/src/main/java/org/checkstyle/autofix/parser/CheckstyleViolation.java @@ -41,11 +41,11 @@ public CheckstyleViolation(Integer line, Integer column, this.fileName = fileName; } - public int getLine() { + public Integer getLine() { return line; } - public int getColumn() { + public Integer getColumn() { return column; } diff --git a/src/main/java/org/checkstyle/autofix/recipe/Header.java b/src/main/java/org/checkstyle/autofix/recipe/Header.java new file mode 100644 index 0000000..725e5ad --- /dev/null +++ b/src/main/java/org/checkstyle/autofix/recipe/Header.java @@ -0,0 +1,198 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// 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.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +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.tree.J; +import org.openrewrite.java.tree.JavaSourceFile; +import org.openrewrite.java.tree.Space; + +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import com.puppycrawl.tools.checkstyle.api.Configuration; + +public class Header extends Recipe { + private static final String HEADER_PROPERTY = "header"; + private static final String HEADER_FILE_PROPERTY = "headerFile"; + private static final String IGNORE_LINES_PROPERTY = "ignoreLines"; + private static final String CHARSET_PROPERTY = "charset"; + + private final List violations; + private final Configuration config; + private final Charset charset; + + public Header(List violations, Configuration config, Charset charset) { + this.violations = violations; + this.config = config; + this.charset = charset; + } + + @Override + public String getDisplayName() { + return "Header recipe"; + } + + @Override + public String getDescription() { + return "Adds headers to Java source files when missing."; + } + + @Override + public TreeVisitor getVisitor() { + final String licenseHeader = extractLicenseHeader(config, charset); + final List ignoreLines = extractIgnoreLines(config); + return new HeaderVisitor(violations, licenseHeader, ignoreLines); + } + + private static String extractLicenseHeader(Configuration config, Charset charset) { + final String header; + try { + if (hasProperty(config, HEADER_PROPERTY)) { + header = config.getProperty(HEADER_PROPERTY); + } + else { + final Charset charsetToUse; + if (hasProperty(config, CHARSET_PROPERTY)) { + charsetToUse = Charset.forName(config.getProperty(CHARSET_PROPERTY)); + } + else { + charsetToUse = charset; + } + final String headerFilePath = config.getProperty(HEADER_FILE_PROPERTY); + header = Files.readString(Path.of(headerFilePath), charsetToUse); + } + } + catch (CheckstyleException | IOException exception) { + throw new IllegalArgumentException("Failed to extract header from config", exception); + } + return header; + } + + private static List extractIgnoreLines(Configuration config) { + final List ignoreLinesList; + try { + if (!hasProperty(config, IGNORE_LINES_PROPERTY)) { + ignoreLinesList = new ArrayList<>(); + } + else { + final String ignoreLines = config.getProperty(IGNORE_LINES_PROPERTY); + ignoreLinesList = Arrays.stream(ignoreLines.split(",")) + .map(String::trim) + .map(Integer::parseInt) + .collect(Collectors.toList()); + } + } + catch (CheckstyleException exception) { + throw new IllegalArgumentException( + "Failed to extract ignore lines from config", exception); + } + return ignoreLinesList; + } + + private static boolean hasProperty(Configuration config, String propertyName) { + return Arrays.asList(config.getPropertyNames()).contains(propertyName); + } + + private static class HeaderVisitor extends JavaIsoVisitor { + private final List violations; + private final String licenseHeader; + private final List ignoreLines; + + HeaderVisitor(List violations, String licenseHeader, + List ignoreLines) { + this.violations = violations; + this.licenseHeader = licenseHeader; + this.ignoreLines = ignoreLines; + } + + @Override + public J visit(Tree tree, ExecutionContext ctx) { + J result = super.visit(tree, ctx); + + if (tree instanceof JavaSourceFile) { + JavaSourceFile sourceFile = (JavaSourceFile) tree; + final Path filePath = sourceFile.getSourcePath().toAbsolutePath(); + + if (hasViolation(filePath)) { + final String currentHeader = extractCurrentHeader(sourceFile); + final String fixedHeader = fixHeaderLines(licenseHeader, + currentHeader, ignoreLines); + + sourceFile = sourceFile.withPrefix( + Space.format(fixedHeader + System.lineSeparator())); + } + result = super.visit(sourceFile, ctx); + } + return result; + } + + private String extractCurrentHeader(JavaSourceFile sourceFile) { + return sourceFile.getComments().stream() + .map(comment -> comment.printComment(getCursor())) + .collect(Collectors.joining(System.lineSeparator())); + } + + private static String fixHeaderLines(String licenseHeader, + String currentHeader, List ignoreLines) { + final List currentLines = Arrays + .stream(currentHeader.split(System.lineSeparator())) + .collect(Collectors.toList()); + final List licenseLines = Arrays.stream(licenseHeader.split( + System.lineSeparator(), -1)).toList(); + + final Set ignoredLineNumbers = new HashSet<>(ignoreLines); + + for (int lineNumber = 1; lineNumber <= licenseLines.size(); lineNumber++) { + final String expectedLine = licenseLines.get(lineNumber - 1); + + if (lineNumber <= currentLines.size()) { + if (!ignoredLineNumbers.contains(lineNumber) + && !expectedLine.equals(currentLines.get(lineNumber - 1))) { + currentLines.set(lineNumber - 1, expectedLine); + } + } + else { + currentLines.add(expectedLine); + } + } + + return String.join(System.lineSeparator(), currentLines); + } + + private boolean hasViolation(Path filePath) { + return violations.removeIf(violation -> { + return filePath.equals(Path.of(violation.getFileName()).toAbsolutePath()); + }); + } + } +} diff --git a/src/main/java/org/checkstyle/autofix/recipe/UpperEll.java b/src/main/java/org/checkstyle/autofix/recipe/UpperEll.java index a46863c..c766fa5 100644 --- a/src/main/java/org/checkstyle/autofix/recipe/UpperEll.java +++ b/src/main/java/org/checkstyle/autofix/recipe/UpperEll.java @@ -70,7 +70,7 @@ private final class UpperEllVisitor extends JavaIsoVisitor { @Override public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) { - this.sourcePath = cu.getSourcePath(); + this.sourcePath = cu.getSourcePath().toAbsolutePath(); return super.visitCompilationUnit(cu, ctx); } @@ -97,9 +97,10 @@ private boolean isAtViolationLocation(J.Literal literal) { final int column = computeColumnPosition(cursor, literal, getCursor()); return violations.stream().anyMatch(violation -> { + final Path absolutePath = Path.of(violation.getFileName()).toAbsolutePath(); return violation.getLine() == line && violation.getColumn() == column - && Path.of(violation.getFileName()).equals(sourcePath); + && absolutePath.equals(sourcePath); }); } diff --git a/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java b/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java index 27bc248..5d0da12 100644 --- a/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java +++ b/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java @@ -21,13 +21,18 @@ import static org.openrewrite.java.Assertions.java; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Arrays; import org.checkstyle.autofix.InputClassRenamer; import org.openrewrite.Recipe; import org.openrewrite.test.RewriteTest; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import com.puppycrawl.tools.checkstyle.api.Configuration; + public abstract class AbstractRecipeTest implements RewriteTest { private static final String BASE_TEST_RESOURCES_PATH = "src/test/resources/org" @@ -37,9 +42,10 @@ private Recipe createPreprocessingRecipe() { return new InputClassRenamer(); } - protected abstract Recipe getRecipe(); + protected abstract Recipe getRecipe() throws CheckstyleException; - protected void testRecipe(String recipePath, String testCaseName) throws IOException { + protected void testRecipe(String recipePath, String testCaseName) throws IOException, + CheckstyleException { final String testCaseDir = testCaseName.toLowerCase(); final String inputFileName = "Input" + testCaseName + ".java"; final String outputFileName = "Output" + testCaseName + ".java"; @@ -60,4 +66,34 @@ protected void testRecipe(String recipePath, String testCaseName) throws IOExcep ); }); } + + protected Configuration extractCheckConfiguration(Configuration config, String checkName) { + + return Arrays.stream(config.getChildren()) + .filter(child -> checkName.equals(child.getName())) + .findFirst() + .orElseThrow(() -> { + return new IllegalArgumentException(checkName + "configuration not " + + "found"); + }); + } + + protected Charset getCharset(Configuration config) { + try { + final String charsetName; + + if (Arrays.asList(config.getPropertyNames()).contains("charset")) { + charsetName = config.getProperty("charset"); + } + else { + charsetName = Charset.defaultCharset().name(); + } + + return Charset.forName(charsetName); + } + catch (CheckstyleException exception) { + throw new IllegalArgumentException("Failed to extract charset from config.", exception); + } + } + } diff --git a/src/test/java/org/checkstyle/autofix/recipe/HeaderTest.java b/src/test/java/org/checkstyle/autofix/recipe/HeaderTest.java new file mode 100644 index 0000000..6ea5d90 --- /dev/null +++ b/src/test/java/org/checkstyle/autofix/recipe/HeaderTest.java @@ -0,0 +1,71 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// 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.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Properties; + +import org.checkstyle.autofix.parser.CheckstyleReportParser; +import org.checkstyle.autofix.parser.CheckstyleViolation; +import org.junit.jupiter.api.Test; +import org.openrewrite.Recipe; + +import com.puppycrawl.tools.checkstyle.ConfigurationLoader; +import com.puppycrawl.tools.checkstyle.PropertiesExpander; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import com.puppycrawl.tools.checkstyle.api.Configuration; + +public class HeaderTest extends AbstractRecipeTest { + + @Override + protected Recipe getRecipe() throws CheckstyleException { + final String reportPath = "src/test/resources/org/checkstyle/autofix/recipe/header" + + "/report.xml"; + + final String configPath = "src/test/resources/org/checkstyle/autofix/recipe/header" + + "/config.xml"; + + final Configuration config = ConfigurationLoader.loadConfiguration( + configPath, new PropertiesExpander(new Properties()) + ); + + final List violations = + CheckstyleReportParser.parse(Path.of(reportPath)); + + return new Header(violations, + extractCheckConfiguration(config, "Header"), getCharset(config)); + } + + @Test + void headerTest() throws IOException, CheckstyleException { + testRecipe("header", "HeaderBlankLines"); + } + + @Test + void headerCommentTest() throws IOException, CheckstyleException { + testRecipe("header", "HeaderComments"); + } + + @Test + void headerIncorrect() throws IOException, CheckstyleException { + testRecipe("header", "HeaderIncorrect"); + } + +} diff --git a/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java b/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java index ab6e228..0225e12 100644 --- a/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java +++ b/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java @@ -26,6 +26,8 @@ import org.junit.jupiter.api.Test; import org.openrewrite.Recipe; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; + public class UpperEllTest extends AbstractRecipeTest { @Override @@ -39,17 +41,17 @@ protected Recipe getRecipe() { } @Test - void hexOctalLiteralTest() throws IOException { + void hexOctalLiteralTest() throws IOException, CheckstyleException { testRecipe("upperell", "HexOctalLiteral"); } @Test - void complexLongLiterals() throws IOException { + void complexLongLiterals() throws IOException, CheckstyleException { testRecipe("upperell", "ComplexLongLiterals"); } @Test - void stringAndCommentTest() throws IOException { + void stringAndCommentTest() throws IOException, CheckstyleException { testRecipe("upperell", "StringAndComments"); } } diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/config.xml b/src/test/resources/org/checkstyle/autofix/recipe/header/config.xml new file mode 100644 index 0000000..eb94d5d --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/config.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/header.txt b/src/test/resources/org/checkstyle/autofix/recipe/header/header.txt new file mode 100644 index 0000000..2880bb8 --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/header.txt @@ -0,0 +1,6 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// Unit test for checkstyle-openrewrite-recipes. +// Dated: XX.XX.XX +// Copyright (C) 2025 Authors. Licensed under Apache 2.0. +// This file is part of the Checkstyle OpenRewrite test suite. +/////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/headerblanklines/InputHeaderBlankLines.java b/src/test/resources/org/checkstyle/autofix/recipe/header/headerblanklines/InputHeaderBlankLines.java new file mode 100644 index 0000000..c095e1d --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/headerblanklines/InputHeaderBlankLines.java @@ -0,0 +1,4 @@ +package org.checkstyle.autofix.recipe.header.headerblanklines; + +public class InputHeaderBlankLines { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/headerblanklines/OutputHeaderBlankLines.java b/src/test/resources/org/checkstyle/autofix/recipe/header/headerblanklines/OutputHeaderBlankLines.java new file mode 100644 index 0000000..4ebba0c --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/headerblanklines/OutputHeaderBlankLines.java @@ -0,0 +1,11 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// Unit test for checkstyle-openrewrite-recipes. +// Dated: XX.XX.XX +// Copyright (C) 2025 Authors. Licensed under Apache 2.0. +// This file is part of the Checkstyle OpenRewrite test suite. +/////////////////////////////////////////////////////////////////////////////////////////////// + +package org.checkstyle.autofix.recipe.header.headerblanklines; + +public class OutputHeaderBlankLines { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/headercomments/InputHeaderComments.java b/src/test/resources/org/checkstyle/autofix/recipe/header/headercomments/InputHeaderComments.java new file mode 100644 index 0000000..fa0cfcb --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/headercomments/InputHeaderComments.java @@ -0,0 +1,10 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// Unit test for checkstyle-openrewrite-recipes. +// Dated: 11.07.25 +// Copyright (C) 2025 Authors. Licensed under Apache. +/////////////////////////////////////////////////////////////////////////////////////////////// + +package org.checkstyle.autofix.recipe.header.headercomments; + +public class InputHeaderComments { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/headercomments/OutputHeaderComments.java b/src/test/resources/org/checkstyle/autofix/recipe/header/headercomments/OutputHeaderComments.java new file mode 100644 index 0000000..539202a --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/headercomments/OutputHeaderComments.java @@ -0,0 +1,11 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// Unit test for checkstyle-openrewrite-recipes. +// Dated: 11.07.25 +// Copyright (C) 2025 Authors. Licensed under Apache 2.0. +// This file is part of the Checkstyle OpenRewrite test suite. +/////////////////////////////////////////////////////////////////////////////////////////////// + +package org.checkstyle.autofix.recipe.header.headercomments; + +public class OutputHeaderComments { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/headerincorrect/InputHeaderIncorrect.java b/src/test/resources/org/checkstyle/autofix/recipe/header/headerincorrect/InputHeaderIncorrect.java new file mode 100644 index 0000000..d3e9e8c --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/headerincorrect/InputHeaderIncorrect.java @@ -0,0 +1,8 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// This is a test class +/////////////////////////////////////////////////////////////////////////////////////////////// + +package org.checkstyle.autofix.recipe.header.headerincorrect; + +public class InputHeaderIncorrect { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/headerincorrect/OutputHeaderIncorrect.java b/src/test/resources/org/checkstyle/autofix/recipe/header/headerincorrect/OutputHeaderIncorrect.java new file mode 100644 index 0000000..b36e6e9 --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/headerincorrect/OutputHeaderIncorrect.java @@ -0,0 +1,11 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// Unit test for checkstyle-openrewrite-recipes. +/////////////////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) 2025 Authors. Licensed under Apache 2.0. +// This file is part of the Checkstyle OpenRewrite test suite. +/////////////////////////////////////////////////////////////////////////////////////////////// + +package org.checkstyle.autofix.recipe.header.headerincorrect; + +public class OutputHeaderIncorrect { +} diff --git a/src/test/resources/org/checkstyle/autofix/recipe/header/report.xml b/src/test/resources/org/checkstyle/autofix/recipe/header/report.xml new file mode 100644 index 0000000..d33ca9c --- /dev/null +++ b/src/test/resources/org/checkstyle/autofix/recipe/header/report.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + +