Skip to content

Commit 63e9a71

Browse files
committed
Issue #25: Added Header Recipe
1 parent 4bf149c commit 63e9a71

File tree

18 files changed

+417
-12
lines changed

18 files changed

+417
-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: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
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.charset.Charset;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.util.ArrayList;
25+
import java.util.Arrays;
26+
import java.util.HashSet;
27+
import java.util.List;
28+
import java.util.Set;
29+
import java.util.stream.Collectors;
30+
31+
import org.checkstyle.autofix.parser.CheckstyleViolation;
32+
import org.openrewrite.ExecutionContext;
33+
import org.openrewrite.Recipe;
34+
import org.openrewrite.Tree;
35+
import org.openrewrite.TreeVisitor;
36+
import org.openrewrite.java.JavaIsoVisitor;
37+
import org.openrewrite.java.tree.J;
38+
import org.openrewrite.java.tree.JavaSourceFile;
39+
import org.openrewrite.java.tree.Space;
40+
41+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
42+
import com.puppycrawl.tools.checkstyle.api.Configuration;
43+
44+
public class Header extends Recipe {
45+
private static final String HEADER_PROPERTY = "header";
46+
private static final String HEADER_FILE_PROPERTY = "headerFile";
47+
private static final String IGNORE_LINES_PROPERTY = "ignoreLines";
48+
private static final String CHARSET_PROPERTY = "charset";
49+
50+
private final List<CheckstyleViolation> violations;
51+
private final Configuration config;
52+
private final Charset charset;
53+
54+
public Header(List<CheckstyleViolation> violations, Configuration config, Charset charset) {
55+
this.violations = violations;
56+
this.config = config;
57+
this.charset = charset;
58+
}
59+
60+
@Override
61+
public String getDisplayName() {
62+
return "Header recipe";
63+
}
64+
65+
@Override
66+
public String getDescription() {
67+
return "Adds headers to Java source files when missing.";
68+
}
69+
70+
@Override
71+
public TreeVisitor<?, ExecutionContext> getVisitor() {
72+
final String licenseHeader = extractLicenseHeader(config, charset);
73+
final List<Integer> ignoreLines = extractIgnoreLines(config);
74+
return new HeaderVisitor(violations, licenseHeader, ignoreLines);
75+
}
76+
77+
private static String extractLicenseHeader(Configuration config, Charset charset) {
78+
final String header;
79+
try {
80+
if (hasProperty(config, HEADER_PROPERTY)) {
81+
header = config.getProperty(HEADER_PROPERTY);
82+
}
83+
else {
84+
final Charset charsetToUse;
85+
if (hasProperty(config, CHARSET_PROPERTY)) {
86+
charsetToUse = Charset.forName(config.getProperty(CHARSET_PROPERTY));
87+
}
88+
else {
89+
charsetToUse = charset;
90+
}
91+
final String headerFilePath = config.getProperty(HEADER_FILE_PROPERTY);
92+
header = Files.readString(Path.of(headerFilePath), charsetToUse);
93+
}
94+
}
95+
catch (CheckstyleException | IOException exception) {
96+
throw new IllegalArgumentException("Failed to extract header from config", exception);
97+
}
98+
return header;
99+
}
100+
101+
private static List<Integer> extractIgnoreLines(Configuration config) {
102+
final List<Integer> ignoreLinesList;
103+
try {
104+
if (!hasProperty(config, IGNORE_LINES_PROPERTY)) {
105+
ignoreLinesList = new ArrayList<>();
106+
}
107+
else {
108+
final String ignoreLines = config.getProperty(IGNORE_LINES_PROPERTY);
109+
ignoreLinesList = Arrays.stream(ignoreLines.split(","))
110+
.map(String::trim)
111+
.map(Integer::parseInt)
112+
.collect(Collectors.toList());
113+
}
114+
}
115+
catch (CheckstyleException exception) {
116+
throw new IllegalArgumentException(
117+
"Failed to extract ignore lines from config", exception);
118+
}
119+
return ignoreLinesList;
120+
}
121+
122+
private static boolean hasProperty(Configuration config, String propertyName) {
123+
return Arrays.asList(config.getPropertyNames()).contains(propertyName);
124+
}
125+
126+
private static class HeaderVisitor extends JavaIsoVisitor<ExecutionContext> {
127+
private final List<CheckstyleViolation> violations;
128+
private final String licenseHeader;
129+
private final List<Integer> ignoreLines;
130+
131+
HeaderVisitor(List<CheckstyleViolation> violations, String licenseHeader,
132+
List<Integer> ignoreLines) {
133+
this.violations = violations;
134+
this.licenseHeader = licenseHeader;
135+
this.ignoreLines = ignoreLines;
136+
}
137+
138+
@Override
139+
public J visit(Tree tree, ExecutionContext ctx) {
140+
J result = super.visit(tree, ctx);
141+
142+
if (tree instanceof JavaSourceFile) {
143+
JavaSourceFile sourceFile = (JavaSourceFile) tree;
144+
final Path filePath = sourceFile.getSourcePath().toAbsolutePath();
145+
146+
if (hasViolation(filePath)) {
147+
final String currentHeader = extractCurrentHeader(sourceFile);
148+
final String fixedHeader = fixHeaderLines(licenseHeader,
149+
currentHeader, ignoreLines);
150+
151+
sourceFile = sourceFile.withPrefix(
152+
Space.format(fixedHeader + System.lineSeparator()));
153+
}
154+
result = super.visit(sourceFile, ctx);
155+
}
156+
return result;
157+
}
158+
159+
private String extractCurrentHeader(JavaSourceFile sourceFile) {
160+
return sourceFile.getComments().stream()
161+
.map(comment -> comment.printComment(getCursor()))
162+
.collect(Collectors.joining(System.lineSeparator()));
163+
}
164+
165+
private static String fixHeaderLines(String licenseHeader,
166+
String currentHeader, List<Integer> ignoreLines) {
167+
final List<String> currentLines = Arrays
168+
.stream(currentHeader.split(System.lineSeparator()))
169+
.collect(Collectors.toList());
170+
final List<String> licenseLines = Arrays.stream(licenseHeader.split(
171+
System.lineSeparator(), -1)).toList();
172+
173+
final Set<Integer> ignoredLineNumbers = new HashSet<>(ignoreLines);
174+
175+
for (int lineNumber = 1; lineNumber <= licenseLines.size(); lineNumber++) {
176+
final String expectedLine = licenseLines.get(lineNumber - 1);
177+
178+
if (lineNumber <= currentLines.size()) {
179+
if (!ignoredLineNumbers.contains(lineNumber)
180+
&& !expectedLine.equals(currentLines.get(lineNumber - 1))) {
181+
currentLines.set(lineNumber - 1, expectedLine);
182+
}
183+
}
184+
else {
185+
currentLines.add(expectedLine);
186+
}
187+
}
188+
189+
return String.join(System.lineSeparator(), currentLines);
190+
}
191+
192+
private boolean hasViolation(Path filePath) {
193+
return violations.removeIf(violation -> {
194+
return filePath.equals(Path.of(violation.getFileName()).toAbsolutePath());
195+
});
196+
}
197+
}
198+
}

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: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
import static org.openrewrite.java.Assertions.java;
2222

2323
import java.io.IOException;
24+
import java.nio.charset.Charset;
2425
import java.nio.file.Files;
2526
import java.nio.file.Paths;
27+
import java.util.Arrays;
2628

2729
import org.checkstyle.autofix.InputClassRenamer;
2830
import org.openrewrite.Recipe;
2931
import org.openrewrite.test.RewriteTest;
3032

33+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
34+
import com.puppycrawl.tools.checkstyle.api.Configuration;
35+
3136
public abstract class AbstractRecipeTest implements RewriteTest {
3237

3338
private static final String BASE_TEST_RESOURCES_PATH = "src/test/resources/org"
@@ -37,9 +42,10 @@ private Recipe createPreprocessingRecipe() {
3742
return new InputClassRenamer();
3843
}
3944

40-
protected abstract Recipe getRecipe();
45+
protected abstract Recipe getRecipe() throws CheckstyleException;
4146

42-
protected void testRecipe(String recipePath, String testCaseName) throws IOException {
47+
protected void testRecipe(String recipePath, String testCaseName) throws IOException,
48+
CheckstyleException {
4349
final String testCaseDir = testCaseName.toLowerCase();
4450
final String inputFileName = "Input" + testCaseName + ".java";
4551
final String outputFileName = "Output" + testCaseName + ".java";
@@ -60,4 +66,34 @@ protected void testRecipe(String recipePath, String testCaseName) throws IOExcep
6066
);
6167
});
6268
}
69+
70+
protected Configuration extractCheckConfiguration(Configuration config, String checkName) {
71+
72+
return Arrays.stream(config.getChildren())
73+
.filter(child -> checkName.equals(child.getName()))
74+
.findFirst()
75+
.orElseThrow(() -> {
76+
return new IllegalArgumentException(checkName + "configuration not "
77+
+ "found");
78+
});
79+
}
80+
81+
protected Charset getCharset(Configuration config) {
82+
try {
83+
final String charsetName;
84+
85+
if (Arrays.asList(config.getPropertyNames()).contains("charset")) {
86+
charsetName = config.getProperty("charset");
87+
}
88+
else {
89+
charsetName = Charset.defaultCharset().name();
90+
}
91+
92+
return Charset.forName(charsetName);
93+
}
94+
catch (CheckstyleException exception) {
95+
throw new IllegalArgumentException("Failed to extract charset from config.", exception);
96+
}
97+
}
98+
6399
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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,
53+
extractCheckConfiguration(config, "Header"), getCharset(config));
54+
}
55+
56+
@Test
57+
void headerTest() throws IOException, CheckstyleException {
58+
testRecipe("header", "HeaderBlankLines");
59+
}
60+
61+
@Test
62+
void headerCommentTest() throws IOException, CheckstyleException {
63+
testRecipe("header", "HeaderComments");
64+
}
65+
66+
@Test
67+
void headerIncorrect() throws IOException, CheckstyleException {
68+
testRecipe("header", "HeaderIncorrect");
69+
}
70+
71+
}

0 commit comments

Comments
 (0)