diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index 12ec7782fb..f14b20da19 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE Release with new features and bugfixes: +* https://github.com/devonfw/IDEasy/issues/1031[#1031]: Added OpenRewrite commandlet * https://github.com/devonfw/IDEasy/issues/2361[#2361]: Improve dotnet installation by setting DOTNET_ROOT * https://github.com/devonfw/IDEasy/issues/218[#218]: Added possibility to start a command in a new window using the process builder * https://github.com/devonfw/IDEasy/issues/788[#788]: Add support for IDE_OPTIONS variable per IDE commandlet diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java index 79cdff16b2..0d45e767ca 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java @@ -139,6 +139,7 @@ public CommandletManagerImpl(IdeContext context) { add(new Npm(context)); add(new Mvn(context)); add(new Msvc(context)); + add(new RewriteCommandlet(context)); add(new GcLogAnalyzer(context)); add(new GcViewer(context)); add(new Gradle(context)); diff --git a/cli/src/main/java/com/devonfw/tools/ide/commandlet/RewriteCommandlet.java b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RewriteCommandlet.java new file mode 100644 index 0000000000..7ece1bb5f5 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/commandlet/RewriteCommandlet.java @@ -0,0 +1,161 @@ +package com.devonfw.tools.ide.commandlet; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.IdeContext; +import com.devonfw.tools.ide.property.EnumProperty; +import com.devonfw.tools.ide.tool.mvn.Mvn; +import com.devonfw.tools.ide.tool.openrewrite.RecipeManager; +import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapper; +import com.devonfw.tools.ide.tool.openrewrite.RewriteRecipeEnum; + +/** + * {@link Commandlet} for OpenRewrite refactoring. + */ +public class RewriteCommandlet extends Commandlet { + + private static final Logger LOG = LoggerFactory.getLogger(RewriteCommandlet.class); + + public final EnumProperty command; + private final RecipeManager recipeManager; + + /** + * The constructor. + * + * @param context the {@link IdeContext}. + */ + public RewriteCommandlet(IdeContext context) { + + super(context); + addKeyword(getName()); + this.command = add(new EnumProperty<>("", true, "recipe_name", RewriteRecipeEnum.class)); + this.recipeManager = new RecipeManager(); + } + + @Override + public String getName() { + + return "rewrite"; + } + + /** + * Parses the raw MVN command from JSON into individual argument tokens, stripping the leading "mvn" if present. + * + * @param recipeRawCommands the raw command string from the recipe configuration. + * @return list of individual MVN argument tokens. + */ + private List adaptMVNCommand(String recipeRawCommands) { + String trimmed = recipeRawCommands.trim(); + if (trimmed.startsWith("mvn ")) { + trimmed = trimmed.substring(4); + } else if (trimmed.equals("mvn")) { + trimmed = ""; + } + List args = new ArrayList<>(); + for (String token : trimmed.split("\\s+")) { + if (!token.isEmpty()) { + args.add(token); + } + } + return args; + } + + /** + * Converts a raw command to dry-run mode by replacing the ":run" goal with ":dryRun". + * + * @param recipeRawCommands the raw command string. + * @return the command with dry-run mode applied. + * @throws CliException if the command does not contain a ":run" goal to replace. + */ + private String changeToDryRunCommand(String recipeRawCommands) { + String result = recipeRawCommands.replace(":run", ":dryRun"); + if (result.equals(recipeRawCommands)) { + throw new CliException("Cannot convert to dry-run: command does not contain ':run' goal: " + recipeRawCommands); + } + return result; + } + + private void showInfo(RecipeWrapper wrapper) { + LOG.info("Recipe [{}], {}", wrapper.ideasyCommand.name(), wrapper.description); + LOG.info("Reference {}", wrapper.url); + LOG.info("Raw command: {}", wrapper.rawCmd); + } + + private boolean confirmApplyChange() { + + String input = this.context.askForInput( + "***Before making actual changes to the code, please confirm it seriously." + + " It is strongly recommended to perform a DRY-RUN first***\n" + + "Type yes to apply changes, or press other keys to perform DRY-RUN: "); + + return input.equalsIgnoreCase("yes"); + + } + + /** + * Searches up from the current working directory to find the nearest pom.xml. + * + * @return the path to the project root containing pom.xml. + * @throws CliException if no pom.xml is found or cwd is not set. + */ + Path findProjectRoot() { + Path dir = this.context.getCwd(); + if (dir == null) { + throw new CliException("Cannot determine current working directory"); + } + while (dir != null) { + if (Files.exists(dir.resolve("pom.xml"))) { + return dir; + } + dir = dir.getParent(); + } + throw new CliException("No pom.xml found in current directory or any parent directory"); + } + + @Override + public void doRun() { + + LOG.info("{} called", getClass().getSimpleName()); + + RewriteRecipeEnum recipeEnum = this.command.getValue(); + + if (!recipeManager.isValidRecipeEnum(recipeEnum)) { + throw new CliException("Invalid recipe name: " + recipeEnum); + } + + RecipeWrapper wrapper = recipeManager.getRecipeWrapper(recipeEnum); + + Path projectRoot = findProjectRoot(); + LOG.info("Target project: {}", projectRoot); + + showInfo(wrapper); + + String commandLine = wrapper.rawCmd; + + if (!confirmApplyChange()) { + commandLine = changeToDryRunCommand(commandLine); + } + + LOG.info("Actual command line: {}", commandLine); + + List args = adaptMVNCommand(commandLine); + // Inject -f so Maven runs in the correct project + args.add(0, "-f"); + args.add(1, projectRoot.toString()); + getCommandlet(Mvn.class).runTool(args); + + } + + @Override + public boolean isIdeHomeRequired() { + + return false; + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java b/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java index 8bd0714350..a5f354c90b 100644 --- a/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java +++ b/cli/src/main/java/com/devonfw/tools/ide/json/JsonMapping.java @@ -26,6 +26,8 @@ import com.devonfw.tools.ide.tool.npm.NpmJsVersions; import com.devonfw.tools.ide.tool.npm.NpmJsVersionsJsonDeserializer; import com.devonfw.tools.ide.tool.npm.NpmJsVersionsJsonSerializer; +import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapper; +import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapperJsonDeserializer; import com.devonfw.tools.ide.tool.pip.PypiObject; import com.devonfw.tools.ide.tool.pip.PypiObjectJsonDeserializer; import com.devonfw.tools.ide.tool.pip.PypiObjectJsonSerializer; @@ -102,6 +104,8 @@ private static ObjectMapper create(boolean supportReflection) { customModule.addDeserializer(ToolSecurity.class, new ToolSecurityJsonDeserializer()); customModule.addSerializer(Cve.class, new CveJsonSerializer()); customModule.addDeserializer(Cve.class, new CveJsonDeserializer()); + // openrewrite mapping + customModule.addDeserializer(RecipeWrapper.class, new RecipeWrapperJsonDeserializer()); // pypi mapping customModule.addDeserializer(PypiObject.class, new PypiObjectJsonDeserializer()); customModule.addSerializer(PypiObject.class, new PypiObjectJsonSerializer()); diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManager.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManager.java new file mode 100644 index 0000000000..74e9ff4b69 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManager.java @@ -0,0 +1,72 @@ +package com.devonfw.tools.ide.tool.openrewrite; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.json.JsonMapping; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Manages the loading and lookup of OpenRewrite {@link RecipeWrapper} configurations from JSON. + */ +public class RecipeManager { + + private static final String OPEN_REWRITE_CONFIG_JSON_PATH = "refactor/openrewrite.json"; + + private final Map recipes; + + public RecipeManager() { + InputStream is = RecipeManager.class.getClassLoader().getResourceAsStream(OPEN_REWRITE_CONFIG_JSON_PATH); + if (is == null) { + throw new CliException("Failed to load " + OPEN_REWRITE_CONFIG_JSON_PATH + " from classpath"); + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + ObjectMapper objectMapper = JsonMapping.create(); + List wrapperList = objectMapper.readValue(reader, objectMapper.getTypeFactory().constructCollectionType(List.class, RecipeWrapper.class)); + + java.util.LinkedHashMap map = new java.util.LinkedHashMap<>(); + for (RecipeWrapper one : wrapperList) { + map.put(one.ideasyCommand, one); + } + this.recipes = java.util.Collections.unmodifiableMap(map); + } catch (IOException e) { + throw new CliException("Failed to load " + OPEN_REWRITE_CONFIG_JSON_PATH, e); + } + } + + /** + * Returns the list of all loaded {@link RecipeWrapper} configurations. + * + * @return an unmodifiable list of available recipes. + */ + public List listAvailableRecipes() { + + return List.copyOf(recipes.values()); + } + + /** + * Checks if a recipe for the given {@link RewriteRecipeEnum} exists in the configuration. + * + * @param recipeEnum the enum constant identifying the recipe. + * @return {@code true} if a matching recipe was found. + */ + public boolean isValidRecipeEnum(RewriteRecipeEnum recipeEnum) { + return recipes.containsKey(recipeEnum); + } + + /** + * Returns the {@link RecipeWrapper} for the given {@link RewriteRecipeEnum}. + * + * @param recipeEnum the enum constant identifying the recipe. + * @return the recipe wrapper. + */ + public RecipeWrapper getRecipeWrapper(RewriteRecipeEnum recipeEnum) { + return recipes.get(recipeEnum); + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapper.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapper.java new file mode 100644 index 0000000000..ae365c5b02 --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapper.java @@ -0,0 +1,21 @@ +package com.devonfw.tools.ide.tool.openrewrite; + +import com.devonfw.tools.ide.json.JsonObject; + +public class RecipeWrapper implements JsonObject { + + public String description; + public String originName; + public String url; + public RewriteRecipeEnum ideasyCommand; + public String rawCmd; + + public RecipeWrapper(String description, String originName, String url, RewriteRecipeEnum ideasyCommand, String rawCmd) { + this.description = description; + this.originName = originName; + this.url = url; + this.ideasyCommand = ideasyCommand; + this.rawCmd = rawCmd; + } + +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapperJsonDeserializer.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapperJsonDeserializer.java new file mode 100644 index 0000000000..a09d87d56b --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RecipeWrapperJsonDeserializer.java @@ -0,0 +1,73 @@ +package com.devonfw.tools.ide.tool.openrewrite; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.devonfw.tools.ide.json.JsonBuilder; +import com.devonfw.tools.ide.json.JsonObjectDeserializer; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; + +/** + * {@link JsonObjectDeserializer} for {@link RecipeWrapper}. + */ +public class RecipeWrapperJsonDeserializer extends JsonObjectDeserializer { + + private static final Logger LOG = LoggerFactory.getLogger(RecipeWrapperJsonDeserializer.class); + + @Override + protected JsonBuilder createBuilder() { + + return new RecipeWrapperBuilder(); + } + + private class RecipeWrapperBuilder extends JsonBuilder { + + private String description; + private String originName; + private String url; + private RewriteRecipeEnum ideasyCommand; + private String rawCmd; + + @Override + public void setProperty(String property, JsonParser p, DeserializationContext ctxt) throws IOException { + + switch (property) { + case "description" -> { + this.description = readValueAsString(p, property, this.description); + } + case "origin_name" -> { + this.originName = readValueAsString(p, property, this.originName); + } + case "url" -> { + this.url = readValueAsString(p, property, this.url); + } + case "ideasy_command" -> { + String value = readValueAsString(p, property, null); + if (value != null) { + try { + this.ideasyCommand = RewriteRecipeEnum.valueOf(value); + } catch (IllegalArgumentException e) { + this.ideasyCommand = RewriteRecipeEnum.UNRECOGNIZED_RECIPE; + } + } + } + case "raw_cmd" -> { + this.rawCmd = readValueAsString(p, property, this.rawCmd); + } + default -> { + LOG.warn("Unknown recipe property: {}", property); + super.setProperty(property, p, ctxt); + } + } + } + + @Override + public RecipeWrapper build() { + + return new RecipeWrapper(this.description, this.originName, this.url, this.ideasyCommand, this.rawCmd); + } + } +} diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RewriteRecipeEnum.java b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RewriteRecipeEnum.java new file mode 100644 index 0000000000..22d00a90bd --- /dev/null +++ b/cli/src/main/java/com/devonfw/tools/ide/tool/openrewrite/RewriteRecipeEnum.java @@ -0,0 +1,5 @@ +package com.devonfw.tools.ide.tool.openrewrite; + +public enum RewriteRecipeEnum { + FORMAT_JAVA_CODE, REMOVE_BLANK_LINES, UNRECOGNIZED_RECIPE +} diff --git a/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json b/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json index 9d570eb1c6..5870fea20d 100644 --- a/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json +++ b/cli/src/main/resources/META-INF/native-image/com.devonfw.tools.IDEasy/ide-cli/resource-config.json @@ -1,7 +1,8 @@ { "resources": { "includes": [ - {"pattern": "nls/.*"} + {"pattern": "nls/.*"}, + {"pattern": "refactor/openrewrite.json"} ] }, "bundles": [ diff --git a/cli/src/main/resources/nls/Help.properties b/cli/src/main/resources/nls/Help.properties index b13135299e..dd8b8b4580 100644 --- a/cli/src/main/resources/nls/Help.properties +++ b/cli/src/main/resources/nls/Help.properties @@ -125,6 +125,9 @@ cmd.release.val.args=Additional arguments to pass to the build and deploy comman cmd.repository=Set up pre-configured git repositories using 'ide repository setup ' cmd.repository.detail=Without further arguments this will set up all pre-configured git repositories.\nAlso, you can provide an explicit git repo as `` argument and IDEasy will automatically clone, build and set up your project based on the existing property file.\nRepositories are configured in 'settings/repository/.properties' and can therefore be shared with your project team for automatic or optional setup. cmd.repository.val.repository=The name of the properties file of the pre-configured git repository to set up, omit to set up all active repositories. +cmd.rewrite=Refactor existing code base with specific recipes provided by OpenRewrite +cmd.rewrite.detail=OpenRewrite is a popular tool for refactoring (meta-programming). Detailed documentation can be found at https://docs.openrewrite.org/ +cmd.rewrite.val.recipe_name=OpenRewrite recipe names (FORMAT_JAVA_CODE|REMOVE_BLANK_LINES) cmd.ruby=Tool commandlet for Ruby (programming language). cmd.ruby.detail=Ruby is a dynamic, open-source programming language with a focus on simplicity and productivity. Detailed documentation can be found at https://www.ruby-lang.org/en/documentation/ cmd.ruff=Tool commandlet for Ruff. diff --git a/cli/src/main/resources/nls/Help_de.properties b/cli/src/main/resources/nls/Help_de.properties index b87c187278..e14d531489 100644 --- a/cli/src/main/resources/nls/Help_de.properties +++ b/cli/src/main/resources/nls/Help_de.properties @@ -125,6 +125,9 @@ cmd.release.val.args=Zusätzliche Argumente, die an den Build- und Deploy-Befehl cmd.repository=Richtet das vorkonfigurierte Git Repository ein mittels 'ide repository setup '. cmd.repository.detail=Dies wird alle vorkonfigurierten Repositories einrichten. Rufen Sie einfach 'ide repository setup ' auf, ersetzen Sie durch den Namen Ihrer Projektkonfigurationsdatei, die sich in 'settings/repository/your_project_name' befindet und IDEasy wird Ihr Projekt basierend auf der vorhandenen Eigenschaftsdatei automatisch klonen, bauen und einrichten.\nWenn Sie den Projektnamen weglassen, werden alle im Repository-Verzeichnis gefundenen Projekte vorkonfiguriert. cmd.repository.val.repository=Der Name der Properties-Datei des vorkonfigurierten Git Repositories zum Einrichten. Falls nicht angegeben, werden alle aktiven Projekte eingerichtet. +cmd.rewrite=Refaktorieren Sie die vorhandene Codebasis mit spezifischen Rezepten von OpenRewrite +cmd.rewrite.detail=OpenRewrite ist ein beliebtes Tool für Refactoring (Metaprogrammierung). Eine ausführliche Dokumentation finden Sie unter https://docs.openrewrite.org/ +cmd.rewrite.val.recipe_name=OpenRewrite Rezeptnamen (FORMAT_JAVA_CODE|REMOVE_BLANK_LINES) cmd.ruby=Werkzeug Kommando für Ruby (Programmiersprache). cmd.ruby.detail=Ruby ist eine dynamische, quelloffene Programmiersprache mit Fokus auf Einfachheit und Produktivität. Detaillierte Dokumentation findet sich unter https://www.ruby-lang.org/de/documentation/ cmd.ruff=Werkzeug Kommando für Ruff. diff --git a/cli/src/main/resources/refactor/openrewrite.json b/cli/src/main/resources/refactor/openrewrite.json new file mode 100644 index 0000000000..4ed6375340 --- /dev/null +++ b/cli/src/main/resources/refactor/openrewrite.json @@ -0,0 +1,16 @@ +[ + { + "origin_name": "java.format_java_code", + "ideasy_command": "FORMAT_JAVA_CODE", + "description": "Format Java code using a standard comprehensive set of Java formatting recipes.", + "url": "https://docs.openrewrite.org/recipes/java/format/autoformat", + "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.AutoFormat -Drewrite.exportDatatables=true" + }, + { + "origin_name": "java.remove_blank_lines", + "ideasy_command": "REMOVE_BLANK_LINES", + "description": "Add and/or remove blank lines.", + "url": "https://docs.openrewrite.org/recipes/java/format/blanklines", + "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.BlankLines -Drewrite.exportDatatables=true" + } +] diff --git a/cli/src/test/java/com/devonfw/tools/ide/commandlet/RewriteCommandletTest.java b/cli/src/test/java/com/devonfw/tools/ide/commandlet/RewriteCommandletTest.java new file mode 100644 index 0000000000..5c905cbc95 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/commandlet/RewriteCommandletTest.java @@ -0,0 +1,146 @@ +package com.devonfw.tools.ide.commandlet; + + +import java.nio.file.Path; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.devonfw.tools.ide.cli.CliException; +import com.devonfw.tools.ide.context.AbstractIdeContextTest; +import com.devonfw.tools.ide.context.IdeTestContext; +import com.devonfw.tools.ide.tool.openrewrite.RecipeManager; +import com.devonfw.tools.ide.tool.openrewrite.RecipeWrapper; +import com.devonfw.tools.ide.tool.openrewrite.RewriteRecipeEnum; + +/** + * Tests for {@link RewriteCommandlet}. + */ +class RewriteCommandletTest extends AbstractIdeContextTest { + + /** + * Tests that the RewriteCommandlet is registered and accessible via the commandlet manager. + */ + @Test + void testCommandletExists() { + IdeTestContext context = newContext("basic"); + RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class); + assertThat(commandlet).isNotNull(); + assertThat(commandlet.getName()).isEqualTo("rewrite"); + } + + /** + * Tests that {@link RecipeManager} correctly loads recipes and validates enums. + */ + @Test + void testRecipeManagerLoadsRecipes() { + RecipeManager manager = new RecipeManager(); + assertThat(manager.listAvailableRecipes()).isNotEmpty(); + assertThat(manager.isValidRecipeEnum(RewriteRecipeEnum.FORMAT_JAVA_CODE)).isTrue(); + assertThat(manager.isValidRecipeEnum(RewriteRecipeEnum.UNRECOGNIZED_RECIPE)).isFalse(); + } + + /** + * Tests that the dry-run conversion correctly replaces :run with :dryRun. + */ + @Test + void testDryRunConversion() { + RecipeManager manager = new RecipeManager(); + RecipeWrapper wrapper = manager.getRecipeWrapper(RewriteRecipeEnum.FORMAT_JAVA_CODE); + assertThat(wrapper.rawCmd).contains(":run"); + + String dryRunCmd = wrapper.rawCmd.replace(":run", ":dryRun"); + assertThat(dryRunCmd).contains(":dryRun"); + assertThat(dryRunCmd).doesNotContain(":run"); + } + + /** + * Tests that the dry-run conversion fails when no :run goal is present. + */ + @Test + void testDryRunConversionFailsWithoutRunGoal() { + String commandWithoutRun = "mvn -U some:otherGoal -Dfoo=bar"; + String result = commandWithoutRun.replace(":run", ":dryRun"); + // No replacement happened + assertThat(result).isEqualTo(commandWithoutRun); + } + + /** + * Tests that the commandlet properly parses MVN commands by stripping the "mvn" prefix. + */ + @Test + void testAdaptMvnCommand() { + RecipeManager manager = new RecipeManager(); + RecipeWrapper wrapper = manager.getRecipeWrapper(RewriteRecipeEnum.FORMAT_JAVA_CODE); + + // Verify the raw command starts with "mvn" + assertThat(wrapper.rawCmd).startsWith("mvn"); + + String trimmed = wrapper.rawCmd.trim(); + String withoutPrefix = trimmed.substring(4); // strip "mvn " + List args = List.of(withoutPrefix.split("\\s+")); + assertThat(args).isNotEmpty(); + assertThat(args.getFirst()).startsWith("-"); // should be a Maven flag + } + + /** + * Tests that ideHome is not required for the RewriteCommandlet. + */ + @Test + void testIdeHomeNotRequired() { + IdeTestContext context = newContext("basic"); + RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class); + assertThat(commandlet.isIdeHomeRequired()).isFalse(); + } + + /** + * Tests that {@link RewriteCommandlet#doRun()} throws a {@link CliException} when an invalid recipe enum is provided, exercising the error handling path. + */ + @Test + void testDoRunThrowsForInvalidRecipe() { + IdeTestContext context = newContext("basic"); + RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class); + commandlet.command.setValue(RewriteRecipeEnum.UNRECOGNIZED_RECIPE); + assertThatThrownBy(commandlet::doRun).isInstanceOf(CliException.class).hasMessageContaining("Invalid recipe name"); + } + + /** + * Tests that {@link RewriteCommandlet#findProjectRoot()} locates the nearest pom.xml when the current working directory contains one. + */ + @Test + void testFindProjectRootFindsPomInCurrentDirectory() { + IdeTestContext context = newContext("release"); + RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class); + // mvn directory contains pom.xml + Path mvnDir = context.getWorkspacePath().resolve("mvn"); + context.setCwd(mvnDir, "main", context.getIdeHome()); + Path projectRoot = commandlet.findProjectRoot(); + assertThat(projectRoot).isEqualTo(mvnDir); + } + + /** + * Tests that {@link RewriteCommandlet#findProjectRoot()} walks up the directory tree to find the nearest pom.xml. + */ + @Test + void testFindProjectRootWalksUpDirectoryTree() { + IdeTestContext context = newContext("release"); + RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class); + // empty directory does NOT contain pom.xml - should walk up to find it + Path emptyDir = context.getWorkspacePath().resolve("empty"); + context.setCwd(emptyDir, "main", context.getIdeHome()); + Path projectRoot = commandlet.findProjectRoot(); + // Should find the pom.xml in the parent workspaces/main directory or above + assertThat(projectRoot.resolve("pom.xml")).exists(); + } + + /** + * Tests that {@link RewriteCommandlet#findProjectRoot()} throws when cwd is not set. + */ + @Test + void testFindProjectRootThrowsWhenNoCwd() { + IdeTestContext context = newContext("basic"); + RewriteCommandlet commandlet = context.getCommandletManager().getCommandlet(RewriteCommandlet.class); + context.setCwd(null, "main", context.getIdeHome()); + assertThatThrownBy(commandlet::findProjectRoot).isInstanceOf(CliException.class).hasMessageContaining("current working directory"); + } +} diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManagerTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManagerTest.java new file mode 100644 index 0000000000..5a5e5b4ad8 --- /dev/null +++ b/cli/src/test/java/com/devonfw/tools/ide/tool/openrewrite/RecipeManagerTest.java @@ -0,0 +1,31 @@ +package com.devonfw.tools.ide.tool.openrewrite; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link RecipeManager}. + */ +class RecipeManagerTest extends Assertions { + + static RecipeManager manager; + + @BeforeAll + static void init() { + manager = new RecipeManager(); + } + + @Test + void testCreation() { + assertThat(manager.listAvailableRecipes()).isNotEmpty(); + } + + @Test + void testEnumValidation() { + assertThat(manager.isValidRecipeEnum(RewriteRecipeEnum.UNRECOGNIZED_RECIPE)).isFalse(); + var validRecipe = java.util.Arrays.stream(RewriteRecipeEnum.values()) + .filter(x -> x != RewriteRecipeEnum.UNRECOGNIZED_RECIPE).findAny().get(); + assertThat(manager.isValidRecipeEnum(validRecipe)).isTrue(); + } +} diff --git a/cli/src/test/resources/refactor/openrewrite.json b/cli/src/test/resources/refactor/openrewrite.json new file mode 100644 index 0000000000..4ed6375340 --- /dev/null +++ b/cli/src/test/resources/refactor/openrewrite.json @@ -0,0 +1,16 @@ +[ + { + "origin_name": "java.format_java_code", + "ideasy_command": "FORMAT_JAVA_CODE", + "description": "Format Java code using a standard comprehensive set of Java formatting recipes.", + "url": "https://docs.openrewrite.org/recipes/java/format/autoformat", + "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.AutoFormat -Drewrite.exportDatatables=true" + }, + { + "origin_name": "java.remove_blank_lines", + "ideasy_command": "REMOVE_BLANK_LINES", + "description": "Add and/or remove blank lines.", + "url": "https://docs.openrewrite.org/recipes/java/format/blanklines", + "raw_cmd": "mvn -U org.openrewrite.maven:rewrite-maven-plugin:run -Drewrite.activeRecipes=org.openrewrite.java.format.BlankLines -Drewrite.exportDatatables=true" + } +]