Skip to content

Commit

Permalink
Recipe#buildRecipeList to aid AI code assistants to write recipes (#4331
Browse files Browse the repository at this point in the history
)
  • Loading branch information
jkschneider authored Jul 16, 2024
1 parent 7eb376f commit 7d056fd
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 1 deletion.
87 changes: 86 additions & 1 deletion rewrite-core/src/main/java/org/openrewrite/Recipe.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;
import org.intellij.lang.annotations.Language;
import org.openrewrite.config.DataTableDescriptor;
import org.openrewrite.config.OptionDescriptor;
Expand Down Expand Up @@ -308,11 +311,40 @@ public boolean causesAnotherCycle() {
* A list of recipes that run, source file by source file,
* after this recipe. This method is guaranteed to be called only once
* per cycle.
* <p>
* When creating a recipe with a fixed recipe list, either override
* this method or {@link #buildRecipeList(RecipeList)} but ideally not
* both, as their default implementations are interconnected.
*
* @return The list of recipes to run.
*/
public List<Recipe> getRecipeList() {
return Collections.emptyList();
RecipeList list = new RecipeList(getName());
buildRecipeList(list);
return list.getRecipes();
}

/**
* Used to build up a recipe list programmatically. Using the
* methods on {@link RecipeList}, the appearance of a recipe
* that chains other recipes with options will be not strikingly
* different from defining it in a recipe.yml.
* <p>
* Building, or at least starting to build, recipes for complex
* migrations with this method is more amenable to AI coding assistants
* since these assistants are primarily optimized for providing completion
* assistance in a single file.
* <p>
* When creating a recipe with a fixed recipe list, either override
* this method or {@link #getRecipeList()} but ideally not
* both, as their default implementations are interconnected.
*
* @param list A recipe list used to build up a series of recipes
* in code in a way that looks fairly declarative and
* therefore is more amenable to AI code completion.
*/
@SuppressWarnings("unused")
public void buildRecipeList(RecipeList list) {
}

/**
Expand Down Expand Up @@ -424,4 +456,57 @@ public Object clone() {
public interface DelegatingRecipe {
Recipe getDelegate();
}

/**
* @return A new recipe builder.
*/
@Incubating(since = "8.31.0")
public static Builder builder(@NlsRewrite.DisplayName @Language("markdown") String displayName,
@NlsRewrite.Description @Language("markdown") String description) {
return new Builder(displayName, description);
}

@Incubating(since = "8.31.0")
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public static class Builder {
@NlsRewrite.DisplayName
@Language("markdown")
final String displayName;

@NlsRewrite.Description
@Language("markdown")
final String description;

TreeVisitor<? extends Tree, ExecutionContext> visitor = TreeVisitor.noop();

public Builder visitor(TreeVisitor<? extends Tree, ExecutionContext> visitor) {
this.visitor = visitor;
return this;
}

public Recipe build(String name) {
return new Recipe() {
@Override
public String getName() {
return name;
}

@Override
public String getDisplayName() {
return displayName;
}

@Override
public String getDescription() {
return description;
}

@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return visitor;
}
};
}
}
}
59 changes: 59 additions & 0 deletions rewrite-core/src/main/java/org/openrewrite/RecipeList.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite;

import lombok.RequiredArgsConstructor;
import org.intellij.lang.annotations.Language;

import java.util.ArrayList;
import java.util.List;

import static java.util.Collections.emptyList;

@Incubating(since = "8.31.0")
@RequiredArgsConstructor
public class RecipeList {
private final String parentRecipeName;
private int recipeIndex = 1;

private List<Recipe> recipes;

public RecipeList recipe(Recipe.Builder recipe) {
return addRecipe(recipe.build(parentRecipeName + "$" + recipeIndex++));
}

public RecipeList recipe(@NlsRewrite.DisplayName @Language("markdown") String displayName,
@NlsRewrite.Description @Language("markdown") String description,
TreeVisitor<? extends Tree, ExecutionContext> visitor) {
return recipe(Recipe.builder(displayName, description).visitor(visitor));
}

public RecipeList recipe(org.openrewrite.Recipe recipe) {
return addRecipe(recipe);
}

public List<Recipe> getRecipes() {
return recipes == null ? emptyList() : recipes;
}

private RecipeList addRecipe(Recipe recipe) {
if (recipes == null) {
recipes = new ArrayList<>();
}
recipes.add(recipe);
return this;
}
}
104 changes: 104 additions & 0 deletions rewrite-core/src/test/java/org/openrewrite/RecipeListTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2024 the original author or authors.
* <p>
* 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
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.openrewrite;

import lombok.EqualsAndHashCode;
import lombok.Value;
import org.junit.jupiter.api.Test;
import org.openrewrite.config.RecipeDescriptor;
import org.openrewrite.marker.RecipesThatMadeChanges;
import org.openrewrite.test.RewriteTest;
import org.openrewrite.text.FindAndReplace;
import org.openrewrite.text.PlainText;
import org.openrewrite.text.PlainTextVisitor;

import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.openrewrite.test.SourceSpecs.text;

public class RecipeListTest implements RewriteTest {

@Test
void declarativeRecipeInCode() {
rewriteRun(
specs -> specs.recipe(new FormalHello("jon", "jonathan"))
.expectedCyclesThatMakeChanges(1).cycles(1),
text(
"hi jon",
"hello jonathan",
spec -> spec.afterRecipe(txt -> {
Optional<Stream<String>> recipeNames = txt.getMarkers().findFirst(RecipesThatMadeChanges.class)
.map(recipes -> recipes.getRecipes().stream()
.map(stack -> stack.stream().map(Recipe::getDescriptor).map(RecipeDescriptor::getName)
.collect(Collectors.joining("->")))
);

assertThat(recipeNames).isPresent();
assertThat(recipeNames.get()).containsExactly(
"org.openrewrite.FormalHello->org.openrewrite.text.FindAndReplace",
"org.openrewrite.FormalHello->org.openrewrite.FormalHello$1"
);
})
)
);
}
}

@Value
@EqualsAndHashCode(callSuper = false)
class FormalHello extends Recipe {
@Option(displayName = "Before name",
description = "The name of a person being greeted")
String beforeName;

@Option(displayName = "After name",
description = "The more formal name of the person.")
String afterName;

@Override
public String getDisplayName() {
return "Formal hello";
}

@Override
public String getDescription() {
return "Be formal. Be cool.";
}

@Override
public void buildRecipeList(RecipeList list) {
list
// TODO would these large option-set recipes
// benefit from builders?
.recipe(new FindAndReplace(
"hi", "hello", null, false, null,
null, null, null)
)
.recipe(
"Say my name, say my name",
"It's late and I'm making bad jokes.",
new PlainTextVisitor<>() {
@Override
public PlainText visitText(PlainText text, ExecutionContext ctx) {
return text.withText(text.getText().replace(beforeName, afterName));
}
}
);
}
}

0 comments on commit 7d056fd

Please sign in to comment.