463 lines
15 KiB
Markdown
463 lines
15 KiB
Markdown
---
|
|
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 Recipe Writing Skill
|
|
|
|
## Overview
|
|
|
|
OpenRewrite recipes are automated refactoring operations that modify Lossless Semantic Trees (LSTs) representing source code. This skill guides through creating recipes efficiently and correctly.
|
|
|
|
## When NOT to Use This Skill
|
|
|
|
Do NOT use this skill for:
|
|
- General Java programming questions unrelated to OpenRewrite
|
|
- Questions about running existing OpenRewrite recipes (use OpenRewrite documentation)
|
|
- Build tool configuration unrelated to recipe development
|
|
- General refactoring advice without OpenRewrite context
|
|
|
|
## Quick Start Decision Tree
|
|
|
|
To determine the best approach quickly:
|
|
|
|
1. Can the transformation be expressed by composing existing recipes?
|
|
→ **Use Declarative YAML** (see Declarative YAML Recipes section below)
|
|
|
|
2. Is it a simple expression/statement replacement pattern?
|
|
→ **Use Refaster Template** (see Refaster Template Recipes section below)
|
|
|
|
3. 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.
|
|
|
|
## 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
|
|
|
|
```yaml
|
|
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 operations
|
|
- `references/recipes-spring-boot-common.csv` - 60 Spring Boot migrations and best practices
|
|
- `references/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 fixes
|
|
- `references/recipes-xml-yaml-json-common.csv` - 50 configuration file operations
|
|
- `references/recipes-static-analysis-common.csv` - 50 code analysis and search recipes
|
|
- `references/recipes-logging-common.csv` - 50 logging framework operations
|
|
- `references/recipes-file-operations.csv` - 14 file and text manipulation operations
|
|
|
|
**Usage 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.
|
|
|
|
### 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.
|
|
|
|
## Examples Quick Reference
|
|
|
|
Navigate to the right example based on your needs:
|
|
|
|
- **New to recipes?** Start with `references/example-say-hello-recipe.java`
|
|
- **Need multi-file analysis?** See `references/example-scanning-recipe.java`
|
|
- **Composing recipes?** Check `references/example-declarative-migration.yml`
|
|
|
|
## Imperative Recipe Development Workflow
|
|
|
|
### 1. Set Up Recipe Class
|
|
|
|
```java
|
|
@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 `@Value` and `@EqualsAndHashCode(callSuper = false)` for immutability
|
|
- Ensure all recipes are serializable
|
|
- Define configurable parameters using `@Option` fields
|
|
- Return a NEW instance from `getVisitor()` each time (no caching)
|
|
|
|
### 2. Implement the Visitor
|
|
|
|
```java
|
|
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 `JavaIsoVisitor` when returning the same type (most common)
|
|
- Use `JavaVisitor` only when changing LST types
|
|
- Call `super.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
|
|
- Return unchanged LST if no change needed (referential equality check)
|
|
- Treat LSTs as immutable. Use `.withX()` methods for modifications
|
|
|
|
### 3. Use JavaTemplate for Complex Changes
|
|
|
|
```java
|
|
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"))`
|
|
- Prefer context-free templates (default) as they are faster
|
|
- Use `.contextSensitive()` only when referencing local scope
|
|
|
|
### 4. Add Preconditions (Performance)
|
|
|
|
```java
|
|
@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
|
|
|
|
## Testing Recipes
|
|
|
|
### Test Structure
|
|
|
|
```java
|
|
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
|
|
- Note that the test harness runs multiple cycles to ensure idempotence
|
|
- Add `//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 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
|
|
|
|
```java
|
|
@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 `@Value` and `@EqualsAndHashCode(callSuper = false)`
|
|
- `getVisitor()` must return NEW instance
|
|
|
|
### Never Mutate LSTs
|
|
|
|
```java
|
|
// 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 `OldType` to `NewType`."
|
|
- 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
|
|
|
|
```java
|
|
maybeAddImport("java.util.List");
|
|
maybeAddImport("java.util.Collections", "emptyList");
|
|
```
|
|
|
|
### Removing Imports
|
|
|
|
```java
|
|
maybeRemoveImport("old.package.Type");
|
|
```
|
|
|
|
### Chaining Visitors
|
|
|
|
```java
|
|
doAfterVisit(new OtherRecipe().getVisitor());
|
|
```
|
|
|
|
### Checking Types
|
|
|
|
```java
|
|
if (methodInvocation.getType() != null &&
|
|
TypeUtils.isOfClassType(methodInvocation.getType(), "com.example.Type")) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## Resources
|
|
|
|
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 recipes
|
|
- `assets/template-declarative-recipe.yml` - YAML recipe template
|
|
- `assets/template-recipe-test.java` - Test class template
|
|
|
|
**Load 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 usage
|
|
- `references/example-scanning-recipe.java` - Advanced ScanningRecipe pattern for multi-file analysis
|
|
- `references/example-declarative-migration.yml` - Real-world YAML migration examples
|
|
|
|
**Load 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 operations
|
|
- `references/recipes-spring-boot-common.csv` - 60 Spring Boot migrations and best practices
|
|
- `references/recipes-framework-migrations-common.csv` - 16 major framework migrations (10 different frameworks)
|
|
- `references/recipes-testing-common.csv` - 60 most useful testing recipes
|
|
- `references/recipes-dependencies-common.csv` - 49 dependency operations (Maven+Gradle when possible)
|
|
- `references/recipes-security-common.csv` - 30 security vulnerability recipes
|
|
- `references/recipes-xml-yaml-json-common.csv` - 50 configuration file operations
|
|
- `references/recipes-static-analysis-common.csv` - 50 code analysis recipes
|
|
- `references/recipes-logging.csv` - 153 logging framework recipes
|
|
- `references/recipes-file-operations.csv` - 14 file manipulation recipes
|
|
- Note: `references/recipes-all.csv` exists for maintenance/script purposes but is too large (4,958 recipes) to be used directly
|
|
|
|
**Load when:** Looking for existing recipes
|
|
|
|
- **Checklist** (`references/`) - Verification guide:
|
|
|
|
- `references/checklist-recipe-development.md` - Comprehensive verification checklist covering planning, implementation, testing, and distribution
|
|
|
|
**Load 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 API
|
|
|
|
**Load when:** Managing the skill itself (meta-operation)
|
|
|
|
## Quick Reference
|
|
|
|
**Key Classes:**
|
|
|
|
- `Recipe` - Base class for all recipes
|
|
- `JavaIsoVisitor<ExecutionContext>` - Most common visitor
|
|
- `JavaTemplate` - For generating code snippets
|
|
- `RewriteTest` - Testing interface
|
|
- `ScanningRecipe<T>` - Multi-file analysis
|
|
|
|
**Key Methods:**
|
|
|
|
- `getVisitor()` - Returns visitor instance
|
|
- `super.visitX()` - Traverse subtree
|
|
- `.withX()` - Create modified LST copy
|
|
- `ListUtils.map()` - Transform lists without mutation
|
|
- `doAfterVisit()` - Chain additional visitors
|
|
|
|
**Use this skill for help with:**
|
|
|
|
- Choosing the right recipe type
|
|
- Structuring recipe classes
|
|
- Writing visitor logic
|
|
- Using JavaTemplate
|
|
- Writing tests
|
|
- Debugging common issues
|
|
- Understanding LST structure
|