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/pom.xml b/pom.xml index be97b7a..94d7ab6 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,13 @@ + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + tests + test + org.openrewrite rewrite-test diff --git a/src/main/java/org/checkstyle/autofix/CheckstyleAutoFix.java b/src/main/java/org/checkstyle/autofix/CheckstyleAutoFix.java index 733bbbe..a4fd3a7 100644 --- a/src/main/java/org/checkstyle/autofix/CheckstyleAutoFix.java +++ b/src/main/java/org/checkstyle/autofix/CheckstyleAutoFix.java @@ -20,8 +20,10 @@ import java.nio.file.Path; import java.util.List; +import org.checkstyle.autofix.parser.CheckConfiguration; import org.checkstyle.autofix.parser.CheckstyleReportParser; import org.checkstyle.autofix.parser.CheckstyleViolation; +import org.checkstyle.autofix.parser.ConfigurationLoader; import org.openrewrite.Option; import org.openrewrite.Recipe; @@ -35,6 +37,17 @@ public class CheckstyleAutoFix extends Recipe { example = "target/checkstyle/checkstyle-report.xml") private String violationReportPath; + @Option(displayName = "Checkstyle config path", + description = "Path to the file containing Checkstyle configuration.", + example = "config/checkstyle.xml") + private String configurationPath; + + @Option(displayName = "Checkstyle properties file path", + description = "Path to the file containing the Checkstyle Properties.", + example = "config/checkstyle.properties", + required = false) + private String propertiesPath; + @Override public String getDisplayName() { return "Checkstyle autoFix"; @@ -49,12 +62,23 @@ public String getViolationReportPath() { return violationReportPath; } + public String getConfigurationPath() { + return configurationPath; + } + + public String getPropertiesPath() { + return propertiesPath; + } + @Override public List getRecipeList() { - final List violations = CheckstyleReportParser .parse(Path.of(getViolationReportPath())); return CheckstyleRecipeRegistry.getRecipes(violations); } + + private CheckConfiguration loadCheckstyleConfiguration() { + return ConfigurationLoader.loadConfiguration(getConfigurationPath(), getPropertiesPath()); + } } diff --git a/src/main/java/org/checkstyle/autofix/parser/CheckConfiguration.java b/src/main/java/org/checkstyle/autofix/parser/CheckConfiguration.java new file mode 100644 index 0000000..5fefb27 --- /dev/null +++ b/src/main/java/org/checkstyle/autofix/parser/CheckConfiguration.java @@ -0,0 +1,108 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// 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.parser; + +import java.util.List; +import java.util.Map; + +public final class CheckConfiguration { + private final String name; + private final Map properties; + private final List children; + private CheckConfiguration parent; + + public CheckConfiguration(String name, + Map properties, List children) { + this.name = name; + this.properties = properties; + this.children = children; + + for (CheckConfiguration child : children) { + child.setParent(this); + } + } + + public String getName() { + return name; + } + + public Map getProperties() { + return properties; + } + + public List getChildren() { + return children; + } + + public String getProperty(String key) { + return properties.get(key); + } + + public int getInt(String propertyName) { + final String value = properties.get(propertyName); + final int result; + try { + result = Integer.parseInt(value); + } + catch (NumberFormatException exception) { + throw new IllegalArgumentException("Invalid integer value for property '" + + propertyName + "': " + value, exception); + } + return result; + } + + public boolean getBoolean(String propertyName) { + return Boolean.parseBoolean(properties.get(propertyName)); + } + + public int[] getIntArray(String propertyName) { + final String value = properties.get(propertyName); + final int[] result; + final String[] parts = value.split(","); + result = new int[parts.length]; + for (int index = 0; index < parts.length; index++) { + try { + result[index] = Integer.parseInt(parts[index].trim()); + } + catch (NumberFormatException exception) { + throw new IllegalArgumentException("Property '" + propertyName + + "' has an invalid integer value: " + parts[index].trim(), exception); + } + } + return result; + } + + public CheckConfiguration getChildByName(String childName) { + CheckConfiguration result = null; + for (CheckConfiguration child : children) { + if (childName.equals(child.getName())) { + result = child; + break; + } + } + return result; + } + + public CheckConfiguration getParent() { + return parent; + } + + private void setParent(CheckConfiguration parent) { + this.parent = parent; + } +} diff --git a/src/main/java/org/checkstyle/autofix/parser/ConfigurationLoader.java b/src/main/java/org/checkstyle/autofix/parser/ConfigurationLoader.java new file mode 100644 index 0000000..58745b4 --- /dev/null +++ b/src/main/java/org/checkstyle/autofix/parser/ConfigurationLoader.java @@ -0,0 +1,91 @@ +/////////////////////////////////////////////////////////////////////////////////////////////// +// 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.parser; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import com.puppycrawl.tools.checkstyle.PropertiesExpander; +import com.puppycrawl.tools.checkstyle.api.CheckstyleException; +import com.puppycrawl.tools.checkstyle.api.Configuration; + +public final class ConfigurationLoader { + + private ConfigurationLoader() { + // utility class + } + + public static CheckConfiguration mapConfiguration(Configuration config) { + final Map properties = new HashMap<>(); + final String[] propertyNames = config.getPropertyNames(); + for (String propertyName : propertyNames) { + try { + final String value = config.getProperty(propertyName); + properties.put(propertyName, value); + + } + catch (CheckstyleException exception) { + throw new IllegalStateException("Error getting property " + propertyName, + exception); + } + } + + final Configuration[] checkstyleChildren = config.getChildren(); + final CheckConfiguration[] simpleChildren = + new CheckConfiguration[checkstyleChildren.length]; + for (int index = 0; index < checkstyleChildren.length; index++) { + simpleChildren[index] = mapConfiguration(checkstyleChildren[index]); + } + + return new CheckConfiguration(config.getName(), properties, List.of(simpleChildren)); + } + + public static CheckConfiguration loadConfiguration(String checkstyleConfigurationPath, + String propFile) { + Properties props = new Properties(); + + if (propFile == null) { + props = System.getProperties(); + } + else { + try (FileInputStream input = new FileInputStream(propFile)) { + props.load(input); + } + catch (IOException exception) { + throw new IllegalStateException("Failed to read: " + propFile, exception); + } + } + + final Configuration checkstyleConfig; + try { + checkstyleConfig = com.puppycrawl.tools.checkstyle.ConfigurationLoader + .loadConfiguration(checkstyleConfigurationPath, new PropertiesExpander(props)); + } + catch (CheckstyleException exception) { + throw new IllegalStateException("Failed to load configuration:" + + checkstyleConfigurationPath, exception); + } + + return mapConfiguration(checkstyleConfig); + } + +} diff --git a/src/main/resources/META-INF/rewrite/recipes.yml b/src/main/resources/META-INF/rewrite/recipes.yml index 76b2c72..ae1b7de 100644 --- a/src/main/resources/META-INF/rewrite/recipes.yml +++ b/src/main/resources/META-INF/rewrite/recipes.yml @@ -15,3 +15,4 @@ tags: recipeList: - org.checkstyle.autofix.CheckstyleAutoFix: violationReportPath: "target/checkstyle/checkstyle-report.xml" + configurationPath: "https://raw.githubusercontent.com/checkstyle/checkstyle/checkstyle-${checkstyle.version}/config/checkstyle-checks.xml" diff --git a/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java b/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java index 27bc248..7976cce 100644 --- a/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java +++ b/src/test/java/org/checkstyle/autofix/recipe/AbstractRecipeTest.java @@ -1,63 +1,115 @@ -/////////////////////////////////////////////////////////////////////////////////////////////// -// 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 static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.openrewrite.java.Assertions.java; -import java.io.IOException; +import java.io.ByteArrayOutputStream; +import java.io.File; import java.nio.file.Files; -import java.nio.file.Paths; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; import org.checkstyle.autofix.InputClassRenamer; +import org.checkstyle.autofix.parser.CheckConfiguration; +import org.checkstyle.autofix.parser.CheckstyleReportParser; +import org.checkstyle.autofix.parser.CheckstyleViolation; +import org.checkstyle.autofix.parser.ConfigurationLoader; import org.openrewrite.Recipe; import org.openrewrite.test.RewriteTest; -public abstract class AbstractRecipeTest implements RewriteTest { +import com.puppycrawl.tools.checkstyle.AbstractXmlTestSupport; +import com.puppycrawl.tools.checkstyle.Checker; +import com.puppycrawl.tools.checkstyle.XMLLogger; +import com.puppycrawl.tools.checkstyle.api.Configuration; +import com.puppycrawl.tools.checkstyle.bdd.InlineConfigParser; +import com.puppycrawl.tools.checkstyle.bdd.TestInputConfiguration; - private static final String BASE_TEST_RESOURCES_PATH = "src/test/resources/org" - + "/checkstyle/autofix/recipe/"; +/** + * Simple base class for recipe tests that extends Checkstyle's AbstractXmlTestSupport. + */ +public abstract class AbstractRecipeTest extends AbstractXmlTestSupport implements RewriteTest { - private Recipe createPreprocessingRecipe() { - return new InputClassRenamer(); - } + /** + * Returns the subpackage name for test resources. + */ + protected abstract String getSubpackage(); - protected abstract Recipe getRecipe(); + /** + * Creates the recipe with violations and configs. + */ + protected abstract Recipe createRecipe(List violations, + CheckConfiguration checkConfigs); + + @Override + protected String getPackageLocation() { + return "org/checkstyle/autofix/recipe/" + getSubpackage(); + } - protected void testRecipe(String recipePath, String testCaseName) throws IOException { - final String testCaseDir = testCaseName.toLowerCase(); + /** + * Main verification method. + */ + protected void verify(String testCaseName) throws Exception { final String inputFileName = "Input" + testCaseName + ".java"; final String outputFileName = "Output" + testCaseName + ".java"; + final String inputPath = testCaseName.toLowerCase() + "/" + inputFileName; + final String outputPath = testCaseName.toLowerCase() + "/" + outputFileName; - final String beforeCode = Files.readString(Paths.get(BASE_TEST_RESOURCES_PATH - + recipePath + "/" + testCaseDir + "/" + inputFileName)); + final List violations = runCheckstyleAndGetViolations(inputPath); - final String afterCode = Files.readString(Paths.get(BASE_TEST_RESOURCES_PATH - + recipePath + "/" + testCaseDir + "/" + outputFileName)); + final CheckConfiguration checkConfigs = getAllCheckConfigurations(inputPath); + + final String beforeCode = readFile(getPath(inputPath)); + final String expectedAfterCode = readFile(getPath(outputPath)); + + final Recipe preprocessingRecipe = new InputClassRenamer(); + final Recipe mainRecipe = createRecipe(violations, checkConfigs); + + testRecipe(beforeCode, expectedAfterCode, preprocessingRecipe, mainRecipe); + } - final Recipe preprocessing = createPreprocessingRecipe(); - final Recipe mainRecipe = getRecipe(); + private List runCheckstyleAndGetViolations(String inputPath) + throws Exception { + + final String configFilePath = getPath(inputPath); + final TestInputConfiguration testInputConfiguration = + InlineConfigParser.parse(configFilePath); + final Configuration parsedConfig = testInputConfiguration.createConfiguration(); + + final Checker checker = createChecker(parsedConfig); + final ByteArrayOutputStream xmlOutput = new ByteArrayOutputStream(); + final XMLLogger logger = new XMLLogger(xmlOutput, XMLLogger.OutputStreamOptions.CLOSE); + checker.addListener(logger); + + final List filesToCheck = Collections.singletonList(new File(configFilePath)); + checker.process(filesToCheck); + + final Path tempXmlPath = Files.createTempFile("checkstyle-report", ".xml"); + try { + Files.write(tempXmlPath, xmlOutput.toByteArray()); + return CheckstyleReportParser.parse(tempXmlPath); + } finally { + Files.deleteIfExists(tempXmlPath); + } + } + + private CheckConfiguration getAllCheckConfigurations(String inputPath) throws Exception { + final String configFilePath = getPath(inputPath); + final TestInputConfiguration testInputConfiguration = + InlineConfigParser.parse(configFilePath); + return ConfigurationLoader.mapConfiguration(testInputConfiguration.createConfiguration()); + } + /** + * Helper method to test recipes using OpenRewrite testing framework. + */ + private void testRecipe(String beforeCode, String expectedAfterCode, + Recipe... recipes) { assertDoesNotThrow(() -> { rewriteRun( - spec -> spec.recipes(preprocessing, mainRecipe), - java(beforeCode, afterCode) + spec -> spec.recipes(recipes), + java(beforeCode, expectedAfterCode) ); }); } -} +} \ No newline at end of file diff --git a/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java b/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java index ab6e228..5db3731 100644 --- a/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java +++ b/src/test/java/org/checkstyle/autofix/recipe/UpperEllTest.java @@ -21,6 +21,7 @@ import java.nio.file.Path; import java.util.List; +import org.checkstyle.autofix.parser.CheckConfiguration; import org.checkstyle.autofix.parser.CheckstyleReportParser; import org.checkstyle.autofix.parser.CheckstyleViolation; import org.junit.jupiter.api.Test; @@ -29,27 +30,29 @@ public class UpperEllTest extends AbstractRecipeTest { @Override - protected Recipe getRecipe() { - final String reportPath = "src/test/resources/org/checkstyle/autofix/recipe/upperell" - + "/report.xml"; + protected String getSubpackage() { + return "upperell"; + } + + @Override + protected Recipe createRecipe(List violations, + CheckConfiguration checkConfigs) { - final List violations = - CheckstyleReportParser.parse(Path.of(reportPath)); return new UpperEll(violations); } @Test - void hexOctalLiteralTest() throws IOException { - testRecipe("upperell", "HexOctalLiteral"); + void hexOctalLiteral() throws Exception { + verify("HexOctalLiteral"); } @Test - void complexLongLiterals() throws IOException { - testRecipe("upperell", "ComplexLongLiterals"); + void complexLongLiterals() throws Exception { + verify("ComplexLongLiterals"); } @Test - void stringAndCommentTest() throws IOException { - testRecipe("upperell", "StringAndComments"); + void stringAndComments() throws Exception { + verify("StringAndComments"); } } diff --git a/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/InputHexOctalLiteral.java b/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/InputHexOctalLiteral.java index fbaf30c..8056a76 100644 --- a/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/InputHexOctalLiteral.java +++ b/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/InputHexOctalLiteral.java @@ -1,3 +1,9 @@ +/* +com.puppycrawl.tools.checkstyle.checks.UpperEllCheck + +*/ + + package org.checkstyle.autofix.recipe.upperell.hexoctalliteral; public class InputHexOctalLiteral { @@ -12,4 +18,4 @@ public void calculateValues() { long octalResult = 01234l + 0xDEADBEEFl; //suppressed violation for 0xDEADBEFl long binaryResult = 0b11110000l; } -} +} \ No newline at end of file diff --git a/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/OutputHexOctalLiteral.java b/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/OutputHexOctalLiteral.java index 840f57a..8910f64 100644 --- a/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/OutputHexOctalLiteral.java +++ b/src/test/resources/org/checkstyle/autofix/recipe/upperell/hexoctalliteral/OutputHexOctalLiteral.java @@ -1,3 +1,9 @@ +/* +UpperEll + + +*/ + package org.checkstyle.autofix.recipe.upperell.hexoctalliteral; public class OutputHexOctalLiteral {