Skip to content
Merged
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |



Expand Down
1 change: 1 addition & 0 deletions config/import-control-test.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@
<allow pkg="java.nio"/>
<allow pkg="java.lang"/>
<allow pkg="javax.xml.stream"/>
<allow pkg="com.puppycrawl.tools.checkstyle"/>
</import-control>
1 change: 1 addition & 0 deletions config/import-control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
<allow pkg="javax.xml.stream"/>
<allow pkg="org.checkstyle"/>
<allow pkg="java.util"/>
<allow pkg="com.puppycrawl.tools.checkstyle"/>
</import-control>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
198 changes: 198 additions & 0 deletions src/main/java/org/checkstyle/autofix/recipe/Header.java
Original file line number Diff line number Diff line change
@@ -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;
Comment on lines +41 to +42
Copy link
Member

Choose a reason for hiding this comment

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

I think we should replace these Checkstyle specific objects with our own like we did with CheckstyleViolation. This will help to avoid Checkstyle dependencies to be spread all around the recipes and have control over needed fields. To not overcomplicate this PR, I'm ok to fix it as part of the PR, which introduces configuration. @Anmol202005 please make a note in the issue to address this point

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done. updated issue: #36


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<CheckstyleViolation> violations;
private final Configuration config;
private final Charset charset;

public Header(List<CheckstyleViolation> 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<Integer> 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<Integer> extractIgnoreLines(Configuration config) {
final List<Integer> 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<ExecutionContext> {
private final List<CheckstyleViolation> violations;
private final String licenseHeader;
private final List<Integer> ignoreLines;

HeaderVisitor(List<CheckstyleViolation> violations, String licenseHeader,
List<Integer> 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<Integer> ignoreLines) {
final List<String> currentLines = Arrays
.stream(currentHeader.split(System.lineSeparator()))
.collect(Collectors.toList());
final List<String> licenseLines = Arrays.stream(licenseHeader.split(
System.lineSeparator(), -1)).toList();

final Set<Integer> 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());
});
}
}
}
5 changes: 3 additions & 2 deletions src/main/java/org/checkstyle/autofix/recipe/UpperEll.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private final class UpperEllVisitor extends JavaIsoVisitor<ExecutionContext> {

@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
this.sourcePath = cu.getSourcePath();
this.sourcePath = cu.getSourcePath().toAbsolutePath();
return super.visitCompilationUnit(cu, ctx);
}

Expand All @@ -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);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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";
Expand All @@ -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);
}
}

}
71 changes: 71 additions & 0 deletions src/test/java/org/checkstyle/autofix/recipe/HeaderTest.java
Original file line number Diff line number Diff line change
@@ -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<CheckstyleViolation> 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");
}

}
Loading
Loading