// Use when creating/writing/building OpenRewrite recipes, working with .java recipe files, RewriteTest files, recipe YAML files, LST visitors, JavaTemplate, visitor patterns, or when discussing recipe types (declarative YAML, Refaster templates, imperative Java recipes) - guides creation of OpenRewrite recipes for automated code transformations, AST manipulation, custom refactoring rules, and code migration.
| name | writing-openrewrite-recipes |
| description | Use when creating/writing/building OpenRewrite recipes, working with .java recipe files, RewriteTest files, recipe YAML files, LST visitors, JavaTemplate, visitor patterns, or when discussing recipe types (declarative YAML, Refaster templates, imperative Java recipes) - guides creation of OpenRewrite recipes for automated code transformations, AST manipulation, custom refactoring rules, and code migration. |
| allowed-tools | Read, Write, Edit, Bash, Grep, Glob |
OpenRewrite recipes are automated refactoring operations that modify Lossless Semantic Trees (LSTs) representing source code. This skill guides through creating recipes efficiently and correctly.
Do NOT use this skill for:
To determine the best approach quickly:
Can the transformation be expressed by composing existing recipes? โ Use Declarative YAML (see Declarative YAML Recipes section below)
Is it a simple expression/statement replacement pattern? โ Use Refaster Template (see Refaster Template Recipes section below)
Requires complex logic, conditional transformations, or custom analysis? โ Use Imperative Java Recipe (see Imperative Recipe Development Workflow below)
For imperative recipes, proceed to "Imperative Recipe Development Workflow" below.
Choose the appropriate recipe type based on your needs:
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
Finding Recipes to Use:
When building declarative YAML recipes, consult the recipe catalog CSV files in the references/ directory:
references/recipes-top.csv - 50 commonly used recipes across all categories (best starting point)references/recipes-java-basic.csv - 32 basic Java refactoring operationsreferences/recipes-spring-boot-common.csv - 60 Spring Boot migrations and best practicesreferences/recipes-framework-migrations-common.csv - 16 major framework migrations (diverse frameworks)references/recipes-testing-common.csv - 60 most useful testing recipes (JUnit, Mockito, AssertJ)references/recipes-dependencies-common.csv - 49 dependency operations (Maven+Gradle when possible)references/recipes-security-common.csv - 30 security vulnerability detection and fixesreferences/recipes-xml-yaml-json-common.csv - 50 configuration file operationsreferences/recipes-static-analysis-common.csv - 50 code analysis and search recipesreferences/recipes-logging-common.csv - 50 logging framework operationsreferences/recipes-file-operations.csv - 14 file and text manipulation operationsUsage Pattern: Start with recipes-top.csv, then consult the specific category file based on what the user needs. These curated lists contain the most practical and commonly used recipes for each category.
Use when: Simple expression/statement replacements
Advantages: Faster than imperative, type-aware
Example use case: Replace StringUtils.equals() with Objects.equals()
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.
Navigate to the right example based on your needs:
references/example-say-hello-recipe.javareferences/example-scanning-recipe.javareferences/example-declarative-migration.yml@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:
@Value and @EqualsAndHashCode(callSuper = false) for immutability@Option fieldsgetVisitor() each time (no caching)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:
JavaIsoVisitor when returning the same type (most common)JavaVisitor only when changing LST typessuper.visitX() to traverse subtree in most cases. Omit the super call only when certain there could be no further edits below the current LST element.withX() methods for modificationsprivate 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:
#{} for string parameters#{any(Type)} for typed LST elements.imports("java.util.List").javaParser(JavaParser.fromJavaVersion().classpath("library-name")).contextSensitive() only when referencing local scope@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.and(
new UsesType<>("com.example.Type", true),
new UsesJavaVersion<>(17)
),
new YourVisitor()
);
}
Benefits: Limits recipe execution to relevant files only, improving performance
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:
//language=XXX comments 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 codeUse when you need to:
@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;
}
};
}
}
@Value and @EqualsAndHashCode(callSuper = false)getVisitor() must return NEW instance// WRONG
method.getArguments().remove(0);
// CORRECT
method.withArguments(ListUtils.map(method.getArguments(), (i, arg) ->
i == 0 ? null : arg
));
OldType to NewType."com.yourorg.VerbNoun (e.g., com.yourorg.ChangePackage)getCursor().putMessage())Map<JavaProject, T>maybeAddImport("java.util.List");
maybeAddImport("java.util.Collections", "emptyList");
maybeRemoveImport("old.package.Type");
doAfterVisit(new OtherRecipe().getVisitor());
if (methodInvocation.getType() != null &&
TypeUtils.isOfClassType(methodInvocation.getType(), "com.example.Type")) {
// ...
}
This skill includes several supporting files organized by purpose:
Templates (assets/) - Files used as starting points for recipe development:
assets/template-imperative-recipe.java - Boilerplate for imperative recipesassets/template-declarative-recipe.yml - YAML recipe templateassets/template-recipe-test.java - Test class templateLoad when: Creating a new recipe or needing a template to start from
Examples (references/) - Reference documentation loaded as needed:
references/example-say-hello-recipe.java - Complete working recipe with test and YAML usagereferences/example-scanning-recipe.java - Advanced ScanningRecipe pattern for multi-file analysisreferences/example-declarative-migration.yml - Real-world YAML migration examplesLoad when: Needing to see a complete example, asking "show me an example", or understanding advanced patterns
Recipe Catalogs (references/) - Curated lists for finding recipes when building declarative YAML recipes:
references/recipes-top.csv - 50 commonly used recipes (best starting point)references/recipes-java-basic.csv - 32 basic Java refactoring operationsreferences/recipes-spring-boot-common.csv - 60 Spring Boot migrations and best practicesreferences/recipes-framework-migrations-common.csv - 16 major framework migrations (10 different frameworks)references/recipes-testing-common.csv - 60 most useful testing recipesreferences/recipes-dependencies-common.csv - 49 dependency operations (Maven+Gradle when possible)references/recipes-security-common.csv - 30 security vulnerability recipesreferences/recipes-xml-yaml-json-common.csv - 50 configuration file operationsreferences/recipes-static-analysis-common.csv - 50 code analysis recipesreferences/recipes-logging.csv - 153 logging framework recipesreferences/recipes-file-operations.csv - 14 file manipulation recipesreferences/recipes-all.csv exists for maintenance/script purposes but is too large (4,958 recipes) to be used directlyLoad when: Looking for existing recipes
Checklist (references/) - Verification guide:
references/checklist-recipe-development.md - Comprehensive verification checklist covering planning, implementation, testing, and distributionLoad when: Reviewing a recipe for completeness, ensuring best practices, or preparing for distribution
Scripts (scripts/) - Utility scripts:
scripts/upload-skill.sh - Script to upload/update the skill via APILoad when: Managing the skill itself (meta-operation)
Key Classes:
Recipe - Base class for all recipesJavaIsoVisitor<ExecutionContext> - Most common visitorJavaTemplate - For generating code snippetsRewriteTest - Testing interfaceScanningRecipe<T> - Multi-file analysisKey Methods:
getVisitor() - Returns visitor instancesuper.visitX() - Traverse subtree.withX() - Create modified LST copyListUtils.map() - Transform lists without mutationdoAfterVisit() - Chain additional visitorsUse this skill for help with: