Skip to content

Commit e090327

Browse files
committed
Issue #25: Added Header Recipe
1 parent cb642ab commit e090327

File tree

16 files changed

+329
-12
lines changed

16 files changed

+329
-12
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@ _No checks analyzed yet_
4343

4444
### Headers
4545

46-
| Status | Check | Recipe | Coverage Notes |
47-
|--------|---------------------------------------------------------------------------------|------------------|----------------|
48-
| 🟢 | [`Header`](https://checkstyle.sourceforge.io/checks/header/header.html#Header ) | `TBD` | |
46+
| Status | Check | Recipe | Coverage Notes |
47+
|--------|---------------------------------------------------------------------------------|------------------|----------------------------|
48+
| 🟡 | [`Header`](https://checkstyle.sourceforge.io/checks/header/header.html#Header ) | `TBD` | only java files are fixed. |
4949

5050

5151

config/import-control-test.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@
1212
<allow pkg="java.nio"/>
1313
<allow pkg="java.lang"/>
1414
<allow pkg="javax.xml.stream"/>
15+
<allow pkg="com.puppycrawl.tools.checkstyle"/>
1516
</import-control>

config/import-control.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
<allow pkg="javax.xml.stream"/>
1212
<allow pkg="org.checkstyle"/>
1313
<allow pkg="java.util"/>
14+
<allow pkg="com.puppycrawl.tools.checkstyle"/>
1415
</import-control>

src/main/java/org/checkstyle/autofix/parser/CheckstyleViolation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ public CheckstyleViolation(Integer line, Integer column,
4141
this.fileName = fileName;
4242
}
4343

44-
public int getLine() {
44+
public Integer getLine() {
4545
return line;
4646
}
4747

48-
public int getColumn() {
48+
public Integer getColumn() {
4949
return column;
5050
}
5151

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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.io.IOException;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.Iterator;
26+
import java.util.List;
27+
28+
import org.checkstyle.autofix.parser.CheckstyleViolation;
29+
import org.openrewrite.ExecutionContext;
30+
import org.openrewrite.Recipe;
31+
import org.openrewrite.Tree;
32+
import org.openrewrite.TreeVisitor;
33+
import org.openrewrite.java.JavaIsoVisitor;
34+
import org.openrewrite.java.tree.Comment;
35+
import org.openrewrite.java.tree.J;
36+
import org.openrewrite.java.tree.JavaSourceFile;
37+
import org.openrewrite.java.tree.Space;
38+
39+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
40+
import com.puppycrawl.tools.checkstyle.api.Configuration;
41+
42+
public class Header extends Recipe {
43+
44+
private static final String HEADER_LITERAL = "header";
45+
46+
private static int violationLine;
47+
48+
private List<CheckstyleViolation> violations;
49+
private Configuration headerConfig;
50+
51+
public Header() {
52+
this.violations = new ArrayList<>();
53+
this.headerConfig = null;
54+
}
55+
56+
public Header(List<CheckstyleViolation> violations, Configuration headerConfig) {
57+
this.violations = violations;
58+
this.headerConfig = headerConfig;
59+
}
60+
61+
@Override
62+
public String getDisplayName() {
63+
return "Header recipe";
64+
}
65+
66+
@Override
67+
public String getDescription() {
68+
return "Adds headers to Java source files when missing.";
69+
}
70+
71+
@Override
72+
public TreeVisitor<?, ExecutionContext> getVisitor() {
73+
return new HeaderVisitor(violations, headerConfig);
74+
}
75+
76+
private static String getLicenseHeader(Configuration child) {
77+
final String result;
78+
final String[] propertyNames = child.getPropertyNames();
79+
final boolean hasHeaderProperty = Arrays.asList(propertyNames).contains(HEADER_LITERAL);
80+
try {
81+
if (hasHeaderProperty) {
82+
result = child.getProperty(HEADER_LITERAL);
83+
}
84+
else {
85+
final String headerFile = child.getProperty("headerFile");
86+
result = readHeaderFileContent(headerFile);
87+
}
88+
}
89+
catch (CheckstyleException exception) {
90+
throw new IllegalArgumentException("Failed to extract header from config", exception);
91+
}
92+
return result;
93+
}
94+
95+
private static String readHeaderFileContent(String headerFilePath) {
96+
final String content;
97+
try {
98+
content = Files.readString(Path.of(headerFilePath));
99+
}
100+
catch (IOException exception) {
101+
throw new IllegalArgumentException("Failed to read: " + headerFilePath, exception);
102+
}
103+
return content;
104+
}
105+
106+
private static class HeaderVisitor extends JavaIsoVisitor<ExecutionContext> {
107+
private final List<CheckstyleViolation> violations;
108+
private final Configuration headerConfig;
109+
110+
HeaderVisitor(List<CheckstyleViolation> violations, Configuration headerConfig) {
111+
this.violations = violations;
112+
this.headerConfig = headerConfig;
113+
}
114+
115+
@Override
116+
public J visit(Tree tree, ExecutionContext ctx) {
117+
J result = super.visit(tree, ctx);
118+
119+
if (tree instanceof JavaSourceFile) {
120+
JavaSourceFile sourceFile = (JavaSourceFile) java.util.Objects.requireNonNull(tree);
121+
final Path absolutePath = sourceFile.getSourcePath().toAbsolutePath();
122+
123+
if (sourceFile.getComments().isEmpty() && isAtViolation(absolutePath)) {
124+
sourceFile = sourceFile.withPrefix(
125+
Space.format(getLicenseHeader(headerConfig)
126+
+ System.lineSeparator().repeat(2)));
127+
}
128+
else if (!sourceFile.getComments().isEmpty() && isAtViolation(absolutePath)) {
129+
final StringBuilder sourceHeaderBuilder = new StringBuilder();
130+
for (Comment comment : sourceFile.getComments()) {
131+
sourceHeaderBuilder.append(comment
132+
.printComment(getCursor())).append(System.lineSeparator());
133+
}
134+
final String sourceHeader = sourceHeaderBuilder.toString();
135+
final String actualHeader = getLicenseHeader(headerConfig);
136+
137+
final String[] sourceLines = sourceHeader.split(System.lineSeparator());
138+
final String[] actualLines = actualHeader.split(System.lineSeparator());
139+
140+
sourceLines[violationLine - 1] = actualLines[violationLine - 1];
141+
final String newHeader = String.join(System.lineSeparator(), sourceLines);
142+
143+
sourceFile = sourceFile.withComments(new ArrayList<>());
144+
sourceFile = sourceFile.withPrefix(
145+
Space.format(newHeader + System.lineSeparator().repeat(2)));
146+
}
147+
result = super.visit(sourceFile, ctx);
148+
}
149+
return result;
150+
}
151+
152+
private boolean isAtViolation(Path currentFileName) {
153+
boolean found = false;
154+
final Iterator<CheckstyleViolation> iterator = violations.iterator();
155+
156+
while (iterator.hasNext()) {
157+
final CheckstyleViolation violation = iterator.next();
158+
final Path absolutePath = Path.of(violation.getFileName()).toAbsolutePath();
159+
160+
if (violation.getColumn() == null && absolutePath.equals(currentFileName)) {
161+
violationLine = violation.getLine();
162+
iterator.remove();
163+
found = true;
164+
break;
165+
}
166+
}
167+
return found;
168+
}
169+
}
170+
}

src/main/java/org/checkstyle/autofix/recipe/UpperEll.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ private final class UpperEllVisitor extends JavaIsoVisitor<ExecutionContext> {
7070

7171
@Override
7272
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
73-
this.sourcePath = cu.getSourcePath();
73+
this.sourcePath = cu.getSourcePath().toAbsolutePath();
7474
return super.visitCompilationUnit(cu, ctx);
7575
}
7676

@@ -97,9 +97,10 @@ private boolean isAtViolationLocation(J.Literal literal) {
9797
final int column = computeColumnPosition(cursor, literal, getCursor());
9898

9999
return violations.stream().anyMatch(violation -> {
100+
final Path absolutePath = Path.of(violation.getFileName()).toAbsolutePath();
100101
return violation.getLine() == line
101102
&& violation.getColumn() == column
102-
&& Path.of(violation.getFileName()).equals(sourcePath);
103+
&& absolutePath.equals(sourcePath);
103104
});
104105
}
105106

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@
2323
import java.io.IOException;
2424
import java.nio.file.Files;
2525
import java.nio.file.Paths;
26+
import java.util.Arrays;
2627

2728
import org.checkstyle.autofix.InputClassRenamer;
2829
import org.openrewrite.Recipe;
2930
import org.openrewrite.test.RewriteTest;
3031

32+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
33+
import com.puppycrawl.tools.checkstyle.api.Configuration;
34+
3135
public abstract class AbstractRecipeTest implements RewriteTest {
3236

3337
private static final String BASE_TEST_RESOURCES_PATH = "src/test/resources/org"
@@ -37,9 +41,10 @@ private Recipe createPreprocessingRecipe() {
3741
return new InputClassRenamer();
3842
}
3943

40-
protected abstract Recipe getRecipe();
44+
protected abstract Recipe getRecipe() throws CheckstyleException;
4145

42-
protected void testRecipe(String recipePath, String testCaseName) throws IOException {
46+
protected void testRecipe(String recipePath, String testCaseName) throws IOException,
47+
CheckstyleException {
4348
final String testCaseDir = testCaseName.toLowerCase();
4449
final String inputFileName = "Input" + testCaseName + ".java";
4550
final String outputFileName = "Output" + testCaseName + ".java";
@@ -60,4 +65,16 @@ protected void testRecipe(String recipePath, String testCaseName) throws IOExcep
6065
);
6166
});
6267
}
68+
69+
protected Configuration extractCheckConfiguration(Configuration config, String checkName) {
70+
71+
return Arrays.stream(config.getChildren())
72+
.filter(child -> checkName.equals(child.getName()))
73+
.findFirst()
74+
.orElseThrow(() -> {
75+
return new IllegalArgumentException(checkName + "configuration not "
76+
+ "found");
77+
});
78+
}
79+
6380
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.io.IOException;
21+
import java.nio.file.Path;
22+
import java.util.List;
23+
import java.util.Properties;
24+
25+
import org.checkstyle.autofix.parser.CheckstyleReportParser;
26+
import org.checkstyle.autofix.parser.CheckstyleViolation;
27+
import org.junit.jupiter.api.Test;
28+
import org.openrewrite.Recipe;
29+
30+
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
31+
import com.puppycrawl.tools.checkstyle.PropertiesExpander;
32+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
33+
import com.puppycrawl.tools.checkstyle.api.Configuration;
34+
35+
public class HeaderTest extends AbstractRecipeTest {
36+
37+
@Override
38+
protected Recipe getRecipe() throws CheckstyleException {
39+
final String reportPath = "src/test/resources/org/checkstyle/autofix/recipe/header"
40+
+ "/report.xml";
41+
42+
final String configPath = "src/test/resources/org/checkstyle/autofix/recipe/header"
43+
+ "/config.xml";
44+
45+
final Configuration config = ConfigurationLoader.loadConfiguration(
46+
configPath, new PropertiesExpander(new Properties())
47+
);
48+
49+
final List<CheckstyleViolation> violations =
50+
CheckstyleReportParser.parse(Path.of(reportPath));
51+
52+
return new Header(violations, extractCheckConfiguration(config, "Header"));
53+
}
54+
55+
@Test
56+
void headerTest() throws IOException, CheckstyleException {
57+
testRecipe("header", "HeaderBlankLines");
58+
}
59+
60+
@Test
61+
void headerCommentTest() throws IOException, CheckstyleException {
62+
testRecipe("header", "HeaderComments");
63+
}
64+
65+
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import org.junit.jupiter.api.Test;
2727
import org.openrewrite.Recipe;
2828

29+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
30+
2931
public class UpperEllTest extends AbstractRecipeTest {
3032

3133
@Override
@@ -39,17 +41,17 @@ protected Recipe getRecipe() {
3941
}
4042

4143
@Test
42-
void hexOctalLiteralTest() throws IOException {
44+
void hexOctalLiteralTest() throws IOException, CheckstyleException {
4345
testRecipe("upperell", "HexOctalLiteral");
4446
}
4547

4648
@Test
47-
void complexLongLiterals() throws IOException {
49+
void complexLongLiterals() throws IOException, CheckstyleException {
4850
testRecipe("upperell", "ComplexLongLiterals");
4951
}
5052

5153
@Test
52-
void stringAndCommentTest() throws IOException {
54+
void stringAndCommentTest() throws IOException, CheckstyleException {
5355
testRecipe("upperell", "StringAndComments");
5456
}
5557
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0"?>
2+
<!DOCTYPE module PUBLIC
3+
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
4+
"https://checkstyle.org/dtds/configuration_1_3.dtd">
5+
<module name="Checker">
6+
7+
<module name="Header">
8+
<property name="headerFile" value="src/test/resources/org/checkstyle/autofix/recipe/header/header.txt"/>
9+
<property name="fileExtensions" value="java"/>
10+
</module>
11+
12+
</module>

0 commit comments

Comments
 (0)