// Expert in test-first development of production-quality OpenRewrite recipes for automated code refactoring. Automatically activates when working with OpenRewrite recipe files, Java/YAML files in `src/main/java/**/rewrite/**` directories, writing tests implementing `RewriteTest`, or when users ask about recipe development, writing recipes, creating migrations, LST manipulation, JavaTemplate usage, visitor patterns, preconditions, scanning recipes, YAML recipes, GitHub Actions transformations, Kubernetes manifest updates, or code migration strategies. Guides recipe type selection (declarative/Refaster/imperative), visitor implementation, and test-driven development workflows.
| name | recipe-writer |
| description | Expert in test-first development of production-quality OpenRewrite recipes for automated code refactoring. Automatically activates when working with OpenRewrite recipe files, Java/YAML files in `src/main/java/**/rewrite/**` directories, writing tests implementing `RewriteTest`, or when users ask about recipe development, writing recipes, creating migrations, LST manipulation, JavaTemplate usage, visitor patterns, preconditions, scanning recipes, YAML recipes, GitHub Actions transformations, Kubernetes manifest updates, or code migration strategies. Guides recipe type selection (declarative/Refaster/imperative), visitor implementation, and test-driven development workflows. |
Create production-quality OpenRewrite recipes using test-first development. This skill combines comprehensive coverage of all recipe types (declarative, Refaster, imperative) with deep domain expertise in Java and YAML transformations.
Core Principle: Write tests first (RED), implement minimally (GREEN), apply OpenRewrite idioms (REFACTOR).
Explicitly invoke this skill for:
Do NOT invoke this skill for:
Here are example requests that activate this skill:
Planning:
Java Implementation:
YAML Implementation:
Debugging:
Testing:
Choose the appropriate recipe type based on your needs.
Start here
├─ Can I compose existing recipes? ───────────────────┐
│ YES → Use Declarative YAML │
│ NO ↓ │
├─ Is it a simple expression/statement replacement? ───┤
│ YES → Use Refaster Template │
│ NO ↓ │
└─ Do I need custom logic or conditional changes? ─────┤
YES → Use Imperative Java Recipe │
│
Still unsure? → Start with declarative, fall back to ─────┘
imperative only when necessary
| Type | Speed | Complexity | Use Cases | Examples |
|---|---|---|---|---|
| Declarative YAML | Fastest | Lowest | Composing existing recipes | Framework migrations, standard refactorings |
| Refaster Template | Fast | Low-Medium | Expression/statement replacements | API updates, method call changes |
| Imperative Java | Slower | High | Complex transformations, conditional logic | Custom analysis, YAML LST manipulation |
Use when: Composing existing recipes with configuration
Advantages:
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
Common declarative recipes:
ChangeType, ChangeMethodName, AddDependency, UpgradeDependencyVersionFindKey, FindValue, ChangeKey, ChangeValue, DeleteKey, MergeYaml, CopyValueUse when: Simple expression/statement replacements with type awareness
Advantages:
Example use case: Replace StringUtils.equals() with Objects.equals()
public class StringUtilsToObjects {
@BeforeTemplate
boolean before(String s1, String s2) {
return StringUtils.equals(s1, s2);
}
@AfterTemplate
boolean after(String s1, String s2) {
return Objects.equals(s1, s2);
}
}
Use when: Complex logic, conditional transformations, custom analysis, or YAML/LST manipulation
Advantages:
Example use case:
Decision Rule: If it can be declarative, make it declarative. Use Refaster for simple replacements. Use imperative only when necessary.
Follow the RED-GREEN-REFACTOR cycle for recipe development:
Phase 1: RED (Write Failing Tests)
↓
Phase 2: DECIDE (Select Recipe Type)
↓
Phase 3: GREEN (Minimal Implementation)
↓
Phase 4: REFACTOR (Apply OpenRewrite Idioms)
↓
Phase 5: DOCUMENT (Add Metadata & Examples)
↓
Phase 6: VALIDATE (Production Readiness)
Start with tests before writing any recipe code. This ensures you understand the transformation and can verify correctness.
For Java recipes:
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
)
);
}
}
For YAML recipes:
class YourYamlRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new YourYamlRecipe());
}
@Test
void updatesGitHubActionsCheckout() {
rewriteRun(
//language=yaml
yaml(
"""
jobs:
build:
steps:
- uses: actions/checkout@v2
""",
"""
jobs:
build:
steps:
- uses: actions/checkout@v4
"""
)
);
}
}
Test Checklist:
Key Principle: Start with simplest possible before/after. Add complexity incrementally.
Use the decision tree above to choose between declarative, Refaster, or imperative.
Ask yourself:
For YAML-specific decisions:
ChangeValue, ChangeKey)Implement just enough to make tests pass. Don't optimize or refactor yet.
For declarative recipes:
src/main/resources/META-INF/rewrite/For imperative Java recipes:
Use templates from ./templates/ directory:
template-imperative-recipe.java - Complete recipe structuretemplate-recipe-test.java - Test structureAutomation: Use ./scripts/init_recipe.py <RecipeName> to generate boilerplate.
For YAML recipes, extend YamlIsoVisitor:
public class YourYamlRecipe extends Recipe {
@Override
public String getDisplayName() {
return "Your recipe display name";
}
@Override
public String getDescription() {
return "Description of transformation.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
entry = super.visitMappingEntry(entry, ctx);
// Match specific key
if ("targetKey".equals(entry.getKey().getValue())) {
// Safe value access
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
if ("oldValue".equals(scalar.getValue())) {
return entry.withValue(
scalar.withValue("newValue")
);
}
}
}
return entry;
}
};
}
}
Verification: Run tests to achieve GREEN state - all tests must pass.
Now improve the recipe using OpenRewrite best practices. Don't skip GREEN to do this - refactoring comes AFTER tests pass.
Refactoring Checklist:
1. Trait Usage (Advanced)
See ./references/trait-implementation-guide.md for details.
2. Recipe Composition
3. OpenRewrite Conventions
displayName and description (both support markdown)@Option annotations with descriptions and examplesnull values and missing elements@Value and @EqualsAndHashCode(callSuper = false) for immutabilitygetVisitor() returns NEW instance (never cached)4. Performance Considerations
5. YAML-Specific Refactoring
Verification:
Add comprehensive documentation to make the recipe discoverable and understandable.
Recipe Metadata (supports markdown):
@Override
public String getDisplayName() {
return "Update GitHub Actions to `actions/checkout@v4`.";
}
@Override
public String getDescription() {
return "Updates all uses of `actions/checkout@v2` and `actions/checkout@v3` to `actions/checkout@v4`.\n\n" +
"**Before:**\n```yaml\n- uses: actions/checkout@v2\n```\n\n" +
"**After:**\n```yaml\n- uses: actions/checkout@v4\n```";
}
Option Documentation:
@Option(
displayName = "Old action reference",
description = "The old action reference to replace (e.g., `actions/checkout@v2`).",
example = "actions/checkout@v2"
)
String oldActionRef;
Javadoc:
Naming Conventions:
com.yourorg.VerbNoun (e.g., com.yourorg.UpdateGitHubActions)Use the comprehensive checklist to ensure production quality.
Quick Validation:
Full Validation:
See ./references/checklist-recipe-development.md for 200+ validation items.
License Headers:
Check for {repository_root}/gradle/licenseHeader.txt. If exists, use ./scripts/add_license_header.sh to add headers.
Quick reference for common implementation patterns.
Set Up Recipe Class:
@Value
@EqualsAndHashCode(callSuper = false)
public class YourRecipe extends Recipe {
@Option(displayName = "Parameter Name",
description = "Clear description.",
example = "com.example.Type")
String parameterName;
@Override
public String getDisplayName() {
return "Your recipe display name.";
}
@Override
public String getDescription() {
return "What this recipe does.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YourVisitor();
}
}
Implement 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;
}
}
Use JavaTemplate:
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
);
Add Preconditions:
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.and(
new UsesType<>("com.example.Type", true),
new UsesJavaVersion<>(17)
),
new YourVisitor()
);
}
Basic YAML Visitor:
public class YourYamlRecipe extends Recipe {
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
entry = super.visitMappingEntry(entry, ctx);
// Match key
if ("targetKey".equals(entry.getKey().getValue())) {
// Safe value access
String value = entry.getValue() instanceof Yaml.Scalar ?
((Yaml.Scalar) entry.getValue()).getValue() : null;
if ("oldValue".equals(value)) {
return entry.withValue(
((Yaml.Scalar) entry.getValue()).withValue("newValue")
);
}
}
return entry;
}
};
}
}
JsonPath Matching:
public class GitHubActionsRecipe extends Recipe {
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
private final JsonPathMatcher matcher =
new JsonPathMatcher("$.jobs.*.steps[*].uses");
@Override
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
scalar = super.visitScalar(scalar, ctx);
if (matcher.matches(getCursor())) {
String value = scalar.getValue();
if (value != null && value.startsWith("actions/checkout@v2")) {
return scalar.withValue(value.replace("@v2", "@v4"));
}
}
return scalar;
}
};
}
}
Common JsonPath Patterns:
See ./references/jsonpath-patterns.md for comprehensive patterns including:
$.jobs.*.steps[*].uses, $.on.push.branches$.spec.template.spec.containers[*].image, $.metadata.labels$.databases.*.connection.host, $[?(@.enabled == true)]Use when you need to see all files before making changes, generate new files, or share data across files.
@Value
@EqualsAndHashCode(callSuper = false)
public class YourScanningRecipe extends ScanningRecipe<YourAccumulator> {
public static class YourAccumulator {
Map<SourceFile, Boolean> fileData = new HashMap<>();
}
@Override
public YourAccumulator getInitialValue(ExecutionContext ctx) {
return new YourAccumulator();
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(YourAccumulator acc) {
return new TreeVisitor<Tree, ExecutionContext>() {
@Override
public Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
// Collect data into accumulator
return tree;
}
};
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(YourAccumulator acc) {
return new TreeVisitor<Tree, ExecutionContext>() {
@Override
public Tree visit(@Nullable Tree tree, ExecutionContext ctx) {
// Use data from accumulator to make changes
return tree;
}
};
}
}
For complete example, see ./examples/example-scanning-recipe.java.
Use the RewriteTest interface for all recipe tests.
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
)
);
}
}
//language=XXX comments - Helps IDE syntax highlight test code""" delimiter one indent to right of open delimiterMulti-document YAML:
@Test
void handlesMultiDocumentYaml() {
rewriteRun(
//language=yaml
yaml(
"""
---
first: document
---
second: document
""",
"""
---
first: updated
---
second: updated
"""
)
);
}
Null value handling:
@Test
void handlesNullValues() {
rewriteRun(
//language=yaml
yaml(
"""
key: null
another:
"""
// Should not crash or change
)
);
}
Comment preservation:
@Test
void preservesComments() {
rewriteRun(
//language=yaml
yaml(
"""
# Important comment
key: oldValue
""",
"""
# Important comment
key: newValue
"""
)
);
}
For more testing patterns, see ./references/testing-patterns.md.
Traits provide semantic abstractions over LST elements, wrapping them with domain-specific logic.
When to use traits:
Basic trait structure:
@Value
public class YourTrait implements Trait<J.ClassDeclaration> {
Cursor cursor;
// Domain-specific accessor
public String getClassName() {
return getTree().getSimpleName();
}
// Nested Matcher class
public static class Matcher extends SimpleTraitMatcher<J.ClassDeclaration> {
@Override
protected @Nullable YourTrait test(Cursor cursor) {
J.ClassDeclaration cd = cursor.getValue();
// Custom matching logic
if (matchesCondition(cd)) {
return new YourTrait(cursor);
}
return null;
}
}
}
Using traits in recipes:
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YourTrait.Matcher().asVisitor((trait, ctx) -> {
String className = trait.getClassName();
// Use semantic API instead of raw LST navigation
return trait.getTree();
});
}
IMPORTANT: Never use deprecated Traits utility classes. Always instantiate matchers directly:
// ❌ Old (deprecated):
Traits.literal()
// ✅ New (preferred):
new Literal.Matcher()
For complete trait implementation guide, see ./references/trait-implementation-guide.md.
Preconditions filter files before running the recipe, improving performance.
Common preconditions:
// Only run on files using specific type
new UsesType<>("com.example.Type", true)
// Only run on files with specific method
new UsesMethod<>("com.example.Type methodName(..)")
// Only run on specific Java version
new UsesJavaVersion<>(17)
// Only run on YAML files
new FindSourceFiles("**/*.yml", "**/*.yaml")
Combining preconditions:
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.and(
new UsesType<>("com.example.Type", true),
new UsesJavaVersion<>(11)
),
new YourVisitor()
);
}
JavaTemplate compiles code snippets once and applies them to LST elements.
When to use:
When NOT to use:
.withX() methods)Template syntax:
// Untyped substitution (strings)
JavaTemplate.builder("System.out.println(#{})")
.build();
// Typed substitution (LST elements)
JavaTemplate.builder("return #{any(java.lang.String)}")
.build();
// With imports
JavaTemplate.builder("List<String> list = new ArrayList<>()")
.imports("java.util.List", "java.util.ArrayList")
.build();
// With classpath
JavaTemplate.builder("@Deprecated(since = \"2.0\")")
.javaParser(JavaParser.fromJavaVersion().classpath("library-name"))
.build();
// Context-sensitive (references local scope)
JavaTemplate.builder("localVariable.method()")
.contextSensitive()
.build();
Applying templates:
// Apply to statement position
method.withBody(
template.apply(
new Cursor(getCursor(), method.getBody()),
method.getBody().getCoordinates().lastStatement(),
args
)
);
Tips:
.contextSensitive() only when referencing local variables/methodsWithin visitor (intra-visitor state):
// Store state
getCursor().putMessage("key", value);
// Retrieve state from cursor hierarchy
Object value = getCursor().getNearestMessage("key");
Between visitors (ScanningRecipe):
Use accumulator pattern - see ScanningRecipe section above.
Never:
Track data per-project, not globally:
public static class Accumulator {
// ✅ Per-project tracking
Map<SourceFile, Boolean> fileData = new HashMap<>();
// ❌ Don't assume single project
// boolean globalFlag;
}
Common issues and solutions when developing recipes.
Symptoms: Recipe doesn't execute on files you expect to change
Solutions:
.yml vs .yaml)Symptoms: Template fails to compile or apply
Solutions:
.imports().javaParser().contextSensitive() if referencing local scope#{} for strings#{any(Type)} for LST elementsSymptoms: RewriteTest passes but recipe fails on actual projects
Solutions:
Symptoms: Running recipe multiple times produces different results each time
Solutions:
getVisitor() returns a NEW instance each time@Value)rewriteRun() which automatically runs multiple cyclesSymptoms: TypeUtils.isOfClassType() returns false when it should be true
Solutions:
null types)Symptoms: YAML recipe doesn't find or transform expected YAML elements
Solutions:
key: null and key:---)super.visitX() to traverse treeSymptoms: Recipe changes YAML but loses comments or formatting
Solutions:
.withX() methodsListUtils for list operations, never mutate directlyFor more troubleshooting guidance, see ./references/troubleshooting-guide.md.
@Value and @EqualsAndHashCode(callSuper = false)getVisitor() must return NEW instance each time// 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)This skill uses progressive disclosure to minimize token usage. Load resources on demand:
templates/template-imperative-recipe.java - Complete recipe class structuretemplates/template-declarative-recipe.yml - YAML recipe formattemplates/template-refaster-template.java - Refaster template structuretemplates/template-recipe-test.java - Test class using RewriteTesttemplates/license-header.txt - Standard license headerexamples/example-say-hello-recipe.java - Simple recipe with JavaTemplateexamples/example-scanning-recipe.java - Multi-file analysis patternexamples/example-yaml-github-actions.java - YAML domain exampleexamples/example-declarative-migration.yml - Framework migrationreferences/java-lst-reference.md - Java LST structure and hierarchyreferences/yaml-lst-reference.md - YAML LST structure and hierarchyreferences/jsonpath-patterns.md - Domain-specific JsonPath patternsreferences/trait-implementation-guide.md - Advanced trait patternsreferences/checklist-recipe-development.md - 200+ validation itemsreferences/common-patterns.md - Copy-paste code snippetsreferences/testing-patterns.md - Test patterns and edge casesreferences/troubleshooting-guide.md - Issue diagnosis and solutions| Resource Type | Typical Size | When to Load |
|---|---|---|
| SKILL.md | ~3,500 tokens | Always (auto) |
| Templates | ~500 tokens each | On demand (Read tool) |
| Examples | ~1,000 tokens each | On demand (Read tool) |
| References | ~1,500-3,000 tokens each | On demand (Read tool) |
Best practice: Only read templates/examples when actively working on implementation. The SKILL.md content provides sufficient guidance for planning and decision-making.
| Class | Purpose |
|---|---|
Recipe | Base class for all recipes |
JavaIsoVisitor<ExecutionContext> | Most common Java visitor type |
YamlIsoVisitor<ExecutionContext> | Most common YAML visitor type |
JavaTemplate | Generate Java code snippets |
RewriteTest | Testing interface |
ScanningRecipe<T> | Multi-file analysis pattern |
JsonPathMatcher | Match YAML/JSON paths |
| Method | Purpose |
|---|---|
getVisitor() | Returns visitor instance (must be NEW) |
super.visitX() | Traverse subtree |
.withX() | Create modified LST copy (immutable) |
ListUtils.map() | Transform lists without mutation |
doAfterVisit() | Chain additional visitors |
maybeAddImport() | Add import if not present |
maybeRemoveImport() | Remove import if unused |
getCursor().putMessage() | Store intra-visitor state |
For quick reference on frequently used patterns, see:
references/common-patterns.md - Import management, visitor chaining, type checkingreferences/jsonpath-patterns.md - GitHub Actions, Kubernetes, CI/CD patternsUse helper scripts for common tasks:
./scripts/init_recipe.py <RecipeName> - Generate recipe boilerplate (class, test file, optional YAML)./scripts/validate_recipe.py [path] - Validate recipe structure, naming, Java compatibility./scripts/add_license_header.sh [file] - Add license headers from gradle/licenseHeader.txtThis skill is optimized for token efficiency:
Strategy: Start with SKILL.md guidance. Load templates for boilerplate. Load references for troubleshooting or advanced features.