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, ExecutionContext> 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+