recipe-writer
npx skills add https://github.com/sjungling/claude-plugins --skill recipe-writer
Agent 安装分布
Skill 文档
OpenRewrite Recipe Writing Expert
Overview
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).
When to Use This Skill
Explicitly invoke this skill for:
- Planning recipes – Determining the best recipe type for a use case
- Implementing recipes – Writing recipe classes, visitors, and JavaTemplate code
- Writing tests – Creating comprehensive test coverage with RewriteTest
- YAML transformations – GitHub Actions, Kubernetes manifests, CI/CD configs
- Java refactoring – Code migrations, API updates, framework modernization
- Debugging recipes – Troubleshooting visitor behavior, type checking, or preconditions
- Converting recipe types – Analyzing if an imperative recipe can be declarative
- Understanding OpenRewrite concepts – Learning about LSTs, cursors, traits, or scanning patterns
When NOT to Use This Skill
Do NOT invoke this skill for:
- General Java programming questions – Use standard Java knowledge unless specifically about OpenRewrite LST manipulation
- â “How do I parse JSON in Java?”
- â “How do I parse Java code into LSTs?”
- General YAML editing – Use standard Edit tools for direct file modifications
- â “Edit this YAML file to change the value”
- â “Create a recipe to update GitHub Actions across all repositories”
- Running OpenRewrite recipes – This skill is for authoring recipes, not executing them
- â “How do I run the Maven plugin?”
- â “How do I test my recipe runs correctly?”
- Build tool configuration – Unless directly related to recipe publishing/distribution
- â “How do I configure Gradle for my project?”
- â “How do I publish my recipe to Maven Central?”
- General refactoring questions – Only use for OpenRewrite recipe implementation
- â “What’s the best way to refactor this code?”
- â “What recipe type should I use for this refactoring?”
- Reading/understanding existing code – Unless analyzing recipe implementation
- â “Explain what this Spring controller does”
- â “Explain what this JavaIsoVisitor is doing”
Quick Examples
Here are example requests that activate this skill:
Planning:
- “I need to migrate from JUnit 4 to JUnit 5 – help me plan the recipe”
- “What’s the best recipe type to replace all ArrayList with List?”
- “Should I use a declarative or imperative recipe for adding annotations?”
- “Help me create a recipe to update GitHub Actions to use Node 20”
Java Implementation:
- “Write a recipe that adds @Deprecated to classes in com.example.old package”
- “Show me how to use JavaTemplate to add a method to a class”
- “Create a recipe that changes method return types from Optional to nullable”
YAML Implementation:
- “Create a recipe to update all GitHub Actions checkout actions to v4”
- “Write a recipe to change Kubernetes image tags across all manifests”
- “Build a recipe to migrate Travis CI configs to GitHub Actions”
Debugging:
- “My recipe isn’t matching the expected classes – help debug”
- “Why is my JavaTemplate throwing a parse error?”
- “The recipe runs but doesn’t make any changes – what’s wrong?”
- “My YAML recipe isn’t preserving comments – how do I fix it?”
Testing:
- “Write tests for a ScanningRecipe that analyzes multiple files”
- “How do I test a recipe that requires external classpath dependencies?”
- “Show me how to test edge cases in my YAML recipe”
Recipe Type Selection
Choose the appropriate recipe type based on your needs.
Decision Tree
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
Recipe Type Comparison
| 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 |
Declarative YAML Recipes (Preferred)
Use when: Composing existing recipes with configuration
Advantages:
- No code required
- Simple and maintainable
- Fast execution
- Easy to understand
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:
- Java:
ChangeType,ChangeMethodName,AddDependency,UpgradeDependencyVersion - YAML:
FindKey,FindValue,ChangeKey,ChangeValue,DeleteKey,MergeYaml,CopyValue
Refaster Template Recipes
Use when: Simple expression/statement replacements with type awareness
Advantages:
- Faster than imperative recipes
- Type-aware matching
- Concise syntax
- Good for API migrations
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);
}
}
Imperative Java Recipes
Use when: Complex logic, conditional transformations, custom analysis, or YAML/LST manipulation
Advantages:
- Full control over transformation logic
- Complex transformations possible
- Access to full LST structure
- Can implement custom matching
Example use case:
- Add modifiers only to variables that aren’t reassigned
- Transform YAML based on complex conditions
- Generate new files based on analysis
- Multi-file coordination with ScanningRecipe
Decision Rule: If it can be declarative, make it declarative. Use Refaster for simple replacements. Use imperative only when necessary.
Test-First Development Workflow
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)
Phase 1: RED – Write Failing Tests
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:
- Write happy path test (simplest transformation)
- Include edge cases (nulls, empty files, missing elements)
- Test no-op scenarios (recipe shouldn’t change unrelated code)
- Test multi-document YAML if relevant
- Include real-world examples if domain is known
- Run tests to confirm RED state – tests must fail initially
Key Principle: Start with simplest possible before/after. Add complexity incrementally.
Phase 2: DECIDE – Select Recipe Type
Use the decision tree above to choose between declarative, Refaster, or imperative.
Ask yourself:
- Can this be done by composing existing recipes? â Declarative
- Is it a simple expression/statement replacement? â Refaster
- Does it need custom logic or LST manipulation? â Imperative
For YAML-specific decisions:
- Simple value changes, key renames â Declarative (use
ChangeValue,ChangeKey) - Complex JsonPath matching with conditions â Imperative
- Multi-step YAML transformations â Declarative (compose multiple recipes)
- Dynamic YAML generation â Imperative
Phase 3: GREEN – Minimal Implementation
Implement just enough to make tests pass. Don’t optimize or refactor yet.
For declarative recipes:
- Create YAML file in
src/main/resources/META-INF/rewrite/ - Compose existing recipes
- Run tests to verify GREEN state
For imperative Java recipes:
Use templates from ./templates/ directory:
template-imperative-recipe.java– Complete recipe structuretemplate-recipe-test.java– Test structure
Automation: 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.
Phase 4: REFACTOR – Apply OpenRewrite Idioms
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)
- Can this recipe implement an existing trait?
- Should a new trait be created for reusable matching logic?
- Separate “what to find” (trait) from “what to do” (recipe)
See ./references/trait-implementation-guide.md for details.
2. Recipe Composition
- Can parts be extracted into smaller, composable recipes?
- Are there opportunities for configurability (parameters)?
- Could this be split into search recipe + modification recipe?
3. OpenRewrite Conventions
- Recipe has clear
displayNameanddescription(both support markdown) - Parameters use
@Optionannotations with descriptions and examples - Properly handles
nullvalues and missing elements - Preserves formatting and comments where possible
- Uses
@Valueand@EqualsAndHashCode(callSuper = false)for immutability -
getVisitor()returns NEW instance (never cached)
4. Performance Considerations
- Minimize LST traversals (don’t visit more than necessary)
- Use preconditions to skip files that won’t match
- Return original object if no changes made (identity check)
5. YAML-Specific Refactoring
- Use JsonPath matching for complex patterns
- Handle multi-document YAML if relevant
- Preserve YAML anchors and aliases
- Test with real-world files (GitHub Actions, K8s, etc.)
Verification:
- All tests still pass after refactoring
- Recipe follows OpenRewrite naming conventions
- Code is cleaner and more maintainable
Phase 5: DOCUMENT – Add Metadata & Examples
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:
- Add class-level Javadoc with use cases
- Show before/after transformations
- Document parameter effects
- Link to related recipes
Naming Conventions:
- Display names: Sentence case, code in backticks, end with period
- Recipe names:
com.yourorg.VerbNoun(e.g.,com.yourorg.UpdateGitHubActions)
Phase 6: VALIDATE – Production Readiness
Use the comprehensive checklist to ensure production quality.
Quick Validation:
- All tests pass (GREEN state maintained)
- Recipe handles edge cases gracefully (no NPEs)
- Formatting/comments preserved in output
- Documentation is clear and includes examples
- Recipe is idempotent (same result on repeated runs)
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.
Implementation Patterns
Quick reference for common implementation patterns.
Java Recipes
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()
);
}
YAML Recipes
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:
- GitHub Actions:
$.jobs.*.steps[*].uses,$.on.push.branches - Kubernetes:
$.spec.template.spec.containers[*].image,$.metadata.labels - Generic YAML:
$.databases.*.connection.host,$[?(@.enabled == true)]
ScanningRecipe Pattern
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.
Testing Recipes
Test Structure
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
)
);
}
}
Testing Best Practices
- Test both changes AND no-changes cases – Ensure recipe doesn’t modify unrelated code
- Test edge cases – Nulls, empty files, missing elements, multi-document YAML
- Test harness runs multiple cycles – Ensures idempotence automatically
- Add
//language=XXXcomments – Helps IDE syntax highlight test code - Use text blocks properly – End
"""delimiter one indent to right of open delimiter
YAML-Specific Testing
Multi-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.
Advanced Features
OpenRewrite Traits
Traits provide semantic abstractions over LST elements, wrapping them with domain-specific logic.
When to use traits:
- You need reusable matching logic across multiple recipes
- You want to separate “what to find” from “what to do”
- You’re working with complex LST patterns repeatedly
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 (Performance Optimization)
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 Deep Dive
JavaTemplate compiles code snippets once and applies them to LST elements.
When to use:
- Adding new code structures (methods, statements, expressions)
- Complex code generation that’s tedious with LST methods
When NOT to use:
- Modifying existing elements (use
.withX()methods) - Simple changes (reordering, renaming, removing)
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:
- Context-free templates (default) are faster
- Use
.contextSensitive()only when referencing local variables/methods - Declare all imports explicitly
- Escape special characters in template strings
State Management
Within 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:
- Use ExecutionContext for visitor state
- Mutate recipe instance fields
- Use static variables
Multi-Module Projects
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;
}
Troubleshooting
Common issues and solutions when developing recipes.
Recipe Not Running on Expected Files
Symptoms: Recipe doesn’t execute on files you expect to change
Solutions:
- Check preconditions – might be too restrictive
- Verify file matches visitor type (JavaIsoVisitor for Java, YamlIsoVisitor for YAML)
- Add debug logging to visitor methods
- Ensure the file contains the patterns you’re looking for
- For YAML: Check file extension (
.ymlvs.yaml)
JavaTemplate Parse Errors
Symptoms: Template fails to compile or apply
Solutions:
- Check imports are declared with
.imports() - Verify classpath includes all referenced types with
.javaParser() - Use
.contextSensitive()if referencing local scope - Escape special characters in template strings
- Ensure placeholder syntax is correct:
#{}for strings#{any(Type)}for LST elements
Tests Passing But Recipe Doesn’t Work in Real Code
Symptoms: RewriteTest passes but recipe fails on actual projects
Solutions:
- Add more realistic test cases with complex code
- Test with external dependencies via parser configuration
- Verify preconditions match real-world usage
- Check for edge cases not covered in tests
- Test with different Java versions if version-specific
Recipe Makes Changes But Not Idempotent
Symptoms: Running recipe multiple times produces different results each time
Solutions:
- Ensure all checks use referential equality (return unchanged LST if no change needed)
- Verify visitor doesn’t accumulate state between invocations
- Check that
getVisitor()returns a NEW instance each time - Ensure recipe class is immutable (uses
@Value) - Test with
rewriteRun()which automatically runs multiple cycles
Type Information Not Available
Symptoms: TypeUtils.isOfClassType() returns false when it should be true
Solutions:
- Ensure test configures classpath with required dependencies
- Check that imports are present in the source file
- Verify the type binding resolved correctly (check for
nulltypes) - Add explicit type attribution if working with dynamic code
YAML Recipe Not Matching Expected Elements
Symptoms: YAML recipe doesn’t find or transform expected YAML elements
Solutions:
- Check JsonPath pattern is correct – test with online JsonPath evaluator
- Verify you’re using correct LST visitor method (visitScalar vs visitMappingEntry)
- Handle null values safely – YAML allows
key: nullandkey: - Check for multi-document YAML (starts with
---) - Verify you’re calling
super.visitX()to traverse tree
YAML Comments or Formatting Not Preserved
Symptoms: Recipe changes YAML but loses comments or formatting
Solutions:
- Never create new LST elements – always use
.withX()methods - Use
ListUtilsfor list operations, never mutate directly - Return original element if no change needed (identity check)
- Check you’re not replacing entire nodes unnecessarily
For more troubleshooting guidance, see ./references/troubleshooting-guide.md.
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 and comments
- Return unchanged LST if no change needed
Immutability & Idempotence
- Recipes must be immutable (no mutable state)
- Same input â same output, always
- Use
@Valueand@EqualsAndHashCode(callSuper = false) getVisitor()must return NEW instance each time
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)
Accessing Bundled Resources
This skill uses progressive disclosure to minimize token usage. Load resources on demand:
Templates (for boilerplate code)
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 header
Examples (for working patterns)
examples/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 migration
References (for detailed guidance)
references/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
When to Load Resources
| 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.
Quick Reference
Key Classes
| 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 |
Key Methods
| 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 |
Common Patterns
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 patterns
Automation Scripts
Use 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 fromgradle/licenseHeader.txt
Token Budget Awareness
This skill is optimized for token efficiency:
- SKILL.md: ~3,500 tokens (loaded when skill activates)
- Templates: Load only when creating new recipes
- Examples: Load only when learning specific patterns
- References: Load only when you need deep dives or validation
Strategy: Start with SKILL.md guidance. Load templates for boilerplate. Load references for troubleshooting or advanced features.
Remember
- Always start with tests (RED)
- Try declarative before Refaster before imperative
- Apply idioms during REFACTOR, not GREEN
- Document with markdown for clarity
- Validate production-readiness before completion
- Preserve formatting and comments
- Do no harm – when in doubt, don’t change