OpenRewrite Learning (Part 3): Recipes and Visitors

Addo Zhang
5 min readDec 28, 2024

--

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 the Accumulator 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:

  1. Validation: The recipe’s configuration is validated using the validate() method. If validation fails, the recipe is skipped.
  2. Visitor Execution: The source files are processed by the recipe’s associated visitors. This step is parallelized to speed up LST traversal and modifications.
  3. 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:

  1. Object Structure (LST): The data structure being processed.
  2. Client Application (Recipe): Initiates the operation.
  3. 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:

  1. Parent: Points to the parent node (root has no parent).
  2. Value: Represents the current LST node.
  3. 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.

--

--

Addo Zhang
Addo Zhang

Written by Addo Zhang

CNCF Ambassador | LF APAC OpenSource Evangelist | Microsoft MVP | SA and Evangelist at https://flomesh.io | Programmer | Blogger | Mazda Lover | Ex-BBer

No responses yet