Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass in single recipe configuration options on the command line #816

Merged
merged 10 commits into from
Jul 24, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.apache.maven.plugins.annotations.Parameter;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
import org.openrewrite.*;
import org.openrewrite.config.CompositeRecipe;
import org.openrewrite.config.DeclarativeRecipe;
import org.openrewrite.config.Environment;
import org.openrewrite.config.RecipeDescriptor;
import org.openrewrite.internal.InMemoryLargeSourceSet;
Expand All @@ -34,6 +36,7 @@

import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
Expand All @@ -55,6 +58,10 @@ public abstract class AbstractRewriteBaseRunMojo extends AbstractRewriteMojo {
@Parameter(property = "rewrite.exportDatatables", defaultValue = "false")
protected boolean exportDatatables;

@Parameter(property = "rewrite.options")
@Nullable
protected LinkedHashSet<String> options;

/**
* Attempt to determine the root of the git repository for the given project.
* Many Gradle builds co-locate the build root with the git repository root, but that is not required.
Expand Down Expand Up @@ -91,13 +98,17 @@ protected ResultsContainer listResults(ExecutionContext ctx) throws MojoExecutio
Environment env = environment(recipeArtifactCoordinatesClassloader);

Recipe recipe = env.activateRecipes(getActiveRecipes());
if (recipe.getRecipeList().isEmpty()) {
getLog().warn("No recipes were activated. " +
"Activate a recipe with <activeRecipes><recipe>com.fully.qualified.RecipeClassName</recipe></activeRecipes> in this plugin's <configuration> in your pom.xml, " +
"or on the command line with -Drewrite.activeRecipes=com.fully.qualified.RecipeClassName");
if (recipe.getName().equals("org.openrewrite.Recipe$Noop")) {
getLog().warn("No recipes were activated." +
" Activate a recipe with <activeRecipes><recipe>com.fully.qualified.RecipeClassName</recipe></activeRecipes> in this plugin's <configuration> in your pom.xml," +
" or on the command line with -Drewrite.activeRecipes=com.fully.qualified.RecipeClassName");
return new ResultsContainer(repositoryRoot, emptyList());
}

if (options != null && !options.isEmpty()) {
configureRecipeOptions(recipe, options);
}

getLog().info("Validating active recipes...");
List<Validated<Object>> validations = new ArrayList<>();
recipe.validateAll(ctx, validations);
Expand Down Expand Up @@ -126,6 +137,72 @@ protected ResultsContainer listResults(ExecutionContext ctx) throws MojoExecutio
}
}

private static void configureRecipeOptions(Recipe recipe, Set<String> options) throws MojoExecutionException {
if (recipe instanceof CompositeRecipe ||
recipe instanceof DeclarativeRecipe ||
recipe instanceof Recipe.DelegatingRecipe ||
!recipe.getRecipeList().isEmpty()) {
// We don't (yet) support configuring potentially nested recipes, as recipes might occur more than once,
// and setting the same value twice might lead to unexpected behavior.
throw new MojoExecutionException(
"Recipes containing other recipes can not be configured from the command line: " + recipe);
}

Map<String, String> optionValues = new HashMap<>();
for (String option : options) {
String[] parts = option.split("=", 2);
if (parts.length == 2) {
optionValues.put(parts[0], parts[1]);
}
}
for (Field field : recipe.getClass().getDeclaredFields()) {
String removed = optionValues.remove(field.getName());
updateOption(recipe, field, removed);
}
if (!optionValues.isEmpty()) {
throw new MojoExecutionException(
String.format("Unknown recipe options: %s", String.join(", ", optionValues.keySet())));
}
}

private static void updateOption(Recipe recipe, Field field, @Nullable String optionValue) throws MojoExecutionException {
Object convertedOptionValue = convertOptionValue(field.getName(), optionValue, field.getType());
if (convertedOptionValue == null) {
return;
}
try {
field.setAccessible(true);
field.set(recipe, convertedOptionValue);
field.setAccessible(false);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new MojoExecutionException(
String.format("Unable to configure recipe '%s' option '%s' with value '%s'",
recipe.getClass().getSimpleName(), field.getName(), optionValue));
}
}

private static @Nullable Object convertOptionValue(String name, @Nullable String optionValue, Class<?> type)
throws MojoExecutionException {
if (optionValue == null) {
return null;
}
if (type.isAssignableFrom(String.class)) {
return optionValue;
}
if (type.isAssignableFrom(boolean.class) || type.isAssignableFrom(Boolean.class)) {
return Boolean.parseBoolean(optionValue);
}
if (type.isAssignableFrom(int.class) || type.isAssignableFrom(Integer.class)) {
return Integer.parseInt(optionValue);
}
if (type.isAssignableFrom(long.class) || type.isAssignableFrom(Long.class)) {
return Long.parseLong(optionValue);
}

throw new MojoExecutionException(
String.format("Unable to convert option: %s value: %s to type: %s", name, optionValue, type));
}

protected LargeSourceSet loadSourceSet(Path repositoryRoot, Environment env, ExecutionContext ctx) throws DependencyResolutionRequiredException, MojoExecutionException {
List<NamedStyles> styles = loadStyles(project, env);

Expand All @@ -144,7 +221,7 @@ protected List<Result> runRecipe(Recipe recipe, LargeSourceSet sourceSet, Execut
if (exportDatatables) {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss-SSS"));
Path datatableDirectoryPath = Paths.get("target", "rewrite", "datatables", timestamp);
getLog().info(String.format("Printing Available Datatables to: %s", datatableDirectoryPath));
getLog().info(String.format("Printing available datatables to: %s", datatableDirectoryPath));
recipeRun.exportDatatablesToCsv(datatableDirectoryPath, ctx);
}

Expand Down
14 changes: 13 additions & 1 deletion src/test/java/org/openrewrite/maven/RewriteRunIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,19 @@ void cloud_suitability_project(MavenExecutionResult result) {
.anySatisfy(line -> assertThat(line).contains("some.jks"));
}

@MavenTest
@SystemProperties({
@SystemProperty(value = "rewrite.activeRecipes", content = "org.openrewrite.maven.RemovePlugin"),
@SystemProperty(value = "rewrite.options", content = "groupId=org.openrewrite.maven,artifactId=rewrite-maven-plugin")
})
void command_line_options(MavenExecutionResult result) {
assertThat(result).isSuccessful().out().error().isEmpty();
assertThat(result).isSuccessful().out().warn()
.contains("Changes have been made to target/maven-it/org/openrewrite/maven/RewriteRunIT/command_line_options/project/pom.xml by:")
.contains(" org.openrewrite.maven.RemovePlugin");
assertThat(result.getMavenProjectResult().getModel().getBuild()).isNull();
}

@MavenTest
@Disabled("We should implement a simpler test to make sure that regular markers don't get added to source files")
void java_upgrade_project(MavenExecutionResult result) {
Expand All @@ -96,5 +109,4 @@ void java_compiler_plugin_project(MavenExecutionResult result) {
.filteredOn(line -> line.contains("Changes have been made"))
.hasSize(1);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.openrewrite.maven</groupId>
<artifactId>command_line_options</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>RewriteRunIT#command_line_options</name>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
</plugin>
</plugins>
</build>
</project>