Recipes
Encapsulates a collection of operations to accomplish some higher-level task like a framework migration.
A recipe represents a group of search and refactoring operations that can be applied to a Lossless Semantic Tree. A recipe can represent a single, stand-alone operation or it can be linked together with other recipes to accomplish a larger goal such as a framework migration.
OpenRewrite provides a managed environment for discovering, instantiating and configuring recipes. To implement a search or refactoring operation, a recipe delegates to a visitor which handles the LST traversal and manipulation.
An imperative recipe is built by extending the
org.openrewrite.Recipe
class and injecting configuration properties via its constructor.Let's look at a simple example where a Java recipe performs an operation to change all references of an original type to a new type:
public class ChangeType extends Recipe {
private final String oldFullyQualifiedTypeName;
private final String newFullyQualifiedTypeName;
// Recipe configuration is injected via the constructor
@JsonCreator
public ChangeType(
@JsonProperty("oldFullyQualifiedTypeName") String oldFullyQualifiedTypeName,
@JsonProperty("newFullQualifiedTypeName") String newFullQualifiedTypeName
) {
this.oldFullyQualifiedTypeName = oldFullyQualifiedTypeName;
this.newFullQualifiedTypeName = newFullQualifiedTypeName;
}
@Override
protected JavaVisitor<ExecutionContext> getVisitor() {
// Construct an instance of a visitor that will operate over the LSTs.
return new ChangeTypeVisitor(oldFullyQualifiedTypeName, newFullyQualifiedTypeName);
}
// In many cases, the visitor is implemented as a private, inner class. This
// ensures that the visitor is only used via its managed, configured recipe.
private class ChangeTypeVisitor extends JavaVisitor<ExecutionContext> {
...
}
}
This recipe accepts two configuration parameters via its constructor. Recipes may be linked together with many other recipes and run in a variety of orders, contexts, and number of cycles. Making your recipes immutable, as this one is, is a strongly recommended best practice. Any mutable state should be local to the visitor, a fresh instance of which should be returned from each invocation of
getVisitor()
.Consider this non-exhaustive list of steps required to migrate a project from JUnit 4 to JUnit 5:
- Replace the annotation
org.junit.Test
withorg.junit.jupiter.api.Test
- Change assertions from
org.junit.Assert
toorg.junit.jupiter.api.Assertions
, including updating the order of arguments within the method invocations - Remove public visibility from test classes and methods that no longer need to be
public
in JUnit 5 - Modify the Maven
pom.xml
to include dependencies on JUnit 5, and remove dependencies on JUnit 4
No one recipe should be responsible for implementing all of these different responsibilities. Instead, each responsibility is handled by its own recipe and those recipes are aggregated together into a single "Migrate JUnit 4 to 5" recipe. The migration recipe has no behavior of its own except to invoke each of the building blocks. Recipes add other recipes to the execution pipeline via the
doNext()
method.In our above example, the "Migrate to JUnit 5" recipe could look similar to the following:
package org.example.testing;
import org.openrewrite.java.ChangeType;
public class JUnit5Migration extends Recipe {