Initial commit
This commit is contained in:
462
skills/writing-openrewrite-recipes/SKILL.md
Normal file
462
skills/writing-openrewrite-recipes/SKILL.md
Normal file
@@ -0,0 +1,462 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user