Skip to content

Commit 7d777e3

Browse files
committed
Issue #25: added header recipe
1 parent a3c2db6 commit 7d777e3

File tree

20 files changed

+430
-15
lines changed

20 files changed

+430
-15
lines changed

config/checkstyle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ checkstyle.regexp.header.file=config/regexp-header.txt
55
checkstyle.importcontrol.file=config/import-control.xml
66
checkstyle.importcontroltest.file=config/import-control-test.xml
77
checkstyle.java.version=11
8+
checkstyle.cache.file=.cache/checkstyle-cachefile

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/CheckstyleAutoFix.java

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@
1717

1818
package org.checkstyle.autofix;
1919

20+
import java.io.File;
21+
import java.io.FileInputStream;
22+
import java.io.FileNotFoundException;
23+
import java.io.IOException;
2024
import java.nio.file.Path;
2125
import java.util.List;
26+
import java.util.Properties;
2227

2328
import org.checkstyle.autofix.parser.CheckstyleReportParser;
2429
import org.checkstyle.autofix.parser.CheckstyleViolation;
2530
import org.openrewrite.Option;
2631
import org.openrewrite.Recipe;
2732

33+
import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
34+
import com.puppycrawl.tools.checkstyle.PropertiesExpander;
35+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
36+
import com.puppycrawl.tools.checkstyle.api.Configuration;
37+
2838
/**
2939
* Main recipe that automatically fixes all supported Checkstyle violations.
3040
*/
@@ -35,6 +45,22 @@ public class CheckstyleAutoFix extends Recipe {
3545
example = "target/checkstyle/checkstyle-report.xml")
3646
private String violationReportPath;
3747

48+
@Option(displayName = "Checkstyle config path",
49+
description = "Path to the file containing Checkstyle configuration.",
50+
example = "config/checkstyle.xml")
51+
private String checkstyleConfigurationPath;
52+
53+
@Option(displayName = "Checkstyle properties file path",
54+
description = "Path to the file containing the Checkstyle Properties.",
55+
example = "config/checkstyle.properties")
56+
private String propertiesPath;
57+
58+
private Configuration cachedConfig;
59+
60+
public CheckstyleAutoFix() {
61+
// Constructor is now clean
62+
}
63+
3864
@Override
3965
public String getDisplayName() {
4066
return "Checkstyle autoFix";
@@ -49,12 +75,69 @@ public String getViolationReportPath() {
4975
return violationReportPath;
5076
}
5177

78+
public String getCheckstyleConfigurationPath() {
79+
return checkstyleConfigurationPath;
80+
}
81+
82+
public String getPropertiesPath() {
83+
return propertiesPath;
84+
}
85+
5286
@Override
5387
public List<Recipe> getRecipeList() {
88+
try {
89+
final Configuration config = loadCheckstyleConfiguration();
90+
91+
final List<CheckstyleViolation> violations = CheckstyleReportParser
92+
.parse(Path.of(getViolationReportPath()));
5493

55-
final List<CheckstyleViolation> violations = CheckstyleReportParser
56-
.parse(Path.of(getViolationReportPath()));
94+
return CheckstyleRecipeRegistry.getRecipes(violations, config);
5795

58-
return CheckstyleRecipeRegistry.getRecipes(violations);
96+
}
97+
catch (CheckstyleException | IOException exception) {
98+
throw new IllegalArgumentException("Failed to load Checkstyle"
99+
+ " configuration or parse violations", exception);
100+
}
59101
}
102+
103+
private Configuration loadCheckstyleConfiguration() throws CheckstyleException, IOException {
104+
final Configuration config;
105+
106+
if (cachedConfig == null) {
107+
final Properties props = new Properties();
108+
final String propFile = getPropertiesPath();
109+
110+
try (FileInputStream input = new FileInputStream(propFile)) {
111+
props.load(input);
112+
}
113+
catch (FileNotFoundException exception) {
114+
throw new IllegalArgumentException("Failed to read: " + propFile, exception);
115+
}
116+
117+
final String configPath = getCheckstyleConfigurationPath();
118+
119+
if (isRemoteUrl(configPath)) {
120+
config = ConfigurationLoader.loadConfiguration(
121+
configPath,
122+
new PropertiesExpander(props)
123+
);
124+
}
125+
else {
126+
final File configFile = new File(configPath);
127+
config = ConfigurationLoader.loadConfiguration(
128+
configFile.toURI().toString(),
129+
new PropertiesExpander(props)
130+
);
131+
}
132+
133+
cachedConfig = config;
134+
}
135+
136+
return cachedConfig;
137+
}
138+
139+
private boolean isRemoteUrl(String path) {
140+
return path != null && (path.startsWith("http://") || path.startsWith("https://"));
141+
}
142+
60143
}

src/main/java/org/checkstyle/autofix/CheckstyleRecipeRegistry.java

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,32 @@
1717

1818
package org.checkstyle.autofix;
1919

20+
import java.io.IOException;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
2023
import java.util.ArrayList;
24+
import java.util.Arrays;
2125
import java.util.HashMap;
2226
import java.util.List;
2327
import java.util.Map;
2428
import java.util.function.Function;
2529
import java.util.stream.Collectors;
2630

2731
import org.checkstyle.autofix.parser.CheckstyleViolation;
32+
import org.checkstyle.autofix.recipe.Header;
2833
import org.checkstyle.autofix.recipe.UpperEll;
2934
import org.openrewrite.Recipe;
3035

36+
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
37+
import com.puppycrawl.tools.checkstyle.api.Configuration;
38+
3139
public final class CheckstyleRecipeRegistry {
3240

3341
private static final Map<String, Function<List<CheckstyleViolation>, Recipe>> RECIPE_MAP =
3442
new HashMap<>();
3543

44+
private static final String HEADER_LITERAL = "header";
45+
3646
static {
3747
RECIPE_MAP.put("UpperEllCheck", UpperEll::new);
3848
}
@@ -47,9 +57,11 @@ private CheckstyleRecipeRegistry() {
4757
* using the simple name of the check, and applies the factory to generate Recipe instances.
4858
*
4959
* @param violations the list of Checkstyle violations
60+
* @param config the Checkstyle configuration
5061
* @return a list of generated Recipe objects
5162
*/
52-
public static List<Recipe> getRecipes(List<CheckstyleViolation> violations) {
63+
public static List<Recipe> getRecipes(List<CheckstyleViolation> violations,
64+
Configuration config) {
5365

5466
final Map<String, List<CheckstyleViolation>> violationsByCheck = violations.stream()
5567
.collect(Collectors.groupingBy(CheckstyleViolation::getSource));
@@ -67,8 +79,50 @@ public static List<Recipe> getRecipes(List<CheckstyleViolation> violations) {
6779
if (recipeFactory != null) {
6880
recipes.add(recipeFactory.apply(checkViolations));
6981
}
82+
83+
else if (HEADER_LITERAL.equals(simpleCheckName)) {
84+
final String headerContent = extractHeaderContent(config);
85+
recipes.add(new Header(checkViolations, headerContent));
86+
}
7087
}
7188

7289
return recipes;
7390
}
91+
92+
private static String readHeaderFileContent(String headerFilePath) {
93+
try {
94+
return Files.readString(Path.of(headerFilePath));
95+
}
96+
catch (IOException exception) {
97+
throw new IllegalArgumentException("Failed to read: " + headerFilePath, exception);
98+
}
99+
}
100+
101+
private static String extractHeaderContent(Configuration config) {
102+
return Arrays.stream(config.getChildren())
103+
.filter(child -> "Header".equals(child.getName()))
104+
.findFirst()
105+
.map(CheckstyleRecipeRegistry::getHeaderFromChild)
106+
.orElseThrow(() -> new IllegalArgumentException("Header configuration not found"));
107+
}
108+
109+
private static String getHeaderFromChild(Configuration child) {
110+
final String result;
111+
final String[] propertyNames = child.getPropertyNames();
112+
final boolean hasHeaderProperty = Arrays.asList(propertyNames).contains(HEADER_LITERAL);
113+
try {
114+
if (hasHeaderProperty) {
115+
result = child.getProperty(HEADER_LITERAL);
116+
}
117+
else {
118+
final String headerFile = child.getProperty("headerFile");
119+
result = readHeaderFileContent(headerFile);
120+
}
121+
}
122+
catch (CheckstyleException exception) {
123+
throw new IllegalArgumentException("Failed to extract header from config", exception);
124+
}
125+
return result;
126+
}
127+
74128
}

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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.nio.file.Path;
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Objects;
24+
25+
import org.checkstyle.autofix.parser.CheckstyleViolation;
26+
import org.openrewrite.ExecutionContext;
27+
import org.openrewrite.Recipe;
28+
import org.openrewrite.Tree;
29+
import org.openrewrite.TreeVisitor;
30+
import org.openrewrite.java.JavaIsoVisitor;
31+
import org.openrewrite.java.tree.J;
32+
import org.openrewrite.java.tree.JavaSourceFile;
33+
import org.openrewrite.java.tree.Space;
34+
35+
public class Header extends Recipe {
36+
37+
private List<CheckstyleViolation> violations;
38+
private String licenseText;
39+
40+
public Header() {
41+
this.violations = new ArrayList<>();
42+
this.licenseText = "";
43+
}
44+
45+
public Header(List<CheckstyleViolation> violations, String licenseText) {
46+
this.violations = violations;
47+
this.licenseText = licenseText;
48+
}
49+
50+
@Override
51+
public String getDisplayName() {
52+
return "Header recipe";
53+
}
54+
55+
@Override
56+
public String getDescription() {
57+
return "Adds headers to Java source files when missing."
58+
+ " Does not override existing license headers.";
59+
}
60+
61+
@Override
62+
public TreeVisitor<?, ExecutionContext> getVisitor() {
63+
return new JavaIsoVisitor<>() {
64+
@Override
65+
public J visit(Tree tree, ExecutionContext ctx) {
66+
final J result;
67+
if (tree instanceof JavaSourceFile) {
68+
JavaSourceFile sourceFile =
69+
(JavaSourceFile) java.util.Objects.requireNonNull(tree);
70+
final Path absolutePath = sourceFile.getSourcePath().toAbsolutePath();
71+
if (sourceFile.getComments().isEmpty() && isAtViolationLocation(absolutePath)) {
72+
sourceFile = sourceFile.withPrefix(Space.format(licenseText
73+
+ System.lineSeparator()));
74+
}
75+
result = super.visit(sourceFile, ctx);
76+
}
77+
else {
78+
result = super.visit(tree, ctx);
79+
}
80+
return result;
81+
}
82+
};
83+
}
84+
85+
private boolean isAtViolationLocation(Path currentFileName) {
86+
87+
return violations.stream().anyMatch(violation -> {
88+
89+
final Path absolutePath =
90+
Path.of(violation.getFileName()).toAbsolutePath();
91+
92+
return violation.getLine() == 1
93+
&& Objects.isNull(violation.getColumn())
94+
&& absolutePath.equals(currentFileName);
95+
});
96+
}
97+
}

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

Lines changed: 4 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,11 @@ 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 =
101+
Path.of(violation.getFileName()).toAbsolutePath();
100102
return violation.getLine() == line
101103
&& violation.getColumn() == column
102-
&& Path.of(violation.getFileName()).equals(sourcePath);
104+
&& absolutePath.equals(sourcePath);
103105
});
104106
}
105107

src/main/resources/META-INF/rewrite/recipes.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,5 @@ tags:
1515
recipeList:
1616
- org.checkstyle.autofix.CheckstyleAutoFix:
1717
violationReportPath: "target/checkstyle/checkstyle-report.xml"
18+
checkstyleConfigurationPath: "https://raw.githubusercontent.com/checkstyle/checkstyle/checkstyle-10.25.0/config/checkstyle-checks.xml"
19+
propertiesPath: "config/checkstyle.properties"

0 commit comments

Comments
 (0)