| name | openrewrite-recipe-writer |
| description | Expert guidance for writing OpenRewrite recipes - automated refactoring operations for source code. Helps choose recipe types, structure code, write tests, and follow best practices. |
OpenRewrite Recipe Writing Skill
Overview
OpenRewrite recipes are automated refactoring operations that modify Lossless Semantic Trees (LSTs) representing source code. This skill guides you through creating recipes efficiently and correctly.
Recipe Type Selection
Choose the appropriate recipe type based on your needs:
Declarative YAML Recipes (Preferred)
Use when: Composing existing recipes with configuration Advantages: No code, simple, maintainable Example use case: Combining framework migration steps
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.MyMigration
displayName: Migrate to Framework X
recipeList:
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: old.Type
newFullyQualifiedTypeName: new.Type
- com.yourorg.OtherRecipe
Refaster Template Recipes
Use when: Simple expression/statement replacements
Advantages: Faster than imperative, type-aware
Example use case: Replace StringUtils.equals() with Objects.equals()
Imperative Java Recipes
Use when: Complex logic, conditional transformations, custom analysis Advantages: Full control, complex transformations Example use case: Add modifiers only to variables that aren't reassigned
Decision Rule: If it can be declarative, make it declarative. Use imperative only when necessary.
Imperative Recipe Development Workflow
1. Set Up Recipe Class
@Value
@EqualsAndHashCode(callSuper = false)
public class YourRecipe extends Recipe {
@Option(displayName = "Display Name",
description = "Clear description.",
example = "com.example.Type")
String parameterName;
@Override
public String getDisplayName() {
return "Your recipe name";
}
@Override
public String getDescription() {
return "What this recipe does.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YourVisitor();
}
}
Key Points:
- Use
@Valueand@EqualsAndHashCode(callSuper = false)for immutability - All recipes must be serializable.
- Options define configurable parameters
getVisitor()returns a NEW instance each time (no caching)
2. Implement the Visitor
public class YourVisitor extends JavaIsoVisitor<ExecutionContext> {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
// ALWAYS call super to traverse the tree
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Check if change is needed (do no harm)
if (!shouldChange(cd)) {
return cd;
}
// Make changes using JavaTemplate or LST methods
cd = makeChanges(cd);
return cd;
}
}
Visitor Guidelines:
- Use
JavaIsoVisitorwhen returning same type (most common) - Use
JavaVisitoronly when changing LST types - Usually call
super.visitX()to traverse subtree. Only when the state of the recipe is such that you know there could be no further edits below the current LST element should you omit thesupercall. - Return unchanged LST if no change needed (referential equality check)
- LSTs are immutable. Use
.withX()methods.
3. Use JavaTemplate for Complex Changes
private final JavaTemplate template = JavaTemplate
.builder("public String hello() { return \"Hello from #{}!\"; }")
.build();
// In visitor method:
classDecl = template.apply(
new Cursor(getCursor(), classDecl.getBody()),
classDecl.getBody().getCoordinates().lastStatement(),
fullyQualifiedClassName
);
Template Tips:
- Use
#{}for string parameters - Use
#{any(Type)}for typed LST elements - Declare imports:
.imports("java.util.List") - Add classpath:
.javaParser(JavaParser.fromJavaVersion().classpath("library-name")) - Context-free templates (default) are faster
- Use
.contextSensitive()only when referencing local scope
4. Add Preconditions (Performance)
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.and(
new UsesType<>("com.example.Type", true),
new UsesJavaVersion<>(17)
),
new YourVisitor()
);
}
Benefits: Recipes only run on relevant files
Testing Recipes
Test Structure
class YourRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new YourRecipe("parameter-value"));
}
@Test
void makesExpectedChange() {
rewriteRun(
//language=java
java(
// Before
"""
package com.example;
class Before { }
""",
// After
"""
package com.example;
class After { }
"""
)
);
}
@Test
void doesNotChangeWhenNotNeeded() {
rewriteRun(
//language=java
java(
"""
package com.example;
class AlreadyCorrect { }
"""
// No second argument = no change expected
)
);
}
}
Notice how in Java template strings, the end """ delimiter is one indent to the right of the open delimiter. Java trims everything to the left of that same column.
Testing Best Practices:
- Test both changes AND no-changes cases
- Test edge cases
- Be aware that the test harness runs multiple cycles - ensures idempotence
- Always add
//language=XXXcomments to the highest level statement whose string arguments entirely consist of code snippets of that same language. This helps the IDE syntax highlight the test code.
ScanningRecipe Pattern
Use when you need to:
- See all files before making changes
- Generate new files based on analysis
- Share data across multiple files
@Value
@EqualsAndHashCode(callSuper = false)
public class YourScanningRecipe extends ScanningRecipe<YourAccumulator> {
public static class YourAccumulator {
Map<JavaProject, Boolean> projectData = new HashMap<>();
}
@Override
public YourAccumulator getInitialValue(ExecutionContext ctx) {
return new YourAccumulator();
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(YourAccumulator acc) {
return new JavaIsoVisitor<>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Collect data into accumulator
return cu;
}
};
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(YourAccumulator acc) {
return new JavaIsoVisitor<>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Use data from accumulator to make changes
return cu;
}
};
}
}
Critical Best Practices
Do No Harm
- If unsure whether a change is safe, DON'T make it
- Make minimal, least invasive changes
- Respect existing formatting
Immutability & Idempotence
- Recipes must be immutable (no mutable state)
- Same input → same output, always
- Use
@Valueand@EqualsAndHashCode(callSuper = false) getVisitor()must return NEW instance
Never Mutate LSTs
// WRONG
method.getArguments().remove(0);
// CORRECT
method.withArguments(ListUtils.map(method.getArguments(), (i, arg) ->
i == 0 ? null : arg
));
Naming Conventions
- Display names: Sentence case, code in backticks, end with period
- Example: "Change type from
OldTypetoNewType." - Recipe names:
com.yourorg.VerbNoun(e.g.,com.yourorg.ChangePackage)
State Management
- Within visitor: Use Cursor messaging (
getCursor().putMessage()) - Between visitors: Use ScanningRecipe accumulator
- Never use ExecutionContext for visitor state
Multi-Module Projects
- Track per-project data:
Map<JavaProject, T> - Don't assume single project per repository
Common Patterns
Adding Imports
maybeAddImport("java.util.List");
maybeAddImport("java.util.Collections", "emptyList");
Removing Imports
maybeRemoveImport("old.package.Type");
Chaining Visitors
doAfterVisit(new OtherRecipe().getVisitor());
Checking Types
if (methodInvocation.getType() != null &&
TypeUtils.isOfClassType(methodInvocation.getType(), "com.example.Type")) {
// ...
}
Resources
This skill includes several supporting files:
Templates:
template-imperative-recipe.java- Boilerplate for imperative recipestemplate-declarative-recipe.yml- YAML recipe templatetemplate-recipe-test.java- Test class template
Examples:
example-say-hello-recipe.java- Complete working recipeexample-scanning-recipe.java- Advanced ScanningRecipe patternexample-declarative-migration.yml- Real-world YAML examples
Checklist:
checklist-recipe-development.md- Comprehensive verification checklist
Quick Reference
Key Classes:
Recipe- Base class for all recipesJavaIsoVisitor<ExecutionContext>- Most common visitorJavaTemplate- For generating code snippetsRewriteTest- Testing interfaceScanningRecipe<T>- Multi-file analysis
Key Methods:
getVisitor()- Returns visitor instancesuper.visitX()- Traverse subtree.withX()- Create modified LST copyListUtils.map()- Transform lists without mutationdoAfterVisit()- Chain additional visitors
Ask me for help with:
- Choosing the right recipe type
- Structuring your recipe class
- Writing visitor logic
- Using JavaTemplate
- Writing tests
- Debugging common issues
- Understanding LST structure