Module 4: Imperative recipes
For use cases beyond what declarative recipes and Refaster templates can handle, you will want to look at writing a Java refactoring recipe.
Imperative recipes use visitors to traverse Lossless Semantic Trees (LSTs) and modify code with full type awareness. You will use JavaTemplate to create new LST elements that replace existing nodes while preserving formatting and style.
As you've already seen, testing is a critical feedback loop for enabling recipe development. The RewriteTest framework validates both the textual output and the underlying LST types and structure, so it will surface type-attribution problems early. This module starts by using examples from the starter project to show how to use tests effectively and how recipe and source inputs can be specified in different ways. After that, you will implement an imperative recipe and use tests to guide each change and confirm that traversal only touches the nodes you intend.
If you get stuck, you can reference the workshop-solutions branch of the starter repo for completed examples (and you’ll also see code embedded inline throughout the steps).
The following concepts in the OpenRewrite documentation are important to understand, particularly when writing imperative OpenRewrite recipes:
These concepts should give you some sense as to the importance of exact type attribution, and how visitors are used to traverse and modify the LST. Without these, it would be next to impossible to write recipes that make changes to your code reliably.
Exercise 4-1: Testing imperative recipes
Module 2 introduced RewriteTest when you added and ran unit tests for a declarative recipe. This exercise expands on those patterns by walking through additional test patterns to use for imperative recipes.
Goals for this exercise
- Understand how to write unit tests for your recipes.
- Learn how to assert the state of the LST before and after running a recipe.
- Explore the various ways to provide recipe and source specifications.
Steps
- Open src/test/java/com/yourorg/AppendToReleaseNotesTest.java. There are some important things to note in this file to help you understand more about the OpenRewrite testing framework:
- The
AppendToReleaseNotesrecipe is directly constructed and passed todefaults(RecipeSpec). (This is different from Module 2, where you usedrecipeFromResources(...)to load a YAML recipe from resources instead.) - These tests use
SourceSpecs.text(...)instead ofjavabecause the file to test is a Markdown file (RELEASE.md), not a Java source file. (And no//language=directive is needed in this case.) - There is an optional
spec -> ...parameter that sets the path (spec.path(Path.of("RELEASE.md"))), so the test asserts creation or edits at that location. - In
createNewReleaseNotes(), the "before" source usesdoesNotExist()to indicate that there is no file to start, meaning that the "after" block indicates that the file should be created with the provided source.
- The
Use SourceSpecs.text(...) for plain text sources; java(...) is only for Java inputs.
- Add a new test to make sure no notes are added that are already there.
- Add a case that asserts: if
RELEASE.mdalready contains the message, the recipe should not append it again. - Mirror the existing test structure, but provide only a "before" source to assert no change is expected.
- Add a case that asserts: if
Reference example: noDuplicateNotes test
@Test
void noDuplicateNotes() {
rewriteRun(
text(
"""
You say goodbye, I say
Hello world
""",
spec -> spec.path(Path.of("RELEASE.md"))
)
);
}
- Open src/test/java/com/yourorg/AssertEqualsToAssertThatTest.java. The thing to note in this file:
- In the recipe specification (
defaults(RecipeSpec)), the call to explicitly configure the parser (.parser(JavaParser.fromJavaVersion().classpath("junit-jupiter-api"))) allows a classpath to be passed in to specify additional packages needed to supply any types used by the recipe.
- In the recipe specification (
- Comment out only
.classpath("junit-jupiter-api")and run the test to observe the type-attribution failure.- The resulting
java.lang.IllegalStateException: LST contains missing or invalid type informationindicates that the type information is missing, and that the test classpath is likely not correctly set up.
- The resulting
- Restore the classpath entry and re-run to confirm the failure is resolved.
Takeaways
RewriteTestsupports multiple styles of recipe specification and source specification.- Classpath configuration is required to keep type information intact.
- Tests should cover both transformation cases and no-change cases.
Exercise 4-2: Write an imperative recipe
In this exercise, you'll use the existing NoGuavaListsNewArrayList recipe as a reference while writing your own imperative recipe.
Goals for this exercise
- Build new code elements (method calls) that replace existing ones.
- Practice implementing visitor logic with
JavaTemplate. - Use tests to validate traversal and type checks.
Steps
Step 1: Use a reference recipe to orient your implementation
- Open src/main/java/com/yourorg/NoGuavaListsNewArrayList.java and read through the visitor methods. There are some important things to note in this file that can help as a reference in writing your own imperative recipe:
- It matches three variants of Guava's
Lists.newArrayList()and uses threeJavaTemplates to replace them withnew ArrayList<>(..). - In
visitMethodInvocation, there are preconditions added that limit which source files are visited. Also,CursorandJavaCoordinatesare used when applyingJavaTemplateto ensure that the changes are made in the correct location. Also, notice how the type parameters passed in to theJavaTemplatematch the arguments passed intoapply. - The calls to
maybeAddImport/maybeRemoveImportensure imports are correctly updated if the first or last LST element using the import is added/removed. - The return value of
visitMethodInvocationis theJavaTemplateresult. When nothing matches,super.visitMethodInvocationis still called to keep traversing.
- It matches three variants of Guava's
In visitCompilationUnit, the call to super.visitCompilationUnit is required to traverse the tree. You can click through to see how the traversal works, and try commenting out that return statement (using return cu; instead) to see how the recipe stops making changes.
The return value of visitMethodInvocation controls traversal: when nothing matches, you still call super.visitMethodInvocation to keep walking the tree.
- Open src/test/java/com/yourorg/NoGuavaListsNewArrayListTest.java and notice the
RewriteTeststructure and how@Test void noChangeNecessary()guards against unconditional changes.
Step 2: Enable the tests for your recipe
- Open src/test/java/com/yourorg/UseIntegerValueOfTest.java. Read the tests to see the cases you need to cover.
- Remove
@Disabledand run the tests (they should fail and describe the expected cases).
Step 3: Implement and iterate
- Open src/main/java/com/yourorg/UseIntegerValueOf.java.
- Using the requirements from the tests you just reviewed, write an imperative recipe that uses
JavaTemplateto match theIntegerboxing constructors and replace them with the correct method calls. Here are some hints to get you started:- Start by setting the
displayNameanddescriptionfields and adding the@Getterannotation. - Implement
getVisitor()and add the transformation logic. - Consider a precondition so the recipe only visits files using the constructor.
- Choose
JavaVisitororJavaIsoVisitorbased on the nodes you need. - Decide what method to overwrite depending on what type of LST elements you need to visit. (Don't forget the call to the superclass version of the method as in Exercise 4-1.)
- Use
JavaTemplate.builder(...).build().apply(...)to make the necessary changes and return the result.
- Start by setting the
Use JavaIsoVisitor when you want to return the same LST node type (isomorphic), and JavaVisitor if you need to return a different J node type (non-isomorphic).
- Build the project and run the tests. Use failures to refine your visitor logic until all tests pass.
Reference example: UseIntegerValueOf.java
package com.yourorg;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Preconditions;
import lombok.Getter;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.JavaVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.TypeUtils;
public class UseIntegerValueOf extends Recipe {
private static final MethodMatcher INTEGER_CONSTRUCTOR = new MethodMatcher("java.lang.Integer#<init>(*)");
@Getter
final String displayName = "Use `Integer.valueOf(x)` or `Integer.parseInt(x)` instead of `new Integer(x)`";
@Getter
final String description = "Replaces unnecessary boxing constructor calls with the more efficient Integer.valueOf(x) for int values, or Integer.parseInt(x) for String values.";
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
new UsesMethod<>(INTEGER_CONSTRUCTOR),
new JavaVisitor<ExecutionContext>() {
@Override
public J visitNewClass(J.NewClass newClass, ExecutionContext ctx) {
J update = super.visitNewClass(newClass, ctx);
if (!(update instanceof J.NewClass)) {
return update;
}
J.NewClass nc = (J.NewClass) update;
if (!INTEGER_CONSTRUCTOR.matches(nc)) {
return nc;
}
Expression arg = nc.getArguments().get(0);
if (TypeUtils.isString(arg.getType())) {
return JavaTemplate.builder("Integer.parseInt(#{any(java.lang.String)})")
.build()
.apply(getCursor(), nc.getCoordinates().replace(), arg);
}
return JavaTemplate.builder("Integer.valueOf(#{any()})")
.build()
.apply(getCursor(), nc.getCoordinates().replace(), arg);
}
}
);
}
}
This recipe is useful as an example for the purposes of this workshop, but it actually could more easily have been written as a Refaster recipe rather than as an imperative one. As a followup exercise, see if you can implement the same change using Refaster templates.
Takeaways
- Imperative recipes give you fine-grained control over how and when transformations are applied.
- Visitors and templates let you traverse LSTs and build replacements while preserving formatting.
- Type information and tests help keep changes correct and scoped to the right cases.
Learn More
Now that you've learned all the basics of building recipes, you may:
- Review conventions and best practices for writing OpenRewrite recipes.
- See how you can contribute to the OpenRewrite community.
- Learn about some of the more complex and powerful features of OpenRewrite in the advanced workshop.