OpenRewrite Learning (Part 3): Recipes and Visitors
This article explores Recipes and Visitors, two core concepts in OpenRewrite. These are discussed together because OpenRewrite recipes are implemented using the Visitor Pattern.
Visitor Pattern is a software design pattern that separates algorithms from the object structures on which they operate. This separation allows adding new operations to existing object structures without modifying them. It adheres to the open/closed principle in object-oriented programming and software engineering.
— Wikipedia
Recipes
A Recipe is a logical collection that performs search and refactoring tasks on Lossless Semantic Trees (LST). Recipes can represent independent, small-scale changes or combine with other recipes to achieve complex goals, such as framework migrations.
OpenRewrite provides a managed environment to discover, instantiate, and configure recipes. When performing search or refactoring, recipes delegate these tasks to Visitors, which handle the traversal and manipulation of the LST.
In the first article, we used the ChangeMethodName recipe to rename the hello
method to greeting
.
Scanning Recipes
A Scanning Recipe is a specialized type of recipe used when new source files need to be created or when all source files must be analyzed before making changes.
At the implementation level, a ScanningRecipe
extends Recipe
and introduces two key components:
- Accumulator: A data structure defined by the recipe to store runtime information.
- Scanner: A
Visitor
responsible for populating theAccumulator
with data.
Example: CreateEmptyJavaClass
The CreateEmptyJavaClass recipe creates an empty Java class (only the declaration, without a body). Before creating the class file, it scans to ensure no files with the same name already exist.
Execution Pipeline
The Execution Pipeline defines how recipes are applied to a set of source files and how code transformations are executed.
When a recipe’s run()
method is invoked with a set of source files, the transformation process begins. The pipeline executes the top-level recipe and all subsequent linked recipes.
Each recipe in the pipeline follows these steps:
- Validation: The recipe’s configuration is validated using the
validate()
method. If validation fails, the recipe is skipped. - Visitor Execution: The source files are processed by the recipe’s associated visitors. This step is parallelized to speed up LST traversal and modifications.
- Linked Recipes Execution: If the recipe links to additional recipes, they are executed in the same manner until all nested recipes are processed.
Execution Context
An Execution Context allows multiple recipes and visitors to share state during execution. When invoking a recipe’s run()
method, an execution context is required. If not explicitly provided, a default context is created internally.
Execution Cycles
In some cases, changes made by one recipe may trigger further changes by another recipe in the pipeline. The pipeline iterates until no further changes are introduced or the maximum cycle limit (default: 3) is reached.
For example:
- Recipe A formats whitespace.
- Recipe B adds new code.
After Recipe A adjusts the formatting, Recipe B inserts new code, which might require additional formatting by Recipe A. This iterative process ensures a fully consistent codebase.
Results
The pipeline produces a collection of Result
objects, each representing changes made to a specific source file.
MethodDescriptiongetBefore()
Returns the original SourceFile
, or null
if a new file.getAfter()
Returns the modified SourceFile
, or null
if deleted.getRecipesThatMadeChanges()
Lists the recipes responsible for the changes.diff()
/diff(Path)
Generates a Git-style diff (optionally with relative paths).
If both getBefore()
and getAfter()
are non-null but their paths differ, the file has been moved.
Visitors
In the Visitor Pattern, the primary roles are:
- Object Structure (LST): The data structure being processed.
- Client Application (Recipe): Initiates the operation.
- Visitor: Implements the logic for processing.
OpenRewrite uses visitElement
to traverse the LST in a depth-first manner. For example, when processing a Java class with multiple methods, the visitor fully processes each method’s tree before moving to the next.
Key Components
Tree
A Tree represents an LST node, such as J
(Java), Xml
, or Yaml
. Each tree node provides:
- Unique ID: Identifies the node.
- accept() Method: Used for visitor callbacks.
- print() Methods: Converts the tree back into readable source code.
- Metadata Markers: Contains LST metadata.
TreeVisitor
All visitors extend the TreeVisitor<T extends Tree, P>
class, which provides the polymorphic navigation and lifecycle for traversing LSTs.
T
: The LST type the visitor processes.P
: An optional shared context for passing data during traversal.
For example, in a Java source file, the LST root is J.CompilationUnit
. The visitor starts traversal from the root, processing package declarations, import statements, and class declarations.
Cursor
The Cursor tracks the visitor’s position in the LST. It contains:
- Parent: Points to the parent node (root has no parent).
- Value: Represents the current LST node.
- Message: A
Map
for storing data during traversal.
Cursors allow backtracking to the root or specific nodes. For example, when visiting a method declaration, you can trace back to its containing class or package.
Quick Start: Custom Recipe
Here’s a simple example of writing a custom recipe to add annotations to specific classes.
Recipe Code
@Value
public class AddAnnotation extends Recipe {
String annotationFullQualifiedName;
String classFullQualifiedName;
@Nullable Map<String, Object> attributes;
@Override
public String getDisplayName() {
return "Add an annotation";
} @Override
public String getDescription() {
return "Adds a specified annotation to a class.";
} @Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
if (!classDecl.getType().getFullyQualifiedName().equals(classFullQualifiedName)) return classDecl;
boolean hasAnnotation = classDecl.getLeadingAnnotations().stream()
.anyMatch(a -> a.getType().toString().equals(annotationFullQualifiedName));
if (!hasAnnotation) {
maybeAddImport(annotationFullQualifiedName);
classDecl = JavaTemplate.builder("@#{}")
.javaParser(JavaParser.fromJavaVersion())
.build()
.apply(getCursor(), classDecl.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));
}
return classDecl;
}
};
}
}
Unit Tests
class AddAnnotationTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new AddAnnotation("javax.annotation.Generated", "com.example.MyClass", null));
}
@Test
void addAnnotation() {
rewriteRun(java("""
package com.example;
class MyClass {}
""", """
package com.example;
@Generated class MyClass {}
"""));
}
}
Conclusion
This article provided an in-depth look at Recipes and Visitors, core elements of OpenRewrite. Recipes organize high-level refactoring logic, while visitors traverse and manipulate the LST. Together, they enable scalable, automated modifications of large codebases.
By walking through a custom recipe to add annotations to classes, this guide demonstrates how to leverage OpenRewrite for practical, domain-specific transformations.