Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:57:41 +08:00
commit c2d0b101b0
22 changed files with 6446 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
---
# ==============================================================================
# Example: Framework Migration Recipe
# ==============================================================================
# This example demonstrates a realistic declarative recipe for migrating
# from one version of a framework to another, combining multiple recipes
# with configuration.
#
# Save this file in: src/main/resources/META-INF/rewrite/
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.MigrateToFrameworkX
displayName: Migrate to Framework X 2.0
description: |
Migrates applications from Framework X 1.x to 2.0. This recipe performs the following steps:
- Updates dependency versions
- Migrates renamed packages and types
- Updates deprecated API usage
- Applies code formatting
tags:
- framework-x
- migration
- upgrade
estimatedEffortPerOccurrence: PT15M
recipeList:
# Step 1: Update dependencies
- org.openrewrite.java.dependencies.ChangeDependency:
oldGroupId: com.example.frameworkx
oldArtifactId: frameworkx-core
newGroupId: com.example.frameworkx
newArtifactId: frameworkx-core
newVersion: 2.0.x
- org.openrewrite.java.dependencies.AddDependency:
groupId: com.example.frameworkx
artifactId: frameworkx-new-module
version: 2.0.x
onlyIfUsing: com.example.frameworkx.NewFeature
configuration: implementation
# Step 2: Package and type migrations
- org.openrewrite.java.ChangePackage:
oldPackageName: com.example.frameworkx.old
newPackageName: com.example.frameworkx.v2
recursive: true
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: com.example.frameworkx.OldConfig
newFullyQualifiedTypeName: com.example.frameworkx.v2.NewConfig
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: com.example.frameworkx.OldClient
newFullyQualifiedTypeName: com.example.frameworkx.v2.Client
# Step 3: Method migrations
- org.openrewrite.java.ChangeMethodName:
methodPattern: com.example.frameworkx.v2.Client execute(..)
newMethodName: run
# Step 4: Apply custom recipes (if you have any)
# - com.yourorg.CustomFrameworkXMigration
# Step 5: Format the code
- org.openrewrite.java.format.AutoFormat
---
# ==============================================================================
# Example: Security Fixes Recipe
# ==============================================================================
# This recipe applies common security fixes across a codebase
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.ApplySecurityFixes
displayName: Apply security best practices
description: Applies a collection of security-related fixes and improvements to the codebase.
tags:
- security
- SAST
recipeList:
# Use secure random number generation
- org.openrewrite.java.security.SecureRandom
# Fix SQL injection vulnerabilities
- org.openrewrite.java.security.UseFileCreateTempFile
# Apply static analysis security fixes
- org.openrewrite.staticanalysis.CommonStaticAnalysis
# Remove unused imports
- org.openrewrite.java.format.RemoveUnusedImports
---
# ==============================================================================
# Example: Testing Framework Migration
# ==============================================================================
# Migrates from JUnit 4 to JUnit 5
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.MigrateToJUnit5
displayName: Migrate to JUnit 5
description: Migrates JUnit 4 tests to JUnit 5 (Jupiter).
tags:
- junit
- testing
- junit5
preconditions:
- org.openrewrite.java.search.UsesType:
fullyQualifiedTypeName: org.junit.Test
recipeList:
# Update dependencies
- org.openrewrite.java.dependencies.RemoveDependency:
groupId: junit
artifactId: junit
- org.openrewrite.java.dependencies.AddDependency:
groupId: org.junit.jupiter
artifactId: junit-jupiter
version: 5.9.x
onlyIfUsing: org.junit.Test
configuration: testImplementation
# Migrate annotations and assertions
- org.openrewrite.java.testing.junit5.JUnit4to5Migration
# Format
- org.openrewrite.java.format.AutoFormat
---
# ==============================================================================
# Example: Code Quality Improvements
# ==============================================================================
# A collection of code quality improvements
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.ImproveCodeQuality
displayName: Improve code quality
description: Applies a comprehensive set of code quality improvements.
tags:
- code-quality
- refactoring
- best-practices
recipeList:
# Static analysis
- org.openrewrite.staticanalysis.CommonStaticAnalysis
# Finalize local variables that aren't reassigned
- org.openrewrite.staticanalysis.FinalizeLocalVariables
# Add missing @Override annotations
- org.openrewrite.staticanalysis.MissingOverrideAnnotation
# Simplify boolean expressions
- org.openrewrite.staticanalysis.SimplifyBooleanExpression
# Remove unnecessary null checks
- org.openrewrite.staticanalysis.UnnecessaryNullCheckBeforeInstanceOf
# Format code
- org.openrewrite.java.format.AutoFormat
---
# ==============================================================================
# Example: Simple Type Replacement
# ==============================================================================
# A focused recipe that just replaces one type with another
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.ReplaceStringUtils
displayName: Replace Apache Commons StringUtils with Spring StringUtils
description: Replaces usage of Apache Commons StringUtils with Spring Framework's StringUtils.
recipeList:
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.apache.commons.lang3.StringUtils
newFullyQualifiedTypeName: org.springframework.util.StringUtils
- org.openrewrite.java.dependencies.AddDependency:
groupId: org.springframework
artifactId: spring-core
version: 6.0.x
onlyIfUsing: org.springframework.util.StringUtils
configuration: implementation
---
# ==============================================================================
# TEST EXAMPLE
# ==============================================================================
# Here's how to test a declarative recipe:
#
# ```java
# package com.yourorg;
#
# import org.junit.jupiter.api.Test;
# import org.openrewrite.java.JavaParser;
# import org.openrewrite.test.RecipeSpec;
# import org.openrewrite.test.RewriteTest;
#
# import static org.openrewrite.java.Assertions.java;
#
# class MigrateToFrameworkXTest implements RewriteTest {
#
# @Override
# public void defaults(RecipeSpec spec) {
# spec
# // Load the recipe from resources
# .recipeFromResources("com.yourorg.MigrateToFrameworkX")
# // Add dependencies needed to compile the "before" code
# .parser(JavaParser.fromJavaVersion()
# .classpath("frameworkx-core-1.0"));
# }
#
# @Test
# void migratesFrameworkXCode() {
# rewriteRun(
# java(
# """
# package com.example;
#
# import com.example.frameworkx.old.OldConfig;
#
# class Example {
# OldConfig config;
# }
# """,
# """
# package com.example;
#
# import com.example.frameworkx.v2.NewConfig;
#
# class Example {
# NewConfig config;
# }
# """
# )
# );
# }
# }
# ```
# ==============================================================================
# TIPS FOR DECLARATIVE RECIPES
# ==============================================================================
# 1. Keep them focused - one migration or fix per recipe
# 2. Use meaningful names that describe what the recipe does
# 3. Document the purpose and steps in the description
# 4. Add tags for searchability
# 5. Use preconditions to avoid running on irrelevant files
# 6. Order matters - recipes run sequentially
# 7. Consider adding AutoFormat at the end
# 8. Test each recipe thoroughly
# 9. Wrap string values in quotes if they contain special characters
# 10. Use estimatedEffortPerOccurrence to help users understand the impact

View File

@@ -0,0 +1,186 @@
package com.yourorg;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.NonNull;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
/**
* A simple recipe that adds a hello() method to a specified class.
*
* This example demonstrates:
* - Basic recipe structure with @Value and @EqualsAndHashCode
* - Using @Option for configurable parameters
* - Using JavaTemplate for code generation
* - Checking preconditions before making changes
* - Following the "do no harm" principle
*
* Based on the official OpenRewrite tutorial:
* https://docs.openrewrite.org/authoring-recipes/writing-a-java-refactoring-recipe
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class SayHelloRecipe extends Recipe {
@Option(
displayName = "Fully Qualified Class Name",
description = "A fully qualified class name indicating which class to add a hello() method to.",
example = "com.yourorg.FooBar"
)
@NonNull
String fullyQualifiedClassName;
@JsonCreator
public SayHelloRecipe(@NonNull @JsonProperty("fullyQualifiedClassName") String fullyQualifiedClassName) {
this.fullyQualifiedClassName = fullyQualifiedClassName;
}
@Override
public String getDisplayName() {
return "Say 'Hello'";
}
@Override
public String getDescription() {
return "Adds a \"hello\" method to the specified class.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// Always return a new instance - never cache visitors
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
// Step 1: Traverse the subtree first
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Step 2: Check if this is the class we're looking for
// Do no harm: return unchanged if this isn't the target class
if (cd.getType() == null || !cd.getType().getFullyQualifiedName().equals(fullyQualifiedClassName)) {
return cd;
}
// Step 3: Check if the class already has a hello() method
// Do no harm: don't add if it already exists
boolean helloMethodExists = cd.getBody().getStatements().stream()
.filter(statement -> statement instanceof J.MethodDeclaration)
.map(J.MethodDeclaration.class::cast)
.anyMatch(methodDeclaration -> "hello".equals(methodDeclaration.getName().getSimpleName()));
if (helloMethodExists) {
return cd;
}
// Step 4: Add the hello() method using JavaTemplate
// The template uses #{} for parameter substitution
J.Block body = JavaTemplate.apply(
"public String hello() { return \"Hello from #{}!\"; }",
new Cursor(getCursor(), cd.getBody()),
cd.getBody().getCoordinates().lastStatement(),
fullyQualifiedClassName
);
return cd.withBody(body);
}
};
}
}
// ============================================================
// TEST CLASS
// ============================================================
/*
package com.yourorg;
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
import static org.openrewrite.java.Assertions.java;
class SayHelloRecipeTest implements RewriteTest {
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new SayHelloRecipe("com.yourorg.FooBar"));
}
@DocumentExample
@Test
void addsHelloToFooBar() {
rewriteRun(
java(
"""
package com.yourorg;
class FooBar {
}
""",
"""
package com.yourorg;
class FooBar {
public String hello() {
return "Hello from com.yourorg.FooBar!";
}
}
"""
)
);
}
@Test
void doesNotChangeExistingHello() {
rewriteRun(
java(
"""
package com.yourorg;
class FooBar {
public String hello() { return ""; }
}
"""
)
);
}
@Test
void doesNotChangeOtherClasses() {
rewriteRun(
java(
"""
package com.yourorg;
class OtherClass {
}
"""
)
);
}
}
*/
// ============================================================
// YAML USAGE
// ============================================================
/*
Save this in src/main/resources/META-INF/rewrite/say-hello.yml:
---
type: specs.openrewrite.org/v1beta/recipe
name: com.yourorg.SayHelloToFooBar
displayName: Say Hello to FooBar
description: Adds a hello() method to the FooBar class.
recipeList:
- com.yourorg.SayHelloRecipe:
fullyQualifiedClassName: com.yourorg.FooBar
*/

View File

@@ -0,0 +1,294 @@
package com.yourorg;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.marker.SearchResult;
import java.util.HashMap;
import java.util.Map;
/**
* A ScanningRecipe that marks classes only if the project uses a specific type.
*
* This example demonstrates:
* - ScanningRecipe with accumulator pattern
* - Three-phase execution: scan, generate (optional), edit
* - Tracking per-project information with Map<JavaProject, T>
* - Using accumulator data to inform editing decisions
* - Proper handling of multi-module projects
*
* Use ScanningRecipe when you need to:
* - See all files before making changes
* - Generate new files based on analysis
* - Share data across multiple files
* - Make decisions based on project-wide information
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class ScanningRecipeExample extends ScanningRecipe<ScanningRecipeExample.Accumulator> {
/**
* The accumulator stores data collected during the scanning phase.
* This data is then available during the editing phase.
*
* Important: For multi-module projects, track data per JavaProject.
*/
public static class Accumulator {
// Track which projects use the target type
Map<JavaProject, Boolean> projectUsesTargetType = new HashMap<>();
// You can store any data structure you need
// Map<JavaProject, Set<String>> projectClasses = new HashMap<>();
// Map<JavaProject, List<MethodInfo>> projectMethods = new HashMap<>();
}
@Override
public String getDisplayName() {
return "Mark classes in projects using target type";
}
@Override
public String getDescription() {
return "Marks classes with SearchResult only if the project uses a specific type.";
}
/**
* Initialize the accumulator before scanning begins.
*/
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}
/**
* Phase 1: SCANNING
*
* The scanner visits all source files and collects information
* into the accumulator. No changes are made in this phase.
*/
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Get the JavaProject marker to track per-project data
JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null);
if (project == null) {
return cu;
}
// Initialize project tracking if needed
acc.projectUsesTargetType.putIfAbsent(project, false);
// Scan for the target type in this file
// In this example, we're looking for usage of java.util.Optional
cu.getImports().stream()
.filter(imp -> imp.getTypeName().equals("java.util.Optional"))
.findFirst()
.ifPresent(imp -> acc.projectUsesTargetType.put(project, true));
// Continue scanning subtree
return super.visitCompilationUnit(cu, ctx);
}
@Override
public J.Import visitImport(J.Import import_, ExecutionContext ctx) {
// You can also scan at finer granularity
// Example: track specific imports, method calls, etc.
return import_;
}
};
}
/**
* Phase 2: GENERATING (optional)
*
* This phase is used to generate new source files based on the
* accumulated data. Return null if no new files are needed.
*
* Uncomment to implement:
*/
/*
@Override
public Collection<SourceFile> generate(Accumulator acc, ExecutionContext ctx) {
List<SourceFile> newFiles = new ArrayList<>();
// Example: Generate a file for each project that uses the target type
for (Map.Entry<JavaProject, Boolean> entry : acc.projectUsesTargetType.entrySet()) {
if (entry.getValue()) {
JavaProject project = entry.getKey();
// Create a new source file
// newFiles.add(createNewFile(project));
}
}
return newFiles;
}
*/
/**
* Phase 3: EDITING
*
* The editor visits all source files again and makes changes
* based on the data collected in the scanning phase.
*/
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Get the project for this file
JavaProject project = cu.getMarkers().findFirst(JavaProject.class).orElse(null);
if (project == null) {
return cu;
}
// Check the accumulator to see if this project uses the target type
Boolean usesTargetType = acc.projectUsesTargetType.get(project);
if (usesTargetType == null || !usesTargetType) {
// Don't make changes in projects that don't use the target type
return cu;
}
// This project uses the target type, continue with editing
return super.visitCompilationUnit(cu, ctx);
}
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Get the project
JavaProject project = getCursor()
.firstEnclosing(J.CompilationUnit.class)
.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project == null) {
return cd;
}
// Only mark classes in projects that use the target type
Boolean usesTargetType = acc.projectUsesTargetType.get(project);
if (usesTargetType != null && usesTargetType) {
// Mark this class with a SearchResult
return SearchResult.found(cd, "Found in project using Optional");
}
return cd;
}
};
}
}
// ============================================================
// SIMPLIFIED EXAMPLE: COUNT CLASSES
// ============================================================
/*
Here's a simpler ScanningRecipe that counts classes per project:
@Value
@EqualsAndHashCode(callSuper = false)
public class CountClasses extends ScanningRecipe<CountClasses.Accumulator> {
public static class Accumulator {
Map<JavaProject, Integer> classCounts = new HashMap<>();
}
@Override
public String getDisplayName() {
return "Count classes per project";
}
@Override
public String getDescription() {
return "Counts the number of classes in each project.";
}
@Override
public Accumulator getInitialValue(ExecutionContext ctx) {
return new Accumulator();
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration cd, ExecutionContext ctx) {
JavaProject project = getCursor()
.firstEnclosing(J.CompilationUnit.class)
.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project != null) {
acc.classCounts.merge(project, 1, Integer::sum);
}
return cd;
}
};
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
// Print the results
return new JavaIsoVisitor<ExecutionContext>() {
private boolean printed = false;
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
if (!printed) {
System.out.println("Class counts by project:");
acc.classCounts.forEach((project, count) ->
System.out.println(project.getProjectName() + ": " + count + " classes")
);
printed = true;
}
return cu;
}
};
}
}
*/
// ============================================================
// KEY TAKEAWAYS
// ============================================================
/*
1. ScanningRecipe has THREE phases:
- Scan: Collect data (no changes)
- Generate: Create new files (optional)
- Edit: Make changes based on collected data
2. Always track per-project data with Map<JavaProject, T>
- Don't assume single project per repository
- Get JavaProject from markers
3. Accumulator is shared across all phases
- Use it to pass data from scan to edit
- Keep it simple and focused
4. Scanner makes NO changes
- Only collect information
- Mark files or store data
5. Editor uses accumulator data
- Make informed decisions
- Can access all collected information
6. When multiple ScanningRecipes are in a recipe list:
- All scanners run first
- Then all generators run
- Then all editors run
- Scanners see state before ANY edits
- But scanners DO see generated files
*/

View File

@@ -0,0 +1,282 @@
package com.example.rewrite;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.yaml.JsonPathMatcher;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
/**
* Example recipe demonstrating YAML manipulation for GitHub Actions workflows.
*
* This recipe updates GitHub Actions checkout action from v2/v3 to v4.
*
* Before:
* ```yaml
* jobs:
* build:
* steps:
* - uses: actions/checkout@v2
* ```
*
* After:
* ```yaml
* jobs:
* build:
* steps:
* - uses: actions/checkout@v4
* ```
*
* Key Concepts Demonstrated:
* 1. YamlIsoVisitor for YAML LST manipulation
* 2. JsonPathMatcher for targeted YAML element matching
* 3. Safe value access with type checking
* 4. Preserving formatting and comments
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class UpdateGitHubActionsCheckout extends Recipe {
@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```";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
// JsonPath to match the 'uses' field in GitHub Actions steps
private final JsonPathMatcher matcher =
new JsonPathMatcher("$.jobs.*.steps[*].uses");
@Override
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
// Always call super to traverse the tree
scalar = super.visitScalar(scalar, ctx);
// Check if this scalar matches our JsonPath
if (matcher.matches(getCursor())) {
String value = scalar.getValue();
// Safe null check
if (value != null) {
// Update v2 to v4
if (value.startsWith("actions/checkout@v2")) {
return scalar.withValue(value.replace("@v2", "@v4"));
}
// Update v3 to v4
if (value.startsWith("actions/checkout@v3")) {
return scalar.withValue(value.replace("@v3", "@v4"));
}
}
}
return scalar;
}
};
}
}
/**
* Additional YAML recipe examples demonstrating other common patterns:
*/
/**
* Example: Update Kubernetes container image tags
*
* Before:
* ```yaml
* spec:
* containers:
* - image: myapp:1.0.0
* ```
*
* After:
* ```yaml
* spec:
* containers:
* - image: myapp:2.0.0
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
class UpdateKubernetesImageTag extends Recipe {
String oldTag;
String newTag;
@Override
public String getDisplayName() {
return "Update Kubernetes image tag.";
}
@Override
public String getDescription() {
return "Updates Kubernetes container image tags.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
private final JsonPathMatcher matcher =
new JsonPathMatcher("$.spec.template.spec.containers[*].image");
@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.endsWith(":" + oldTag)) {
return scalar.withValue(
value.substring(0, value.lastIndexOf(":")) + ":" + newTag
);
}
}
return scalar;
}
};
}
}
/**
* Example: Change key name in YAML
*
* Before:
* ```yaml
* oldKey: value
* ```
*
* After:
* ```yaml
* newKey: value
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
class ChangeYamlKey extends Recipe {
String oldKey;
String newKey;
@Override
public String getDisplayName() {
return "Change YAML key name.";
}
@Override
public String getDescription() {
return "Renames a YAML key while preserving its value.";
}
@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);
// Check if this entry has the old key
if (oldKey.equals(entry.getKey().getValue())) {
// Replace the key while preserving everything else
return entry.withKey(
((Yaml.Scalar) entry.getKey()).withValue(newKey)
);
}
return entry;
}
};
}
}
/**
* Example: Update value based on key match
*
* Before:
* ```yaml
* database:
* host: localhost
* ```
*
* After:
* ```yaml
* database:
* host: prod-db.example.com
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
class UpdateYamlValue extends Recipe {
String keyPath;
String oldValue;
String newValue;
@Override
public String getDisplayName() {
return "Update YAML value.";
}
@Override
public String getDescription() {
return "Updates a YAML value at a specific key path.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
private final JsonPathMatcher matcher = new JsonPathMatcher(keyPath);
@Override
public Yaml.Scalar visitScalar(Yaml.Scalar scalar, ExecutionContext ctx) {
scalar = super.visitScalar(scalar, ctx);
if (matcher.matches(getCursor())) {
String value = scalar.getValue();
if (oldValue.equals(value)) {
return scalar.withValue(newValue);
}
}
return scalar;
}
};
}
}
/**
* Common JsonPath Patterns for YAML Recipes:
*
* GitHub Actions:
* - $.jobs.*.steps[*].uses - All 'uses' in steps
* - $.on.push.branches - Push trigger branches
* - $.jobs.*.runs-on - Runner configuration
* - $.jobs.*.strategy.matrix - Matrix strategy
*
* Kubernetes:
* - $.spec.template.spec.containers[*].image - Container images
* - $.metadata.labels - Labels
* - $.spec.replicas - Replica count
* - $.spec.template.spec.containers[*].env[*] - Environment variables
*
* Generic YAML:
* - $.databases.*.connection.host - Nested configuration
* - $[?(@.enabled == true)] - Conditional matching
* - $..*[?(@.type == 'service')] - Deep search with condition
*
* See references/jsonpath-patterns.md for comprehensive examples.
*/

View File

@@ -0,0 +1,198 @@
# OpenRewrite Recipe Development Checklist
Use this checklist to ensure you've covered all important aspects of recipe development.
## Planning Phase
### Recipe Type Selection
- [ ] Determined if recipe can be declarative (preferred)
- [ ] Evaluated if Refaster template would work for simple replacements
- [ ] Confirmed imperative recipe is necessary for complex logic
- [ ] Identified which LST elements need to be visited
- [ ] Reviewed existing recipes to avoid duplication
### Requirements Gathering
- [ ] Clearly defined what the recipe should change
- [ ] Identified what should NOT be changed
- [ ] Documented expected input and output
- [ ] Listed any dependencies or external types needed
- [ ] Determined if multi-file analysis is required (ScanningRecipe)
## Implementation Phase
### Recipe Class Structure
- [ ] Used `@Value` and `@EqualsAndHashCode(callSuper = false)` for immutability
- [ ] All fields are final (via Lombok or manual implementation)
- [ ] Added `@JsonCreator` constructor with `@JsonProperty` annotations
- [ ] Defined `@Option` fields with clear descriptions and examples
- [ ] Implemented `getDisplayName()` with sentence-case name
- [ ] Implemented `getDescription()` with clear, period-ending description
- [ ] `getVisitor()` returns NEW instance (never cached)
### Visitor Implementation
- [ ] Chose correct visitor type (JavaIsoVisitor vs JavaVisitor)
- [ ] Called `super.visitX()` in overridden visit methods
- [ ] Checked for null before accessing type information
- [ ] Implemented "do no harm" - return unchanged LST when unsure
- [ ] Used `.withX()` methods instead of mutating LSTs
- [ ] Used `ListUtils` instead of stream operations on LST collections
- [ ] Avoided creating new lists unnecessarily
### JavaTemplate Usage (if applicable)
- [ ] Used typed substitution `#{any(Type)}` for LST elements
- [ ] Used untyped substitution `#{}` for strings
- [ ] Declared all imports with `.imports()`
- [ ] Declared static imports with `.staticImports()`
- [ ] Configured parser with `.javaParser()` if referencing external types
- [ ] Added classpath dependencies or stubs as needed
- [ ] Used context-free templates (default) when possible
- [ ] Only used `.contextSensitive()` when necessary
### Advanced Features (if applicable)
- [ ] Added preconditions with `Preconditions.check()`
- [ ] Used `UsesType` or `UsesMethod` to filter applicable files
- [ ] Implemented cursor messaging for intra-visitor communication
- [ ] For ScanningRecipe: defined accumulator with `Map<JavaProject, T>`
- [ ] For ScanningRecipe: implemented `getInitialValue()`
- [ ] For ScanningRecipe: implemented `getScanner()` (no changes, only collect)
- [ ] For ScanningRecipe: implemented `getVisitor()` (uses accumulator data)
- [ ] For ScanningRecipe: implemented `generate()` if creating new files
### Imports and Dependencies
- [ ] Used `maybeAddImport()` for new types
- [ ] Used `maybeRemoveImport()` for removed types
- [ ] Chained visitors with `doAfterVisit()` when needed
## Testing Phase
### Test Structure
- [ ] Created test class implementing `RewriteTest`
- [ ] Implemented `defaults(RecipeSpec)` with recipe configuration
- [ ] Added `@DocumentExample` to primary test
### Test Coverage
- [ ] Test for expected changes (before → after)
- [ ] Test for no changes when not applicable (before only)
- [ ] Test for edge cases and boundary conditions
- [ ] Test with multiple files
- [ ] Test that recipe doesn't change already-correct code
- [ ] Test with different parameter values (if applicable)
- [ ] Test with different Java versions (if version-specific)
- [ ] Added classpath dependencies with `spec.parser()`
### Test Quality
- [ ] Test names clearly describe what is being tested
- [ ] Used meaningful package and class names in test code
- [ ] Included comments explaining complex test scenarios
- [ ] Verified tests pass (including multi-cycle verification)
- [ ] Checked that recipe is idempotent (runs produce same result)
## Code Quality Phase
### Best Practices
- [ ] Recipe follows "do no harm" principle
- [ ] Recipe makes minimal, least invasive changes
- [ ] Recipe is immutable (no mutable state)
- [ ] Recipe is idempotent (same input → same output)
- [ ] Recipe never mutates LSTs
- [ ] Recipe respects existing formatting
- [ ] Used referential equality checks (same object = no change)
### Naming Conventions
- [ ] Display name uses sentence case
- [ ] Display name uses backticks around code elements
- [ ] Display name ends with period (if complete sentence)
- [ ] Description is clear and concise
- [ ] Recipe class name follows `VerbNoun` pattern
- [ ] Package name follows `com.yourorg.category` pattern
### Performance
- [ ] Added preconditions to skip irrelevant files
- [ ] Used context-free JavaTemplates when possible
- [ ] Avoided unnecessary LST allocations
- [ ] Recipe completes work in single cycle (no `causesAnotherCycle`)
- [ ] For ScanningRecipe: minimized accumulator size
### Multi-Module Support
- [ ] For ScanningRecipe: tracked data per `JavaProject`
- [ ] Did not assume single project per repository
- [ ] Retrieved JavaProject from markers correctly
## Documentation Phase
### Code Documentation
- [ ] Added class-level JavaDoc describing the recipe
- [ ] Documented all `@Option` fields clearly
- [ ] Added inline comments for complex logic
- [ ] Included usage example in JavaDoc
### External Documentation
- [ ] Created YAML file in `src/main/resources/META-INF/rewrite/` (if distributing)
- [ ] Added recipe to catalog/index (if applicable)
- [ ] Documented any known limitations
- [ ] Added tags for categorization
## Distribution Phase
### Build Configuration
- [ ] Recipe compiles with Java 8 target (or appropriate version)
- [ ] Added `-parameters` compiler flag for Jackson
- [ ] Included rewrite-recipe-bom for dependency management
- [ ] Tests run successfully with build tool
### Publishing
- [ ] Published to local Maven repository for testing (`publishToMavenLocal`)
- [ ] Tested recipe in separate project
- [ ] Configured publishing to artifact repository (if applicable)
- [ ] Tagged release in version control
## Final Verification
### Smoke Testing
- [ ] Ran recipe on sample project
- [ ] Verified changes are correct
- [ ] Verified no unwanted changes were made
- [ ] Checked that formatting is preserved
- [ ] Ran recipe multiple times (idempotence check)
### Common Pitfalls Avoided
- [ ] Did not mutate LSTs directly
- [ ] Did not cache visitor instances
- [ ] Did not use ExecutionContext for visitor state
- [ ] Did not forget to call `super.visitX()`
- [ ] Did not create unnecessary list allocations
- [ ] Did not forget imports in JavaTemplate
- [ ] Did not skip null checks on type information
- [ ] Did not assume single project per repository
## Recipe-Specific Checklists
### For Declarative Recipes
- [ ] Saved in `src/main/resources/META-INF/rewrite/`
- [ ] Used `type: specs.openrewrite.org/v1beta/recipe`
- [ ] All recipe names are fully qualified
- [ ] All parameters are correctly indented
- [ ] String values with special characters are quoted
- [ ] Recipe list is in logical order
- [ ] Tested with `spec.recipeFromResources()`
### For Refaster Template Recipes
- [ ] Created template class with `@RecipeDescriptor`
- [ ] Implemented `@BeforeTemplate` methods
- [ ] Implemented single `@AfterTemplate` method
- [ ] Verified generated recipe works correctly
- [ ] Recipe name uses generated form (e.g., `RecipesName`)
## Notes
Remember:
- **Do no harm**: If unsure, don't change
- **Minimize changes**: Make only necessary modifications
- **Immutability**: Recipes and LSTs must not be mutated
- **Test thoroughly**: Both positive and negative cases
- **Document clearly**: Future maintainers will thank you
For more information, see:
- SKILL.md in this skill directory
- OpenRewrite documentation in this repository
- Examples in examples/ directory

View File

@@ -0,0 +1,356 @@
# OpenRewrite Recipe Common Patterns
Quick reference for frequently used code patterns in recipe development.
## Import Management
### Adding Imports
```java
maybeAddImport("java.util.List");
maybeAddImport("java.util.Collections", "emptyList"); // Static import
```
### Removing Imports
```java
maybeRemoveImport("old.package.Type");
```
## Visitor Patterns
### Chaining Visitors
```java
doAfterVisit(new OtherRecipe().getVisitor());
```
### Cursor Messaging (Intra-visitor State)
```java
// Store state
getCursor().putMessage("key", value);
// Retrieve state
Object value = getCursor().getNearestMessage("key");
```
## Type Checking
### Check Class Type
```java
if (methodInvocation.getType() != null &&
TypeUtils.isOfClassType(methodInvocation.getType(), "com.example.Type")) {
// Type matches
}
```
### Check Method Type
```java
if (TypeUtils.isOfType(method.getMethodType(), "com.example.Type", "methodName")) {
// Method matches
}
```
### Check Assignability
```java
if (TypeUtils.isAssignableTo("java.util.Collection", someType)) {
// Type is assignable to Collection
}
```
## LST Manipulation
### Modifying Lists (Never Mutate!)
```java
// WRONG - Mutates the list
method.getArguments().remove(0);
// CORRECT - Creates new list with ListUtils
method.withArguments(ListUtils.map(method.getArguments(), (i, arg) ->
i == 0 ? null : arg // null removes the element
));
```
### Adding to Lists
```java
// Add at end
classDecl.withModifiers(
ListUtils.concat(classDecl.getModifiers(), newModifier)
);
// Add at beginning
classDecl.withModifiers(
ListUtils.concat(newModifier, classDecl.getModifiers())
);
```
### Replacing in Lists
```java
classDecl.withModifiers(
ListUtils.map(classDecl.getModifiers(), mod ->
shouldReplace(mod) ? newModifier : mod
)
);
```
## JavaTemplate Patterns
### Simple Template
```java
JavaTemplate template = JavaTemplate
.builder("new Expression()")
.build();
// Apply
expression = template.apply(getCursor(), expression.getCoordinates().replace());
```
### Template with Imports
```java
JavaTemplate template = JavaTemplate
.builder("Collections.emptyList()")
.imports("java.util.Collections")
.build();
```
### Template with Parameters
```java
JavaTemplate template = JavaTemplate
.builder("new #{any(String)}(#{})")
.build();
// Apply with parameters
expression = template.apply(
getCursor(),
expression.getCoordinates().replace(),
typeName, // #{any(String)}
constructorArg // #{}
);
```
### Template with External Classpath
```java
JavaTemplate template = JavaTemplate
.builder("new CustomType()")
.imports("com.external.CustomType")
.javaParser(JavaParser.fromJavaVersion()
.classpath("external-library"))
.build();
```
### Context-Sensitive Template
```java
// Use ONLY when referencing local variables/methods
JavaTemplate template = JavaTemplate
.builder("localVariable.toString()")
.contextSensitive()
.build();
```
## Preconditions
### Single Precondition
```java
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
new UsesType<>("com.example.Type", true),
new YourVisitor()
);
}
```
### Multiple Preconditions (AND)
```java
return Preconditions.check(
Preconditions.and(
new UsesType<>("com.example.Type", true),
new UsesMethod<>("com.example.Type methodName(..)")
),
new YourVisitor()
);
```
### Multiple Preconditions (OR)
```java
return Preconditions.check(
Preconditions.or(
new UsesType<>("com.example.TypeA", true),
new UsesType<>("com.example.TypeB", true)
),
new YourVisitor()
);
```
### Java Version Check
```java
return Preconditions.check(
new UsesJavaVersion<>(17),
new YourVisitor()
);
```
## ScanningRecipe Patterns
### Basic Accumulator Structure
```java
public static class Accumulator {
// Use per-project tracking for multi-module support
Map<JavaProject, Set<String>> projectData = new HashMap<>();
}
```
### Scanner (First Pass - Collect Only)
```java
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) {
return new JavaIsoVisitor<>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Collect data - DO NOT MODIFY LST
JavaProject project = cu.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project != null) {
acc.projectData
.computeIfAbsent(project, k -> new HashSet<>())
.add(someData);
}
return cu; // Return unchanged
}
};
}
```
### Visitor (Second Pass - Modify Using Accumulator)
```java
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) {
return new JavaIsoVisitor<>() {
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
JavaProject project = cu.getMarkers()
.findFirst(JavaProject.class)
.orElse(null);
if (project == null) {
return cu;
}
// Use accumulator data to make decisions
Set<String> projectData = acc.projectData.get(project);
if (projectData != null && projectData.contains(someCondition)) {
// Make changes
}
return cu;
}
};
}
```
## Formatting Preservation
### Use Space.format()
```java
// Preserve formatting when adding elements
Space.format("\n" + indent)
```
### Copy Formatting from Existing Elements
```java
newElement = newElement.withPrefix(existingElement.getPrefix());
```
## Testing Patterns
### Multi-file Tests
```java
rewriteRun(
java(
"""
package com.example;
class First { }
""",
"""
package com.example;
class First { /* modified */ }
"""
),
java(
"""
package com.example;
class Second { }
"""
// No second arg = no change expected
)
);
```
### Tests with Parser Configuration
```java
@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new YourRecipe())
.parser(JavaParser.fromJavaVersion()
.classpath("external-library"));
}
```
### Tests with Different Java Versions
```java
rewriteRun(
spec -> spec.parser(JavaParser.fromJavaVersion().version("11")),
java(
// Java 11 specific code
)
);
```
## Error Handling
### Safe Type Access
```java
// Always check for null
if (element.getType() != null) {
// Safe to use type
}
```
### Safe Method Type Access
```java
JavaType.Method methodType = method.getMethodType();
if (methodType != null && methodType.getDeclaringType() != null) {
// Safe to use
}
```
## Performance Optimization
### Referential Equality Check
```java
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
if (!shouldChange(cd)) {
return cd; // Same object = no work downstream
}
```
### Early Return Pattern
```java
// Check cheapest conditions first
if (element.getName() == null) {
return element;
}
if (element.getType() == null) {
return element;
}
// Then check more expensive conditions
if (!TypeUtils.isOfClassType(element.getType(), targetType)) {
return element;
}
// Finally, make changes
return makeChanges(element);
```

View File

@@ -0,0 +1,420 @@
# Java LST Structure Reference
Complete reference for OpenRewrite's Java Lossless Semantic Tree (LST) structure.
## Overview
The Java LST represents Java code as a tree structure that preserves all formatting, comments, and whitespace. This allows transformations that maintain the original file's appearance.
## Type Hierarchy
```
org.openrewrite.java.tree.J
├── J.CompilationUnit (root of Java file)
├── J.ClassDeclaration (class definitions)
├── J.MethodDeclaration (method definitions)
├── J.MethodInvocation (method calls)
├── J.VariableDeclarations (variable declarations)
├── J.Block (code blocks)
├── J.If (if statements)
├── J.ForLoop (for loops)
├── J.WhileLoop (while loops)
├── J.Try (try-catch blocks)
├── J.Import (import statements)
├── J.Annotation (annotations)
├── J.Binary (binary operations: +, -, *, /, &&, ||, etc.)
├── J.Literal (primitive literals)
├── J.Identifier (variable/type names)
├── J.NewClass (object instantiation)
├── J.Return (return statements)
├── J.Assignment (assignments)
└── ... (many more)
```
## Core Types
### J.CompilationUnit
The root element of a Java source file.
```java
public interface CompilationUnit extends JavaSourceFile, J {
List<Import> getImports();
List<ClassDeclaration> getClasses();
Space getEof();
// ... other methods
}
```
**Usage:**
```java
@Override
public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext ctx) {
// Visit entire file
cu = super.visitCompilationUnit(cu, ctx);
// Access package declaration
String packageName = cu.getPackageDeclaration() != null ?
cu.getPackageDeclaration().getExpression().printTrimmed() : null;
// Access imports
List<J.Import> imports = cu.getImports();
// Access classes
List<J.ClassDeclaration> classes = cu.getClasses();
return cu;
}
```
---
### J.ClassDeclaration
Represents class, interface, enum, or record declarations.
```java
public interface ClassDeclaration extends Statement, TypedTree {
List<Annotation> getLeadingAnnotations();
List<Modifier> getModifiers();
Kind getKind(); // Class, Interface, Enum, Record, Annotation
Identifier getName();
@Nullable TypeParameters getTypeParameters();
@Nullable TypeTree getExtends();
@Nullable Container<TypeTree> getImplements();
Block getBody();
JavaType.FullyQualified getType();
}
```
**Usage:**
```java
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
classDecl = super.visitClassDeclaration(classDecl, ctx);
// Get class name
String className = classDecl.getSimpleName();
// Get fully qualified name
if (classDecl.getType() != null) {
String fqn = classDecl.getType().getFullyQualifiedName();
}
// Check if interface
if (classDecl.getKind() == J.ClassDeclaration.Kind.Type.Interface) {
// ...
}
// Access methods
List<Statement> statements = classDecl.getBody().getStatements();
for (Statement statement : statements) {
if (statement instanceof J.MethodDeclaration) {
J.MethodDeclaration method = (J.MethodDeclaration) statement;
// Process method
}
}
return classDecl;
}
```
---
### J.MethodDeclaration
Represents method declarations.
```java
public interface MethodDeclaration extends Statement, TypedTree {
List<Annotation> getLeadingAnnotations();
List<Modifier> getModifiers();
@Nullable TypeParameters getTypeParameters();
@Nullable TypeTree getReturnTypeExpression();
Identifier getName();
List<Statement> getParameters();
@Nullable Container<NameTree> getThrows();
@Nullable Block getBody();
JavaType.Method getMethodType();
}
```
**Usage:**
```java
@Override
public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
method = super.visitMethodDeclaration(method, ctx);
// Get method name
String methodName = method.getSimpleName();
// Get parameters
List<Statement> params = method.getParameters();
// Get return type
TypeTree returnType = method.getReturnTypeExpression();
// Get method body
J.Block body = method.getBody();
// Check if method matches signature
if (method.getMethodType() != null &&
TypeUtils.isOfType(method.getMethodType(), "com.example.Type", "methodName")) {
// Method matches
}
return method;
}
```
---
### J.MethodInvocation
Represents method calls.
```java
public interface MethodInvocation extends Expression, TypedTree, MethodCall {
@Nullable Expression getSelect(); // Object being called on
Identifier getName();
List<Expression> getArguments();
JavaType.Method getMethodType();
}
```
**Usage:**
```java
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
method = super.visitMethodInvocation(method, ctx);
// Get method name
String methodName = method.getSimpleName();
// Get arguments
List<Expression> args = method.getArguments();
// Check if calling specific method
if (method.getMethodType() != null &&
TypeUtils.isOfType(method.getMethodType(), "java.lang.String", "equals")) {
// This is a String.equals() call
}
// Get select (object being called on)
Expression select = method.getSelect();
return method;
}
```
---
### J.VariableDeclarations
Represents variable declarations.
```java
public interface VariableDeclarations extends Statement, TypedTree {
List<Annotation> getLeadingAnnotations();
List<Modifier> getModifiers();
@Nullable TypeTree getTypeExpression();
List<NamedVariable> getVariables();
}
```
**Usage:**
```java
@Override
public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
multiVariable = super.visitVariableDeclarations(multiVariable, ctx);
// Get type
TypeTree type = multiVariable.getTypeExpression();
// Get all variables declared
for (J.VariableDeclarations.NamedVariable var : multiVariable.getVariables()) {
String varName = var.getSimpleName();
Expression initializer = var.getInitializer();
// Process variable
}
return multiVariable;
}
```
---
### J.Import
Represents import statements.
```java
public interface Import extends Statement {
boolean isStatic();
FieldAccess getQualid();
}
```
**Usage:**
```java
@Override
public J.Import visitImport(J.Import _import, ExecutionContext ctx) {
_import = super.visitImport(_import, ctx);
// Get fully qualified name
String fqn = _import.getQualid().printTrimmed();
// Check if static import
if (_import.isStatic()) {
// Static import
}
return _import;
}
```
---
### J.Annotation
Represents annotations.
```java
public interface Annotation extends Expression {
NameTree getAnnotationType();
@Nullable Container<Expression> getArguments();
}
```
**Usage:**
```java
@Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
annotation = super.visitAnnotation(annotation, ctx);
// Get annotation type
String annotationType = annotation.getAnnotationType().printTrimmed();
// Check specific annotation
if ("org.junit.Test".equals(annotationType)) {
// This is a @Test annotation
}
// Get arguments
if (annotation.getArguments() != null) {
List<Expression> args = annotation.getArguments().getElements();
}
return annotation;
}
```
---
## Common Patterns
### Type Checking
```java
// Check if method invocation is of specific type
if (method.getMethodType() != null &&
TypeUtils.isOfClassType(method.getMethodType().getDeclaringType(), "com.example.Class")) {
// Method is declared in com.example.Class
}
// Check method signature
if (TypeUtils.isOfType(method.getMethodType(), "com.example.Type", "methodName")) {
// Method matches
}
```
### Safe Value Access
```java
// Always check for null before accessing type information
if (classDecl.getType() != null) {
String fqn = classDecl.getType().getFullyQualifiedName();
}
// Check for null on optional elements
if (method.getBody() != null) {
List<Statement> statements = method.getBody().getStatements();
}
```
### Modifying LST Elements
```java
// Always use .withX() methods - never mutate
classDecl = classDecl.withName(classDecl.getName().withSimpleName("NewName"));
// Use ListUtils for list operations
classDecl = classDecl.withModifiers(
ListUtils.concat(classDecl.getModifiers(), newModifier)
);
// Remove from list
method = method.withArguments(
ListUtils.map(method.getArguments(), (i, arg) ->
i == indexToRemove ? null : arg
)
);
```
### Import Management
```java
// Add import if not present
maybeAddImport("java.util.List");
// Add static import
maybeAddImport("java.util.Collections", "emptyList");
// Remove import
maybeRemoveImport("old.package.Type");
```
### Visitor Chaining
```java
// Chain another visitor after this one
doAfterVisit(new SomeOtherRecipe().getVisitor());
```
## Visit Method Reference
Common visit methods to override:
| LST Element | Visit Method | Common Use |
|-------------|--------------|------------|
| `J.CompilationUnit` | `visitCompilationUnit()` | Entire file operations |
| `J.ClassDeclaration` | `visitClassDeclaration()` | Class modifications |
| `J.MethodDeclaration` | `visitMethodDeclaration()` | Method modifications |
| `J.MethodInvocation` | `visitMethodInvocation()` | Method call changes |
| `J.VariableDeclarations` | `visitVariableDeclarations()` | Variable operations |
| `J.Block` | `visitBlock()` | Code block operations |
| `J.If` | `visitIf()` | Conditional logic |
| `J.ForLoop` | `visitForLoop()` | Loop transformations |
| `J.Import` | `visitImport()` | Import management |
| `J.Annotation` | `visitAnnotation()` | Annotation operations |
| `J.Binary` | `visitBinary()` | Binary operations |
| `J.Literal` | `visitLiteral()` | Literal values |
| `J.Assignment` | `visitAssignment()` | Assignment operations |
| `J.Return` | `visitReturn()` | Return statements |
| `J.NewClass` | `visitNewClass()` | Object instantiation |
## Best Practices
1. **Always call super** - `super.visitX()` traverses the subtree
2. **Return modified copies** - Never mutate LST elements directly
3. **Use `.withX()` methods** - For all modifications
4. **Handle null cases** - Check for null before accessing type information
5. **Preserve formatting** - LST methods maintain formatting automatically
6. **Use ListUtils** - For all list operations (never mutate directly)
7. **Check types safely** - Use TypeUtils methods with null checks
## See Also
- `references/common-patterns.md` - Code patterns for common operations
- `references/troubleshooting-guide.md` - Solutions to common issues
- `templates/template-imperative-recipe.java` - Complete recipe template

View File

@@ -0,0 +1,524 @@
# JsonPath Patterns for YAML Recipes
Comprehensive collection of JsonPath patterns for common YAML structures including GitHub Actions, Kubernetes, CI/CD configs, and generic YAML.
## Overview
JsonPath provides a query language for navigating YAML/JSON structures. In OpenRewrite, use `JsonPathMatcher` to match specific locations in YAML files.
```java
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (matcher.matches(getCursor())) {
// This element matches the path
}
```
## JsonPath Syntax Quick Reference
| Syntax | Meaning | Example |
|--------|---------|---------|
| `$` | Root element | `$` |
| `.` | Child element | `$.jobs` |
| `..` | Recursive descent | `$..uses` |
| `*` | Wildcard (any element) | `$.jobs.*` |
| `[*]` | Array wildcard | `$.steps[*]` |
| `[n]` | Array index | `$.steps[0]` |
| `[start:end]` | Array slice | `$.steps[0:3]` |
| `[?(@.key)]` | Filter expression | `$[?(@.name)]` |
---
## GitHub Actions Patterns
### Workflow-Level Patterns
```java
// Root workflow properties
"$.name" // Workflow name
"$.on" // Trigger configuration
"$.env" // Workflow-level environment variables
"$.permissions" // Workflow-level permissions
"$.concurrency" // Concurrency configuration
"$.defaults" // Default settings
// Specific triggers
"$.on.push" // Push trigger
"$.on.pull_request" // Pull request trigger
"$.on.workflow_dispatch" // Manual trigger
"$.on.schedule[*]" // Scheduled triggers (cron)
// Trigger details
"$.on.push.branches[*]" // Push branch filters
"$.on.push.paths[*]" // Push path filters
"$.on.pull_request.types[*]" // PR event types
```
### Job-Level Patterns
```java
// All jobs
"$.jobs" // Jobs object
"$.jobs.*" // Any job (wildcard)
"$.jobs.build" // Specific job named 'build'
// Job properties
"$.jobs.*.name" // Job display name
"$.jobs.*.runs-on" // Runner specification
"$.jobs.*.needs" // Job dependencies
"$.jobs.*.if" // Job conditions
"$.jobs.*.timeout-minutes" // Job timeout
"$.jobs.*.strategy" // Matrix/parallel strategy
"$.jobs.*.env" // Job-level environment variables
"$.jobs.*.permissions" // Job-level permissions
"$.jobs.*.container" // Container configuration
"$.jobs.*.services" // Service containers
// Matrix strategy
"$.jobs.*.strategy.matrix" // Matrix configuration
"$.jobs.*.strategy.matrix.os[*]" // OS variations
"$.jobs.*.strategy.matrix.node[*]" // Node.js versions
"$.jobs.*.strategy.fail-fast" // Fail-fast setting
"$.jobs.*.strategy.max-parallel" // Parallel limit
```
### Step-Level Patterns
```java
// All steps
"$.jobs.*.steps" // Steps array
"$.jobs.*.steps[*]" // Any step in any job
"$.jobs.build.steps[*]" // Steps in 'build' job
"$.jobs.*.steps[0]" // First step of each job
// Step properties
"$.jobs.*.steps[*].name" // Step name
"$.jobs.*.steps[*].uses" // Action reference
"$.jobs.*.steps[*].run" // Shell command
"$.jobs.*.steps[*].with" // Action inputs
"$.jobs.*.steps[*].env" // Step environment variables
"$.jobs.*.steps[*].if" // Step condition
"$.jobs.*.steps[*].continue-on-error" // Error handling
// Action inputs (with block)
"$.jobs.*.steps[*].with.node-version" // Specific input
"$.jobs.*.steps[*].with.*" // Any input
```
### Complete GitHub Actions Examples
```java
// Find all uses of actions/checkout
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (matcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
if (scalar.getValue().startsWith("actions/checkout@")) {
// Found checkout action
}
}
}
// Find all jobs running on ubuntu-latest
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.runs-on");
if (matcher.matches(getCursor()) && "runs-on".equals(entry.getKey().getValue())) {
// Process runner specification
}
// Find all node-version specifications in matrix
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.strategy.matrix.node[*]");
if (matcher.matches(getCursor())) {
// Process node version entry
}
// Find environment variables at any level
JsonPathMatcher matcher = new JsonPathMatcher("$..env.*");
if (matcher.matches(getCursor())) {
// Found an environment variable
}
```
---
## Kubernetes Patterns
### Pod/Deployment Patterns
```java
// Metadata
"$.metadata.name" // Resource name
"$.metadata.namespace" // Namespace
"$.metadata.labels.*" // Any label
"$.metadata.annotations.*" // Any annotation
// Spec
"$.spec.replicas" // Replica count
"$.spec.selector" // Pod selector
"$.spec.template" // Pod template
// Pod template
"$.spec.template.metadata.labels.*" // Pod labels
"$.spec.template.spec.containers[*]" // All containers
"$.spec.template.spec.initContainers[*]" // Init containers
"$.spec.template.spec.volumes[*]" // Volumes
// Container details
"$.spec.template.spec.containers[*].name" // Container name
"$.spec.template.spec.containers[*].image" // Container image
"$.spec.template.spec.containers[*].ports[*]" // Container ports
"$.spec.template.spec.containers[*].env[*]" // Environment variables
"$.spec.template.spec.containers[*].resources" // Resource limits
"$.spec.template.spec.containers[*].volumeMounts[*]" // Volume mounts
```
### Service Patterns
```java
"$.spec.type" // Service type (ClusterIP, NodePort, LoadBalancer)
"$.spec.selector.*" // Service selector
"$.spec.ports[*]" // Service ports
"$.spec.ports[*].port" // Port number
"$.spec.ports[*].targetPort" // Target port
"$.spec.ports[*].protocol" // Protocol (TCP/UDP)
```
### ConfigMap/Secret Patterns
```java
"$.data.*" // Any data entry
"$.data.config\\.yaml" // Specific data key
"$.stringData.*" // String data entries
"$.binaryData.*" // Binary data entries
```
### Ingress Patterns
```java
"$.spec.rules[*]" // Ingress rules
"$.spec.rules[*].host" // Host pattern
"$.spec.rules[*].http.paths[*]" // Path rules
"$.spec.rules[*].http.paths[*].path" // Path pattern
"$.spec.rules[*].http.paths[*].backend" // Backend service
"$.spec.tls[*]" // TLS configuration
```
### Complete Kubernetes Examples
```java
// Update container images for nginx
JsonPathMatcher matcher = new JsonPathMatcher("$.spec.template.spec.containers[*].image");
if (matcher.matches(getCursor()) && "image".equals(entry.getKey().getValue())) {
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
if (scalar.getValue().startsWith("nginx:")) {
// Update nginx version
}
}
}
// Find all resource limits
JsonPathMatcher matcher = new JsonPathMatcher("$.spec.template.spec.containers[*].resources.limits.memory");
if (matcher.matches(getCursor())) {
// Process memory limit
}
// Find all environment variables across all containers
JsonPathMatcher matcher = new JsonPathMatcher("$..containers[*].env[*].name");
if (matcher.matches(getCursor())) {
// Process environment variable
}
```
---
## CI/CD Configuration Patterns
### GitLab CI
```java
// Job-level
"$.*.script[*]" // Script commands in any job
"$.*.stage" // Job stage
"$.*.image" // Docker image
"$.*.services[*]" // Service containers
"$.*.before_script[*]" // Before script commands
"$.*.after_script[*]" // After script commands
"$.*.variables.*" // Job variables
"$.*.cache" // Cache configuration
"$.*.artifacts" // Artifacts configuration
// Pipeline-level
"$.stages[*]" // Pipeline stages
"$.variables.*" // Global variables
"$.default" // Default settings
```
### CircleCI
```java
// Jobs
"$.jobs.*" // Any job
"$.jobs.*.docker[*]" // Docker images
"$.jobs.*.docker[*].image" // Docker image
"$.jobs.*.steps[*]" // Job steps
"$.jobs.*.environment.*" // Job environment
// Workflows
"$.workflows.*" // Any workflow
"$.workflows.*.jobs[*]" // Workflow jobs
```
### Travis CI
```java
"$.language" // Language
"$.os" // Operating system
"$.dist" // Distribution
"$.script[*]" // Build script
"$.before_install[*]" // Before install
"$.install[*]" // Install commands
"$.before_script[*]" // Before script
"$.after_success[*]" // After success
"$.matrix.include[*]" // Matrix builds
```
### Jenkins (Declarative Pipeline)
```java
"$.pipeline.agent" // Agent specification
"$.pipeline.stages[*]" // Pipeline stages
"$.pipeline.stages[*].stage" // Stage name
"$.pipeline.stages[*].steps[*]" // Stage steps
"$.pipeline.environment.*" // Environment variables
"$.pipeline.post" // Post actions
```
---
## Generic YAML Patterns
### Common Structures
```java
// Root level
"$.*" // Any root-level property
"$.version" // Version field
"$.name" // Name field
"$.description" // Description field
// Nested structures
"$.config.*" // Any config property
"$.settings.*" // Any settings property
"$.metadata.*" // Any metadata property
// Arrays
"$.items[*]" // Any item in array
"$.items[0]" // First item
"$.items[-1]" // Last item (not supported in all implementations)
// Deep search
"$..name" // Any 'name' at any level
"$..version" // Any 'version' at any level
```
### Package Manager Configs
```java
// package.json (npm)
"$.scripts.*" // npm scripts
"$.dependencies.*" // Dependencies
"$.devDependencies.*" // Dev dependencies
"$.peerDependencies.*" // Peer dependencies
// composer.json (PHP)
"$.require.*" // PHP dependencies
"$.require-dev.*" // Dev dependencies
"$.autoload.psr-4.*" // PSR-4 autoload
// Gemfile (Ruby) - typically not YAML but similar patterns
"$.dependencies[*]" // Gem dependencies
```
### Docker Compose
```java
"$.version" // Compose file version
"$.services.*" // Any service
"$.services.*.image" // Service image
"$.services.*.build" // Build configuration
"$.services.*.ports[*]" // Port mappings
"$.services.*.environment.*" // Environment variables
"$.services.*.volumes[*]" // Volume mounts
"$.services.*.depends_on[*]" // Dependencies
"$.networks.*" // Network definitions
"$.volumes.*" // Volume definitions
```
### Ansible Playbooks
```java
"$[*].hosts" // Target hosts
"$[*].tasks[*]" // All tasks
"$[*].tasks[*].name" // Task names
"$[*].tasks[*].when" // Task conditions
"$[*].vars.*" // Variables
"$[*].roles[*]" // Roles
```
---
## Advanced Patterns
### Recursive Descent
Find elements at any depth in the structure:
```java
// Find all 'version' keys anywhere in document
JsonPathMatcher matcher = new JsonPathMatcher("$..version");
// Find all 'env' objects anywhere
JsonPathMatcher matcher = new JsonPathMatcher("$..env");
// Find all arrays named 'items' anywhere
JsonPathMatcher matcher = new JsonPathMatcher("$..items[*]");
```
### Multiple Matchers
Use multiple matchers for precise targeting:
```java
// Match steps that use actions AND are in build job
JsonPathMatcher stepMatcher = new JsonPathMatcher("$.jobs.build.steps[*].uses");
JsonPathMatcher anyStepMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (stepMatcher.matches(getCursor()) || anyStepMatcher.matches(getCursor())) {
// Process action reference
}
```
### Combining with Key Checks
```java
// Match 'uses' key within steps
JsonPathMatcher pathMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
// Check both path AND key name
if (pathMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
// This is definitely a 'uses' field in a step
}
return super.visitMappingEntry(entry, ctx);
}
```
---
## Pattern Selection Guide
| Use Case | Pattern Type | Example |
|----------|--------------|---------|
| Exact path | Explicit path | `$.jobs.build.steps[0]` |
| Any child | Wildcard | `$.jobs.*` |
| Any array item | Array wildcard | `$.steps[*]` |
| Any depth | Recursive descent | `$..version` |
| Conditional | With key check | `pathMatcher.matches() && "key".equals(key)` |
---
## Common Mistakes
### 1. Forgetting Key Name Check
```java
// ❌ WRONG - matches parent path, not the specific key
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (matcher.matches(getCursor())) {
// This matches the STEP, not the 'uses' key
}
// ✅ CORRECT - check both path and key
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (matcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
// Now we're definitely at the 'uses' key
}
```
### 2. Wrong Visitor Method
```java
// ❌ WRONG - visitMapping() doesn't work for sequences
JsonPathMatcher matcher = new JsonPathMatcher("$.steps[*]");
public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) {
// Won't match array items
}
// ✅ CORRECT - use visitSequenceEntry()
JsonPathMatcher matcher = new JsonPathMatcher("$.steps[*]");
public Yaml.Sequence.Entry visitSequenceEntry(Yaml.Sequence.Entry entry, ExecutionContext ctx) {
if (matcher.matches(getCursor())) {
// Process sequence entry
}
}
```
### 3. Overly Broad Patterns
```java
// ❌ TOO BROAD - matches 'uses' anywhere
JsonPathMatcher matcher = new JsonPathMatcher("$..uses");
// ✅ BETTER - specific to GitHub Actions steps
JsonPathMatcher matcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
```
---
## Testing JsonPath Patterns
```java
// In your test, verify the path matches
@Test
void testJsonPathMatching() {
rewriteRun(
spec -> spec.recipe(new YourRecipe()),
yaml(
"""
jobs:
build:
steps:
- uses: actions/checkout@v2
""",
"""
jobs:
build:
steps:
- uses: actions/checkout@v3
"""
)
);
}
```
---
## Quick Reference Table
| YAML Structure | JsonPath Pattern | Visitor Method |
|----------------|------------------|----------------|
| Root property | `$.property` | `visitMappingEntry` |
| Nested property | `$.parent.child` | `visitMappingEntry` |
| Any property | `$.*` or `$.parent.*` | `visitMappingEntry` |
| Array item | `$.array[*]` | `visitSequenceEntry` |
| Nested array | `$.parent.array[*]` | `visitSequenceEntry` |
| Any depth | `$..property` | `visitMappingEntry` |
| Multiple levels | `$.a.*.b.*.c` | `visitMappingEntry` |
---
## Additional Resources
- JsonPath Specification: https://goessner.net/articles/JsonPath/
- JsonPath Evaluator (online tool): https://jsonpath.com/
- OpenRewrite JsonPathMatcher JavaDoc: https://docs.openrewrite.org/

View File

@@ -0,0 +1,328 @@
# OpenRewrite Traits Guide
This is a reference guide for understanding and using OpenRewrite Traits in recipe development.
## What are Traits?
Traits are semantic wrappers around LST elements that implement the `Trait<T extends Tree>` interface. They encapsulate domain-specific logic for identifying and accessing properties of tree elements, providing a higher-level abstraction over raw LST navigation.
## IMPORTANT - Deprecated Traits Utility Classes
Older OpenRewrite code used utility classes like `org.openrewrite.java.trait.Traits`, `org.openrewrite.gradle.trait.Traits`, etc. These are now deprecated. Always instantiate matchers directly:
```java
// ❌ Old (deprecated):
Traits.literal()
Traits.methodAccess("...")
// ✅ New (preferred):
new Literal.Matcher()
new MethodAccess.Matcher("...")
```
## Core Trait Interface
The Trait interface is minimal and only requires implementing `getCursor()`:
```java
public interface Trait<T extends Tree> {
Cursor getCursor();
default T getTree() {
return getCursor().getValue();
}
}
```
## Example Trait Implementation
Here's a complete trait implementation pattern (implementation details like Lombok are optional):
```java
// Using Lombok for convenience (optional - you can implement manually)
@Value
public class YamlScalar implements Trait<Yaml.Block> {
Cursor cursor; // Required for getCursor()
// Optional: Additional fields for caching computed values
@Nullable String cachedValue;
// Domain-specific accessor methods
public @Nullable String getValue() {
if (cachedValue != null) {
return cachedValue;
}
Yaml.Block block = getTree();
return block instanceof Yaml.Scalar
? ((Yaml.Scalar) block).getValue()
: null;
}
// Static utility methods for shared logic
public static boolean isScalar(Cursor cursor) {
return cursor.getValue() instanceof Yaml.Scalar;
}
// Modification methods return new trait instances
public YamlScalar withValue(String newValue) {
Yaml.Scalar scalar = (Yaml.Scalar) getTree();
Yaml.Scalar updated = scalar.withValue(newValue);
return new YamlScalar(cursor.withValue(updated), null);
}
// Matcher nested as static inner class
public static class Matcher extends SimpleTraitMatcher<YamlScalar> {
// Optional: Configuration fields for filtering
@Nullable
protected String requiredValue;
@Override
protected @Nullable YamlScalar test(Cursor cursor) {
Object value = cursor.getValue();
if (!(value instanceof Yaml.Block)) {
return null;
}
// Complex matching logic with guards
if (!isScalar(cursor)) {
return null;
}
Yaml.Scalar scalar = (Yaml.Scalar) value;
// Apply filters if configured
if (requiredValue != null &&
!requiredValue.equals(scalar.getValue())) {
return null;
}
// Return trait with cached data
return new YamlScalar(cursor, scalar.getValue());
}
// Configuration methods for the matcher
public Matcher withRequiredValue(String value) {
this.requiredValue = value;
return this;
}
}
}
```
## Important Implementation Notes
1. **Required Interface**: Only `getCursor()` is required by the Trait interface
2. **Implementation Flexibility**: You can use Lombok, manual implementation, or any pattern you prefer
3. **Matcher as Inner Class**: By convention, nest the Matcher as a static inner class
4. **Additional Fields**: Traits can have fields beyond cursor to cache expensive computations
5. **Static Utilities**: Include static helper methods for validation and shared logic
6. **Matcher Fields**: Matchers can have configuration fields for filtering behavior
7. **test() Complexity**: Real `test()` methods often contain substantial validation logic, not just simple instanceof checks
## TraitMatcher API Methods
The `TraitMatcher` interface provides several powerful methods for finding traits in the LST. Understanding these methods is essential for effective trait usage. The `SimpleTraitMatcher<U>` base class implements all these methods using a single abstract `test(Cursor)` method.
### Core API Methods
```java
// Test if cursor matches and return trait instance
Optional<U> get(Cursor cursor)
// Like get() but throws NoSuchElementException if no match
U require(Cursor cursor)
// Stream of all matching traits in ancestor chain (up the tree)
Stream<U> higher(Cursor cursor)
// Stream of all matching traits in descendants (down the tree)
Stream<U> lower(Cursor cursor)
// Stream of all matching traits in entire source file
Stream<U> lower(SourceFile sourceFile)
// Convert matcher to TreeVisitor for use in recipes
<P> TreeVisitor<? extends Tree, P> asVisitor(VisitFunction2<U, P> visitor)
```
### How SimpleTraitMatcher Implements These Methods
`SimpleTraitMatcher<U>` provides default implementations of all TraitMatcher methods by calling your abstract `test(Cursor)` method:
- `get(Cursor)` - Calls `test(cursor)` and wraps result in Optional
- `require(Cursor)` - Calls `test(cursor)` and throws if null
- `higher(Cursor)` - Walks up cursor stack, calling `test()` on each ancestor
- `lower(Cursor)` - Visits all descendants, calling `test()` on each node
- `asVisitor()` - Creates a TreeVisitor that calls `test()` on every visited node
This means you only need to implement `test(Cursor)` to get all these capabilities.
## Example 1: Finding All Traits in a File
Use `lower()` to find all matching traits in a source file or subtree:
```java
// Create matcher for GitHub Actions steps
ActionStep.Matcher stepMatcher = new ActionStep.Matcher();
// In your visitor, find all ActionStep traits in the entire file
@Override
public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) {
// Get stream of all ActionStep traits in this file
Stream<ActionStep> allSteps = stepMatcher.lower(documents);
// Process the stream - e.g., collect all action references
List<String> actionRefs = allSteps
.map(step -> step.getActionRef())
.filter(ref -> ref != null)
.collect(Collectors.toList());
// Log found actions
for (String ref : actionRefs) {
System.out.println("Found action: " + ref);
}
return super.visitDocuments(documents, ctx);
}
```
## Example 2: Checking Parent Context with higher()
Use `higher()` to search up the ancestor chain to check if you're within a specific context:
```java
// Trait for GitHub Actions permissions blocks
PermissionsScope.Matcher permissionsMatcher = new PermissionsScope.Matcher();
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
// Check if we're inside a permissions block
Optional<PermissionsScope> permissionsScope =
permissionsMatcher.higher(getCursor()).findFirst();
if (permissionsScope.isPresent() && "contents".equals(entry.getKey().getValue())) {
// We found a "contents" key inside a permissions block
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
if ("write".equals(scalar.getValue())) {
return SearchResult.found(entry,
"Found write permission to contents");
}
}
}
return super.visitMappingEntry(entry, ctx);
}
```
## Example 3: Using asVisitor() in Recipes
The `asVisitor()` method is the primary way to use traits in recipes:
```java
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
ActionStep.Matcher matcher = new ActionStep.Matcher();
// Convert matcher to visitor with VisitFunction2
return matcher.asVisitor((step, ctx) -> {
String ref = step.getActionRef();
if (ref != null && ref.contains("@v2")) {
return step.withActionRef(ref.replace("@v2", "@v3")).getTree();
}
return step.getTree();
});
}
```
## When to Create a Trait
A trait is the best choice when:
1. **You want to provide shared functionality encapsulating several possible LST types**
- Example: A `YamlValue` trait that works with both `Yaml.Scalar`, `Yaml.Sequence`, and `Yaml.Mapping`
- The trait provides a unified interface regardless of the underlying concrete type
- Allows recipes to work generically with different YAML structures
2. **You want to provide functionality specific to a subset of an individual LST type**
- Example: An `ActionStep` trait for `Yaml.Mapping.Entry` elements that have a `uses` key
- Not all mapping entries are action steps, only those matching specific criteria
- The trait represents a semantic concept within a broader LST type
Additional scenarios where traits add value:
- **Semantic abstraction** over complex LST patterns (e.g., "a workflow trigger")
- **Reusable matching logic** across multiple recipes
- **Domain-specific accessors** that hide LST complexity
- **Composable filtering** with matcher chains
## When to Use Utility Interfaces Instead
Use utility interfaces for:
- **Simple helper methods** for visitors (e.g., safe scalar extraction)
- **Stateless operations** that don't need cursor context
- **Common patterns** that don't warrant full trait machinery
- **Mixins** to share functionality across visitor classes
## Domain-Specific Matcher Base Classes
For traits within a specific domain (e.g., YAML, Gradle, Maven), consider creating an intermediate matcher base class that provides shared utilities:
```java
public abstract class YamlTraitMatcher<U extends Trait<?>>
extends SimpleTraitMatcher<U> {
// Shared utility for all YAML trait matchers
protected boolean withinMapping(Cursor cursor) {
return cursor.firstEnclosing(Yaml.Mapping.class) != null;
}
// Common validation logic
protected boolean isValidYamlContext(Cursor cursor) {
SourceFile sourceFile = cursor.firstEnclosing(SourceFile.class);
return sourceFile instanceof Yaml.Documents;
}
// Helper for finding parent entries
protected @Nullable Yaml.Mapping.Entry getParentEntry(Cursor cursor) {
return cursor.firstEnclosing(Yaml.Mapping.Entry.class);
}
}
```
Then your specific trait matchers extend this base class:
```java
public static class Matcher extends YamlTraitMatcher<ActionStep> {
@Override
protected @Nullable ActionStep test(Cursor cursor) {
// Can use withinMapping(), isValidYamlContext(), etc. here
if (!isValidYamlContext(cursor)) {
return null;
}
// ... rest of matching logic
return null;
}
}
```
## Trait Best Practices Summary
**Core Requirements:**
- Implement `Trait<T extends Tree>` interface with `getCursor()` method
- Nest a `Matcher` as a static inner class extending `SimpleTraitMatcher<T>`
- Override `test(Cursor)` in your matcher to implement matching logic
**Instantiation Pattern:**
```java
// ✅ Correct - Direct instantiation
YamlScalar.Matcher matcher = new YamlScalar.Matcher();
// ❌ Wrong - Using deprecated Traits utility
Traits.yamlScalar() // Don't use - deprecated
```
**Common Patterns:**
- Use `higher()` to search ancestor chain
- Use `lower()` to search descendants
- Use `asVisitor()` in recipe `getVisitor()` methods
- Extend domain-specific matchers like `YamlTraitMatcher` for shared utilities

View File

@@ -0,0 +1,736 @@
# YAML LST Structure Reference
Complete reference for OpenRewrite's YAML Lossless Semantic Tree (LST) structure.
## Overview
The YAML LST represents YAML documents as a tree structure that preserves all formatting, comments, and whitespace. This allows transformations that maintain the original file's appearance.
## Type Hierarchy
```
org.openrewrite.yaml.tree.Yaml
├── Yaml.Documents (root container)
├── Yaml.Document (single document in multi-doc file)
├── Yaml.Mapping (key-value pairs, similar to JSON object)
│ └── Yaml.Mapping.Entry (single key-value pair)
├── Yaml.Sequence (arrays/lists)
│ └── Yaml.Sequence.Entry (single array item)
├── Yaml.Scalar (primitive values: strings, numbers, booleans)
│ └── Yaml.Scalar.Key (key in a key-value pair)
└── Yaml.Anchor (YAML anchors and aliases)
```
## Core Types
### Yaml.Documents
The root element containing one or more YAML documents.
```java
public interface Documents extends Yaml {
List<Document> getDocuments();
Documents withDocuments(List<Document> documents);
}
```
**Usage:**
```java
@Override
public Yaml.Documents visitDocuments(Yaml.Documents documents, ExecutionContext ctx) {
// Process all documents in the file
List<Yaml.Document> modified = ListUtils.map(
documents.getDocuments(),
doc -> (Yaml.Document) visit(doc, ctx)
);
return documents.withDocuments(modified);
}
```
---
### Yaml.Document
A single YAML document (files can contain multiple documents separated by `---`).
```java
public interface Document extends Yaml {
Block getBlock(); // Root block (usually Mapping or Sequence)
Document withBlock(Block block);
boolean isExplicit(); // True if document starts with ---
}
```
**Usage:**
```java
@Override
public Yaml.Document visitDocument(Yaml.Document document, ExecutionContext ctx) {
if (document.getBlock() instanceof Yaml.Mapping) {
Yaml.Mapping root = (Yaml.Mapping) document.getBlock();
// Process root mapping
Yaml.Mapping modified = (Yaml.Mapping) visit(root, ctx);
if (modified != root) {
return document.withBlock(modified);
}
}
return super.visitDocument(document, ctx);
}
```
---
### Yaml.Mapping
Represents a YAML mapping (key-value pairs), equivalent to JSON objects or Python dictionaries.
```java
public interface Mapping extends Block {
List<Entry> getEntries();
Mapping withEntries(List<Entry> entries);
String getAnchor(); // YAML anchor if present
}
```
**Example YAML:**
```yaml
name: my-workflow
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
```
**Navigation:**
```java
@Override
public Yaml.Mapping visitMapping(Yaml.Mapping mapping, ExecutionContext ctx) {
for (Yaml.Mapping.Entry entry : mapping.getEntries()) {
String key = entry.getKey().getValue();
Block value = entry.getValue();
if ("jobs".equals(key) && value instanceof Yaml.Mapping) {
Yaml.Mapping jobsMapping = (Yaml.Mapping) value;
// Process each job
for (Yaml.Mapping.Entry jobEntry : jobsMapping.getEntries()) {
String jobName = jobEntry.getKey().getValue();
// Process job...
}
}
}
return super.visitMapping(mapping, ctx);
}
```
---
### Yaml.Mapping.Entry
A single key-value pair within a mapping.
```java
public interface Entry extends Yaml {
Yaml.Scalar.Key getKey();
Block getValue(); // Can be Scalar, Mapping, or Sequence
Entry withKey(Yaml.Scalar.Key key);
Entry withValue(Block value);
}
```
**Key Methods:**
- `getKey().getValue()` - Get the key as a string (always safe, no cast needed)
- `getValue()` - Get the value (requires type check and cast)
- `withKey()` - Create new entry with different key
- `withValue()` - Create new entry with different value
**Usage:**
```java
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
String keyName = entry.getKey().getValue(); // Safe access
// Check value type before processing
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
String value = scalar.getValue();
// Modify value
if ("old-value".equals(value)) {
return entry.withValue(scalar.withValue("new-value"));
}
} else if (entry.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping nested = (Yaml.Mapping) entry.getValue();
// Process nested mapping
} else if (entry.getValue() instanceof Yaml.Sequence) {
Yaml.Sequence sequence = (Yaml.Sequence) entry.getValue();
// Process sequence
}
return super.visitMappingEntry(entry, ctx);
}
```
---
### Yaml.Sequence
Represents a YAML sequence (array/list).
```java
public interface Sequence extends Block {
List<Entry> getEntries();
Sequence withEntries(List<Entry> entries);
String getAnchor();
}
```
**Example YAML:**
```yaml
branches:
- main
- develop
- feature/*
# Or inline style:
branches: [main, develop, feature/*]
```
**Navigation:**
```java
@Override
public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) {
List<Yaml.Sequence.Entry> entries = sequence.getEntries();
// Iterate through sequence items
for (Yaml.Sequence.Entry entry : entries) {
if (entry.getBlock() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock();
String value = scalar.getValue();
// Process value...
} else if (entry.getBlock() instanceof Yaml.Mapping) {
Yaml.Mapping mapping = (Yaml.Mapping) entry.getBlock();
// Process mapping item...
}
}
return super.visitSequence(sequence, ctx);
}
```
**Adding Items:**
```java
@Override
public Yaml.Sequence visitSequence(Yaml.Sequence sequence, ExecutionContext ctx) {
// Check if 'main' branch exists
boolean hasMain = false;
for (Yaml.Sequence.Entry entry : sequence.getEntries()) {
if (entry.getBlock() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock();
if ("main".equals(scalar.getValue())) {
hasMain = true;
break;
}
}
}
if (!hasMain) {
// Create new scalar for 'main'
Yaml.Scalar mainScalar = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.PLAIN,
null,
"main"
);
// Wrap in sequence entry
Yaml.Sequence.Entry mainEntry = new Yaml.Sequence.Entry(
Tree.randomId(),
Space.format("\n - "), // Proper indentation
Markers.EMPTY,
mainScalar,
false
);
// Add to sequence
return sequence.withEntries(
ListUtils.concat(sequence.getEntries(), mainEntry)
);
}
return super.visitSequence(sequence, ctx);
}
```
---
### Yaml.Sequence.Entry
A single item in a sequence.
```java
public interface Entry extends Yaml {
Block getBlock(); // The actual value
Entry withBlock(Block block);
boolean isTrailingCommaPrefix();
}
```
**Usage:**
```java
@Override
public Yaml.Sequence.Entry visitSequenceEntry(Yaml.Sequence.Entry entry, ExecutionContext ctx) {
if (entry.getBlock() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getBlock();
String value = scalar.getValue();
// Update version references
if (value.contains("@v2")) {
String updated = value.replace("@v2", "@v3");
return entry.withBlock(scalar.withValue(updated));
}
}
return super.visitSequenceEntry(entry, ctx);
}
```
---
### Yaml.Scalar
Represents primitive values (strings, numbers, booleans, null).
```java
public interface Scalar extends Block {
Style getStyle(); // PLAIN, SINGLE_QUOTED, DOUBLE_QUOTED, etc.
String getAnchor();
String getValue();
Scalar withValue(String value);
Scalar withStyle(Style style);
enum Style {
PLAIN,
SINGLE_QUOTED,
DOUBLE_QUOTED,
LITERAL,
FOLDED
}
}
```
**Example YAML:**
```yaml
plain: value
single: 'value'
double: "value"
number: 42
boolean: true
null_value: null
multiline: |
Line 1
Line 2
```
**Usage:**
```java
// Reading scalar values
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
String value = scalar.getValue();
Yaml.Scalar.Style style = scalar.getStyle();
// Modify value while preserving style
Yaml.Scalar updated = scalar.withValue("new-value");
return entry.withValue(updated);
}
// Creating new scalars
Yaml.Scalar newScalar = new Yaml.Scalar(
Tree.randomId(), // Unique ID
Space.EMPTY, // Prefix whitespace
Markers.EMPTY, // Markers for search results, etc.
Yaml.Scalar.Style.PLAIN, // Quoting style
null, // Anchor
"value" // Actual value
);
```
---
### Yaml.Scalar.Key
Special scalar type used for keys in mappings.
```java
public interface Key extends Yaml {
String getValue();
Key withValue(String value);
}
```
**Usage:**
```java
// Keys are always accessible via entry.getKey()
String keyName = entry.getKey().getValue(); // No casting needed!
// Rename a key
if ("old-key".equals(entry.getKey().getValue())) {
Yaml.Scalar.Key newKey = entry.getKey().withValue("new-key");
return entry.withKey(newKey);
}
```
---
## Navigation Patterns
### Cursor Navigation
The `Cursor` provides context about the current position in the tree.
```java
// Get parent elements
Cursor parent = getCursor().getParent();
Cursor grandparent = getCursor().getParent(2);
// Check parent type
if (parent != null && parent.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue();
// Process parent...
}
// Get all ancestors of a type
Iterator<Yaml.Mapping> mappings = getCursor().getPathAsStream()
.filter(p -> p instanceof Yaml.Mapping)
.map(p -> (Yaml.Mapping) p)
.iterator();
```
### Finding Siblings
```java
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
// Get parent mapping to access siblings
Cursor parent = getCursor().getParent();
if (parent != null && parent.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping parentMapping = (Yaml.Mapping) parent.getValue();
// Find sibling entries
for (Yaml.Mapping.Entry sibling : parentMapping.getEntries()) {
if (sibling != entry) {
String siblingKey = sibling.getKey().getValue();
// Check sibling...
}
}
}
return super.visitMappingEntry(entry, ctx);
}
```
### Path-Based Navigation with JsonPath
```java
// Match specific paths in YAML structure
JsonPathMatcher jobMatcher = new JsonPathMatcher("$.jobs.*");
JsonPathMatcher stepMatcher = new JsonPathMatcher("$.jobs.*.steps[*]");
JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
// This is a 'uses' field within a step
// Process it...
}
return super.visitMappingEntry(entry, ctx);
}
```
---
## Type Checking and Casting
### Safe Type Checking Pattern
```java
Block value = entry.getValue();
if (value instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) value;
String stringValue = scalar.getValue();
// Process scalar...
} else if (value instanceof Yaml.Mapping) {
Yaml.Mapping mapping = (Yaml.Mapping) value;
// Process mapping...
} else if (value instanceof Yaml.Sequence) {
Yaml.Sequence sequence = (Yaml.Sequence) value;
// Process sequence...
} else {
// Handle other types or null
}
```
### Null Safety
```java
// Keys never need null checking (always present)
String key = entry.getKey().getValue(); // Safe
// Values might be null or unexpected types
Block value = entry.getValue();
if (value == null) {
// Handle null value (represents 'key:' with no value)
}
// Scalar values can be null string
if (value instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) value;
String stringValue = scalar.getValue();
if (stringValue == null || "null".equals(stringValue)) {
// Handle YAML null
}
}
```
---
## Creating New Elements
### Creating Scalars
```java
// Plain scalar
Yaml.Scalar plain = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.PLAIN,
null,
"value"
);
// Quoted scalar
Yaml.Scalar quoted = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.DOUBLE_QUOTED,
null,
"value with spaces"
);
```
### Creating Mapping Entries
```java
// Create key
Yaml.Scalar.Key key = new Yaml.Scalar.Key(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
"key-name"
);
// Create value
Yaml.Scalar value = new Yaml.Scalar(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
Yaml.Scalar.Style.PLAIN,
null,
"value"
);
// Create entry
Yaml.Mapping.Entry newEntry = new Yaml.Mapping.Entry(
Tree.randomId(),
Space.format("\n "), // Indentation
Markers.EMPTY,
key,
Space.format(" "), // Space after colon
value
);
```
### Creating Sequences
```java
// Create sequence items
List<Yaml.Sequence.Entry> entries = new ArrayList<>();
entries.add(new Yaml.Sequence.Entry(
Tree.randomId(),
Space.format("\n - "),
Markers.EMPTY,
new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY,
Yaml.Scalar.Style.PLAIN, null, "item1"),
false
));
entries.add(new Yaml.Sequence.Entry(
Tree.randomId(),
Space.format("\n - "),
Markers.EMPTY,
new Yaml.Scalar(Tree.randomId(), Space.EMPTY, Markers.EMPTY,
Yaml.Scalar.Style.PLAIN, null, "item2"),
false
));
// Create sequence
Yaml.Sequence sequence = new Yaml.Sequence(
Tree.randomId(),
Space.EMPTY,
Markers.EMPTY,
null, // anchor
entries
);
```
---
## Common Pitfalls
### 1. Not Calling Super Methods
```java
// ❌ WRONG - tree traversal stops
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if ("target".equals(entry.getKey().getValue())) {
return entry.withValue(newValue);
}
return entry; // ❌ Should call super
}
// ✅ CORRECT
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
if ("target".equals(entry.getKey().getValue())) {
return entry.withValue(newValue);
}
return super.visitMappingEntry(entry, ctx); // ✅
}
```
### 2. Mutating Instead of Creating New Objects
```java
// ❌ WRONG - LST is immutable
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
scalar.setValue("new-value"); // ❌ This doesn't exist
// ✅ CORRECT
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
Yaml.Scalar updated = scalar.withValue("new-value");
return entry.withValue(updated);
```
### 3. Forgetting Type Checks
```java
// ❌ WRONG - may throw ClassCastException
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
// ✅ CORRECT
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
// Process safely
}
```
### 4. Incorrect Whitespace/Indentation
```java
// ❌ WRONG - no indentation
Space.EMPTY // Results in key:value on same line as parent
// ✅ CORRECT - proper YAML indentation
Space.format("\n ") // Newline + 2-space indent
```
### 5. Not Returning Original When Unchanged
```java
// ❌ WRONG - creates unnecessary tree copies
return entry.withValue(entry.getValue());
// ✅ CORRECT - return original if unchanged
if (shouldModify) {
return entry.withValue(newValue);
}
return super.visitMappingEntry(entry, ctx); // Returns original
```
---
## Complete Example: Multi-Level Navigation
```java
/**
* Find all GitHub Actions steps using deprecated actions
* and update them to newer versions
*/
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
// Match: $.jobs.*.steps[*].uses
JsonPathMatcher usesMatcher = new JsonPathMatcher("$.jobs.*.steps[*].uses");
if (usesMatcher.matches(getCursor()) && "uses".equals(entry.getKey().getValue())) {
if (entry.getValue() instanceof Yaml.Scalar) {
Yaml.Scalar scalar = (Yaml.Scalar) entry.getValue();
String actionRef = scalar.getValue();
// Navigate to parent step to check conditions
Cursor stepCursor = getCursor().getParent(2);
if (stepCursor != null && stepCursor.getValue() instanceof Yaml.Mapping) {
Yaml.Mapping step = (Yaml.Mapping) stepCursor.getValue();
// Check if step has 'if' condition
boolean hasCondition = false;
for (Yaml.Mapping.Entry stepEntry : step.getEntries()) {
if ("if".equals(stepEntry.getKey().getValue())) {
hasCondition = true;
break;
}
}
// Only update unconditional steps
if (!hasCondition && actionRef.contains("@v2")) {
String updated = actionRef.replace("@v2", "@v3");
return entry.withValue(scalar.withValue(updated));
}
}
}
}
return super.visitMappingEntry(entry, ctx);
}
```
---
## Reference Chart
| LST Type | Represents | Common Methods | Notes |
|----------|-----------|----------------|-------|
| `Documents` | File root | `getDocuments()` | Container for multiple docs |
| `Document` | Single doc | `getBlock()` | May have `---` separator |
| `Mapping` | Key-value pairs | `getEntries()` | Like JSON object |
| `Mapping.Entry` | One key-value | `getKey()`, `getValue()` | Basic building block |
| `Sequence` | Array/list | `getEntries()` | Ordered collection |
| `Sequence.Entry` | Array item | `getBlock()` | Wraps actual value |
| `Scalar` | Primitive value | `getValue()`, `getStyle()` | String, number, bool, null |
| `Scalar.Key` | Mapping key | `getValue()` | Always string, no cast needed |
## Additional Resources
- OpenRewrite YAML LST JavaDoc: https://docs.openrewrite.org/reference/yaml-lossless-semantic-trees
- YAML Specification: https://yaml.org/spec/1.2/spec.html
- OpenRewrite Visitor Pattern: https://docs.openrewrite.org/concepts-and-explanations/visitors

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env bash
#
# License Header Script
#
# Adds or updates license headers in Java source files based on the project's
# license template file (gradle/licenseHeader.txt).
#
# Usage:
# ./add_license_header.sh <java-file>
# ./add_license_header.sh src/main/java/com/example/MyRecipe.java
#
# Features:
# - Checks for gradle/licenseHeader.txt in repository root
# - Substitutes ${year} with current year
# - Preserves existing package/import statements
# - Skips files that already have the correct header
#
# Exit codes:
# 0 - Success (header added or already present)
# 1 - License template file not found
# 2 - Invalid arguments or file not found
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get current year
CURRENT_YEAR=$(date +%Y)
# Function to find repository root
find_repo_root() {
local dir="$PWD"
while [[ "$dir" != "/" ]]; do
if [[ -d "$dir/.git" ]] || [[ -f "$dir/build.gradle" ]] || [[ -f "$dir/build.gradle.kts" ]]; then
echo "$dir"
return 0
fi
dir=$(dirname "$dir")
done
echo "$PWD"
return 1
}
# Function to show usage
usage() {
echo "Usage: $0 <java-file>"
echo ""
echo "Add or update license header in a Java source file."
echo ""
echo "Example:"
echo " $0 src/main/java/com/example/MyRecipe.java"
exit 2
}
# Check arguments
if [[ $# -ne 1 ]]; then
usage
fi
JAVA_FILE="$1"
# Validate Java file exists
if [[ ! -f "$JAVA_FILE" ]]; then
echo -e "${RED}Error: File not found: $JAVA_FILE${NC}" >&2
exit 2
fi
# Validate it's a Java file
if [[ ! "$JAVA_FILE" =~ \.java$ ]]; then
echo -e "${RED}Error: Not a Java file: $JAVA_FILE${NC}" >&2
exit 2
fi
# Find repository root and license header file
REPO_ROOT=$(find_repo_root)
LICENSE_HEADER_FILE="$REPO_ROOT/gradle/licenseHeader.txt"
# Check if license header template exists
if [[ ! -f "$LICENSE_HEADER_FILE" ]]; then
echo -e "${YELLOW}Warning: License header template not found at: $LICENSE_HEADER_FILE${NC}" >&2
echo -e "${YELLOW}Skipping license header addition.${NC}" >&2
exit 1
fi
# Read license header template and substitute ${year}
LICENSE_HEADER=$(sed "s/\${year}/$CURRENT_YEAR/g" "$LICENSE_HEADER_FILE")
# Create a temporary file for the new content
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Check if file already has a license header (starts with /* or //)
FIRST_LINE=$(head -n 1 "$JAVA_FILE")
if [[ "$FIRST_LINE" =~ ^/\* ]] || [[ "$FIRST_LINE" =~ ^// ]]; then
# File has some kind of header comment
# Extract everything after the header comment
# Find the end of the comment block
if [[ "$FIRST_LINE" =~ ^/\* ]]; then
# Multi-line comment - find the closing */
LINE_NUM=$(grep -n "\*/" "$JAVA_FILE" | head -n 1 | cut -d: -f1)
if [[ -n "$LINE_NUM" ]]; then
# Extract content after the comment block
tail -n +$((LINE_NUM + 1)) "$JAVA_FILE" > "$TEMP_FILE.body"
else
# No closing found, treat whole file as body
cp "$JAVA_FILE" "$TEMP_FILE.body"
fi
else
# Single-line comments - skip all leading // lines
LINE_NUM=$(grep -n -m 1 "^[^/]" "$JAVA_FILE" | head -n 1 | cut -d: -f1)
if [[ -n "$LINE_NUM" ]]; then
tail -n +$LINE_NUM "$JAVA_FILE" > "$TEMP_FILE.body"
else
# All lines are comments, preserve the file
cp "$JAVA_FILE" "$TEMP_FILE.body"
fi
fi
# Write new header + body
echo "$LICENSE_HEADER" > "$TEMP_FILE"
echo "" >> "$TEMP_FILE" # Add blank line after header
cat "$TEMP_FILE.body" >> "$TEMP_FILE"
rm -f "$TEMP_FILE.body"
echo -e "${GREEN}✓ Updated license header in: $JAVA_FILE${NC}"
else
# No header comment found, prepend the license header
echo "$LICENSE_HEADER" > "$TEMP_FILE"
echo "" >> "$TEMP_FILE" # Add blank line after header
cat "$JAVA_FILE" >> "$TEMP_FILE"
echo -e "${GREEN}✓ Added license header to: $JAVA_FILE${NC}"
fi
# Replace original file with new content
mv "$TEMP_FILE" "$JAVA_FILE"
exit 0

View File

@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
OpenRewrite Recipe Initialization Script
Generates boilerplate code for new OpenRewrite YAML recipes including:
- Recipe class file
- Test file
- Optional declarative YAML recipe
Usage:
python init_recipe.py --name MyRecipe --package com.example.rewrite --description "Recipe description"
"""
import argparse
import os
import sys
from datetime import datetime
from pathlib import Path
def read_license_header():
"""Read license header from gradle/licenseHeader.txt if it exists."""
license_path = Path.cwd()
while license_path != license_path.parent:
license_file = license_path / "gradle" / "licenseHeader.txt"
if license_file.exists():
with open(license_file, 'r') as f:
content = f.read()
# Substitute ${year} with current year
content = content.replace("${year}", str(datetime.now().year))
return content + "\n"
license_path = license_path.parent
return ""
def to_snake_case(name):
"""Convert PascalCase to snake_case."""
result = []
for i, char in enumerate(name):
if char.isupper() and i > 0:
result.append('_')
result.append(char.lower())
return ''.join(result)
def generate_recipe_class(name, package, description, license_header):
"""Generate the recipe class file content."""
return f"""{license_header}package {package};
import org.openrewrite.*;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
public class {name} extends Recipe {{
@Override
public String getDisplayName() {{
return "{name}";
}}
@Override
public String getDescription() {{
return "{description}";
}}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {{
return new YamlIsoVisitor<ExecutionContext>() {{
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{
// TODO: Implement recipe logic
return super.visitMappingEntry(entry, ctx);
}}
}};
}}
}}
"""
def generate_parameterized_recipe_class(name, package, description, license_header):
"""Generate a parameterized recipe class file content."""
return f"""{license_header}package {package};
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
@Value
@EqualsAndHashCode(callSuper = false)
public class {name} extends Recipe {{
@Option(
displayName = "Parameter name",
description = "Description of the parameter",
example = "example-value"
)
String parameterName;
@Override
public String getDisplayName() {{
return "{name}";
}}
@Override
public String getDescription() {{
return "{description}";
}}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {{
return new YamlIsoVisitor<ExecutionContext>() {{
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {{
// TODO: Implement recipe logic using parameterName
return super.visitMappingEntry(entry, ctx);
}}
}};
}}
}}
"""
def generate_test_class(name, package, license_header):
"""Generate the test class file content."""
return f"""{license_header}package {package};
import org.junit.jupiter.api.Test;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
import static org.openrewrite.yaml.Assertions.yaml;
class {name}Test implements RewriteTest {{
@Override
public void defaults(RecipeSpec spec) {{
spec.recipe(new {name}());
}}
@Test
void basicTransformation() {{
rewriteRun(
yaml(
\"\"\"
# Before YAML
key: old-value
\"\"\",
\"\"\"
# After YAML
key: new-value
\"\"\"
)
);
}}
@Test
void doesNotChangeUnrelatedYaml() {{
rewriteRun(
yaml(
\"\"\"
unrelated: value
\"\"\"
)
);
}}
@Test
void handlesEdgeCases() {{
rewriteRun(
yaml(
\"\"\"
# Empty value
key:
# Null value
key2: null
\"\"\"
)
);
}}
}}
"""
def generate_declarative_recipe(name, package, description):
"""Generate declarative YAML recipe content."""
return f"""---
type: specs.openrewrite.org/v1beta/recipe
name: {package}.{name}
displayName: {name}
description: {description}
recipeList:
- org.openrewrite.yaml.search.FindKey:
keyPath: $.some.path
- org.openrewrite.yaml.ChangeValue:
keyPath: $.some.path
value: newValue
"""
def create_file(path, content):
"""Create a file with the given content."""
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'w') as f:
f.write(content)
print(f"Created: {path}")
def main():
parser = argparse.ArgumentParser(
description="Initialize a new OpenRewrite YAML recipe with boilerplate code"
)
parser.add_argument(
"--name",
required=True,
help="Recipe class name (PascalCase, e.g., UpdateGitHubActions)"
)
parser.add_argument(
"--package",
required=True,
help="Package name (e.g., com.example.rewrite)"
)
parser.add_argument(
"--description",
required=True,
help="Recipe description"
)
parser.add_argument(
"--parameterized",
action="store_true",
help="Generate parameterized recipe with @Option annotation"
)
parser.add_argument(
"--declarative",
action="store_true",
help="Also generate declarative YAML recipe template"
)
parser.add_argument(
"--output-dir",
default=".",
help="Output directory (default: current directory)"
)
args = parser.parse_args()
# Validate recipe name
if not args.name[0].isupper():
print("Error: Recipe name must start with uppercase letter (PascalCase)")
sys.exit(1)
# Read license header
license_header = read_license_header()
if license_header:
print(f"Found license header (will use year {datetime.now().year})")
# Convert package to path
package_path = args.package.replace('.', '/')
base_path = Path(args.output_dir)
# Generate file paths
recipe_path = base_path / "src" / "main" / "java" / package_path / f"{args.name}.java"
test_path = base_path / "src" / "test" / "java" / package_path / f"{args.name}Test.java"
# Generate recipe class
if args.parameterized:
recipe_content = generate_parameterized_recipe_class(
args.name, args.package, args.description, license_header
)
else:
recipe_content = generate_recipe_class(
args.name, args.package, args.description, license_header
)
# Generate test class
test_content = generate_test_class(args.name, args.package, license_header)
# Create files
create_file(recipe_path, recipe_content)
create_file(test_path, test_content)
# Generate declarative recipe if requested
if args.declarative:
yaml_name = to_snake_case(args.name)
yaml_path = base_path / "src" / "main" / "resources" / "META-INF" / "rewrite" / f"{yaml_name}.yml"
yaml_content = generate_declarative_recipe(args.name, args.package, args.description)
create_file(yaml_path, yaml_content)
print("\n✓ Recipe initialization complete!")
print("\nNext steps:")
print("1. Write failing tests in the test file")
print("2. Implement the recipe logic")
print("3. Run tests to verify: ./gradlew test")
if args.declarative:
print("4. Choose between imperative (Java) or declarative (YAML) approach")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
Validate OpenRewrite recipe structure, naming conventions, and Java compatibility.
This script checks:
1. Recipe class structure (extends Recipe, has required annotations)
2. Required methods (getDisplayName, getDescription, getVisitor)
3. Naming conventions (package, class name, display name)
4. Java compatibility (can compile with Java 8)
5. YAML recipe format (for declarative recipes)
Usage:
python validate_recipe.py <path-to-recipe>
python validate_recipe.py <path-to-recipe> --java-version 8
python validate_recipe.py <path-to-recipe> --no-compile
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import List, Tuple
class Colors:
"""ANSI color codes for terminal output"""
GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
BLUE = '\033[94m'
RESET = '\033[0m'
BOLD = '\033[1m'
def print_success(message: str):
print(f"{Colors.GREEN}{Colors.RESET} {message}")
def print_error(message: str):
print(f"{Colors.RED}{Colors.RESET} {message}")
def print_warning(message: str):
print(f"{Colors.YELLOW}{Colors.RESET} {message}")
def print_info(message: str):
print(f"{Colors.BLUE}{Colors.RESET} {message}")
def check_java_file_structure(content: str, file_path: Path) -> List[str]:
"""Validate Java recipe file structure"""
errors = []
# Check for Recipe class
if not re.search(r'class\s+\w+\s+extends\s+Recipe', content):
errors.append("Recipe class must extend Recipe")
# Check for @Value annotation (for immutability)
if '@Value' not in content:
print_warning(f"Consider using @Value annotation for immutability")
# Check for @EqualsAndHashCode
if '@EqualsAndHashCode' not in content:
print_warning("Consider using @EqualsAndHashCode(callSuper = false)")
# Check for required methods
if 'getDisplayName()' not in content:
errors.append("Recipe must override getDisplayName()")
if 'getDescription()' not in content:
errors.append("Recipe must override getDescription()")
if 'getVisitor()' not in content:
errors.append("Recipe must override getVisitor()")
# Check for proper return in getVisitor
if 'getVisitor()' in content:
visitor_match = re.search(r'public\s+TreeVisitor<\?.*?>\s+getVisitor\([^)]*\)\s*{([^}]+)}', content, re.DOTALL)
if visitor_match:
visitor_body = visitor_match.group(1)
if 'new ' not in visitor_body:
print_warning("getVisitor() should return a NEW instance (no caching)")
# Check display name ends with period
display_name_match = re.search(r'getDisplayName\(\)\s*{\s*return\s*"([^"]+)"', content)
if display_name_match:
display_name = display_name_match.group(1)
if not display_name.endswith('.') and not display_name.endswith('!') and not display_name.endswith('?'):
print_warning(f"Display name should end with a period: '{display_name}'")
# Check for @Option annotations on parameters
option_count = content.count('@Option')
if option_count > 0:
# Check that options have example
for match in re.finditer(r'@Option\([^)]+\)', content):
option = match.group(0)
if 'example' not in option:
print_warning("@Option should include an example parameter")
return errors
def check_naming_conventions(content: str, file_path: Path) -> List[str]:
"""Check naming conventions"""
errors = []
# Extract package name
package_match = re.search(r'package\s+([\w.]+);', content)
if package_match:
package = package_match.group(1)
if package.startswith('com.yourorg') or package.startswith('com.example'):
print_warning(f"Update placeholder package name: {package}")
# Extract class name
class_match = re.search(r'class\s+(\w+)\s+extends\s+Recipe', content)
if class_match:
class_name = class_match.group(1)
# Check naming convention (VerbNoun pattern)
if not re.match(r'^[A-Z][a-z]+[A-Z]', class_name):
print_warning(f"Recipe class name should follow VerbNoun pattern: {class_name}")
# Check file name matches class name
expected_filename = f"{class_name}.java"
if file_path.name != expected_filename:
errors.append(f"File name {file_path.name} does not match class name {expected_filename}")
return errors
def check_java8_compatibility_patterns(content: str) -> List[str]:
"""Check for Java 8 incompatible patterns"""
warnings = []
# Check for var keyword
if re.search(r'\bvar\b', content):
warnings.append("Found 'var' keyword - not available in Java 8")
# Check for text blocks
if '"""' in content:
warnings.append("Found text blocks (triple quotes) - not available in Java 8")
# Check for switch expressions
if re.search(r'switch\s*\([^)]+\)\s*{[^}]*->', content):
warnings.append("Found switch expression - not available in Java 8")
# Check for pattern matching
if re.search(r'instanceof\s+\w+\s+\w+\s+&&', content):
warnings.append("Found pattern matching in instanceof - not available in Java 8")
# Check for record keyword
if re.search(r'\brecord\s+\w+', content):
warnings.append("Found record - not available in Java 8")
return warnings
def compile_with_javac(file_path: Path, java_version: int = 8) -> Tuple[bool, str]:
"""Try to compile the file with javac"""
try:
# Create a temporary directory for compilation
import tempfile
with tempfile.TemporaryDirectory() as tmpdir:
result = subprocess.run(
['javac', '-source', str(java_version), '-target', str(java_version),
'-d', tmpdir, str(file_path)],
capture_output=True,
text=True,
timeout=10
)
if result.returncode == 0:
return True, ""
else:
return False, result.stderr
except FileNotFoundError:
return False, "javac not found in PATH"
except subprocess.TimeoutExpired:
return False, "Compilation timed out"
except Exception as e:
return False, str(e)
def validate_yaml_recipe(content: str, file_path: Path) -> List[str]:
"""Validate YAML recipe format"""
errors = []
# Check for required fields
if 'type: specs.openrewrite.org/v1beta/recipe' not in content:
errors.append("YAML recipe must have 'type: specs.openrewrite.org/v1beta/recipe'")
if not re.search(r'^name:\s+[\w.]+', content, re.MULTILINE):
errors.append("YAML recipe must have 'name' field")
if not re.search(r'^displayName:', content, re.MULTILINE):
errors.append("YAML recipe must have 'displayName' field")
if not re.search(r'^description:', content, re.MULTILINE):
errors.append("YAML recipe must have 'description' field")
if not re.search(r'^recipeList:', content, re.MULTILINE):
errors.append("YAML recipe must have 'recipeList' field")
# Check naming convention
name_match = re.search(r'^name:\s+([\w.]+)', content, re.MULTILINE)
if name_match:
name = name_match.group(1)
if not re.match(r'^[\w.]+\.[\w.]+$', name):
print_warning(f"Recipe name should be fully qualified: {name}")
if 'yourorg' in name.lower() or 'example' in name.lower():
print_warning(f"Update placeholder recipe name: {name}")
return errors
def validate_recipe(file_path: Path, java_version: int = 8, skip_compile: bool = False) -> bool:
"""Validate a recipe file"""
print(f"\n{Colors.BOLD}Validating: {file_path}{Colors.RESET}\n")
if not file_path.exists():
print_error(f"File not found: {file_path}")
return False
# Read file content
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print_error(f"Error reading file: {e}")
return False
errors = []
warnings = []
# Determine file type
if file_path.suffix == '.java':
print_info("Checking Java recipe structure...")
errors.extend(check_java_file_structure(content, file_path))
print_info("Checking naming conventions...")
errors.extend(check_naming_conventions(content, file_path))
print_info(f"Checking Java {java_version} compatibility...")
java8_warnings = check_java8_compatibility_patterns(content)
warnings.extend(java8_warnings)
# Try to compile if not skipped
if not skip_compile:
print_info("Attempting compilation...")
success, compile_error = compile_with_javac(file_path, java_version)
if not success:
if "javac not found" in compile_error:
print_warning("javac not found - skipping compilation check")
else:
errors.append(f"Compilation failed:\n{compile_error}")
else:
print_success("Compilation successful")
elif file_path.suffix in ['.yml', '.yaml']:
print_info("Checking YAML recipe format...")
errors.extend(validate_yaml_recipe(content, file_path))
else:
print_error(f"Unsupported file type: {file_path.suffix}")
return False
# Print results
print()
if errors:
print(f"{Colors.RED}{Colors.BOLD}ERRORS:{Colors.RESET}")
for error in errors:
print_error(error)
print()
if warnings:
print(f"{Colors.YELLOW}{Colors.BOLD}WARNINGS:{Colors.RESET}")
for warning in warnings:
print_warning(warning)
print()
if not errors and not warnings:
print(f"{Colors.GREEN}{Colors.BOLD}✓ All checks passed!{Colors.RESET}\n")
return True
elif not errors:
print(f"{Colors.YELLOW}{Colors.BOLD}⚠ Validation passed with warnings{Colors.RESET}\n")
return True
else:
print(f"{Colors.RED}{Colors.BOLD}✗ Validation failed{Colors.RESET}\n")
return False
def main():
parser = argparse.ArgumentParser(
description='Validate OpenRewrite recipe files',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
Examples:
# Validate a Java recipe
python validate_recipe.py src/main/java/com/example/MyRecipe.java
# Validate with Java 11
python validate_recipe.py MyRecipe.java --java-version 11
# Skip compilation check
python validate_recipe.py MyRecipe.java --no-compile
# Validate a YAML recipe
python validate_recipe.py src/main/resources/META-INF/rewrite/my-recipe.yml
'''
)
parser.add_argument('path', type=str, help='Path to recipe file')
parser.add_argument('--java-version', type=int, default=8,
help='Target Java version (default: 8)')
parser.add_argument('--no-compile', action='store_true',
help='Skip compilation check')
args = parser.parse_args()
file_path = Path(args.path)
success = validate_recipe(file_path, args.java_version, args.no_compile)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,13 @@
Copyright ${year} the original author or authors.
<p>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
<p>
https://www.apache.org/licenses/LICENSE-2.0
<p>
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,122 @@
---
# OpenRewrite Declarative Recipe Template
# Save this file in: src/main/resources/META-INF/rewrite/
# Required: Recipe type identifier
type: specs.openrewrite.org/v1beta/recipe
# Required: Fully qualified recipe name (convention: com.yourorg.RecipeName)
name: com.yourorg.YourRecipeName
# Required: Human-readable recipe name (sentence case, end with period if sentence)
displayName: Your recipe display name
# Required: Clear description of what the recipe does
description: A clear description of what this recipe accomplishes. This can span multiple lines and should explain the purpose and effect of running this recipe.
# Optional: Tags for categorization and searchability
tags:
- category1
- category2
- framework-name
# Optional: Estimated time saved by this recipe (in ISO-8601 duration format)
# estimatedEffortPerOccurrence: PT5M
# Optional: Set to true if this recipe requires multiple execution cycles
# causesAnotherCycle: true
# Optional: Define preconditions that files must meet to run this recipe
# preconditions:
# - org.openrewrite.java.search.UsesType:
# fullyQualifiedTypeName: com.example.TargetType
# Required: List of recipes to execute (in order)
recipeList:
# Example: Recipe with no parameters
- org.openrewrite.java.format.AutoFormat
# Example: Recipe with parameters
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: old.package.OldType
newFullyQualifiedTypeName: new.package.NewType
# Optional parameter
ignoreDefinition: false
# Example: Another recipe with parameters
- org.openrewrite.java.ChangePackage:
oldPackageName: com.old.package
newPackageName: com.new.package
recursive: true
# Example: Add a dependency (only if type is used)
- org.openrewrite.java.dependencies.AddDependency:
groupId: org.example
artifactId: example-library
version: latest.release
onlyIfUsing: com.example.SomeClass
configuration: implementation
# Example: Maven-specific recipe
- org.openrewrite.maven.UpgradePluginVersion:
groupId: org.apache.maven.plugins
artifactId: maven-compiler-plugin
newVersion: 3.11.0
# Add your recipes here
# - com.yourorg.AnotherRecipe
# - com.yourorg.YetAnotherRecipe:
# parameter: value
---
# You can define multiple recipes in the same file by using '---' separator
# type: specs.openrewrite.org/v1beta/recipe
# name: com.yourorg.AnotherRecipe
# displayName: Another recipe
# description: Description of the second recipe.
# recipeList:
# - ...
# Common Recipe References:
#
# Java Type Changes:
# - org.openrewrite.java.ChangeType
# - org.openrewrite.java.ChangePackage
# - org.openrewrite.java.ChangeMethodName
# - org.openrewrite.java.ChangeFieldName
#
# Dependency Management:
# - org.openrewrite.java.dependencies.AddDependency
# - org.openrewrite.java.dependencies.RemoveDependency
# - org.openrewrite.java.dependencies.ChangeDependency
# - org.openrewrite.java.dependencies.UpgradeDependencyVersion
#
# Maven:
# - org.openrewrite.maven.UpgradePluginVersion
# - org.openrewrite.maven.UpgradeDependencyVersion
# - org.openrewrite.maven.ChangePropertyValue
#
# Static Analysis:
# - org.openrewrite.staticanalysis.CommonStaticAnalysis
# - org.openrewrite.staticanalysis.FinalizeLocalVariables
# - org.openrewrite.staticanalysis.RemoveUnusedImports
#
# Testing:
# - org.openrewrite.java.testing.junit5.JUnit4to5Migration
# - org.openrewrite.java.testing.junit5.AssertToAssertions
#
# Spring:
# - org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
# - org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7
#
# Formatting:
# - org.openrewrite.java.format.AutoFormat
# - org.openrewrite.java.format.RemoveUnusedImports
# - org.openrewrite.java.format.TabsAndIndents
#
# Search recipes (use these in preconditions):
# - org.openrewrite.java.search.UsesType
# - org.openrewrite.java.search.UsesMethod
# - org.openrewrite.java.search.FindTypes
# - org.openrewrite.java.search.FindMethods

View File

@@ -0,0 +1,200 @@
package com.yourorg;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.jspecify.annotations.NonNull;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.JavaTemplate;
import org.openrewrite.java.tree.J;
/**
* TODO: Add recipe description
*
* Example usage in YAML:
* ```yaml
* type: specs.openrewrite.org/v1beta/recipe
* name: com.yourorg.MyRecipeGroup
* recipeList:
* - com.yourorg.TemplateRecipe:
* parameterName: value
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class TemplateRecipe extends Recipe {
/**
* TODO: Add options for recipe configuration
* Each option becomes a parameter that can be configured in YAML
*/
@Option(
displayName = "Parameter Display Name",
description = "A clear description of what this parameter does.",
example = "com.example.ExampleValue"
)
@NonNull
String parameterName;
// Add more @Option fields as needed
// @Option(displayName = "Another Parameter", ...)
// String anotherParameter;
/**
* All recipes must be serializable via Jackson.
* Use @JsonCreator and @JsonProperty annotations.
*/
@JsonCreator
public TemplateRecipe(
@NonNull @JsonProperty("parameterName") String parameterName
) {
this.parameterName = parameterName;
}
@Override
public String getDisplayName() {
return "Your recipe display name";
}
@Override
public String getDescription() {
return "A clear description of what this recipe does. Use sentence case and end with a period.";
}
/**
* Optional: Add preconditions to improve performance
* Recipes only run on files that match these conditions
*/
// @Override
// public TreeVisitor<?, ExecutionContext> getVisitor() {
// return Preconditions.check(
// new UsesType<>("com.example.SomeType", true),
// new TemplateRecipeVisitor()
// );
// }
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// IMPORTANT: Always return a NEW instance (no caching)
return new TemplateRecipeVisitor();
}
/**
* The visitor implements the actual transformation logic.
* Use JavaIsoVisitor when always returning the same LST type.
*/
public class TemplateRecipeVisitor extends JavaIsoVisitor<ExecutionContext> {
/**
* Optional: Create JavaTemplates for complex code generation
* Templates are parsed once and can be reused
*/
// private final JavaTemplate template = JavaTemplate
// .builder("your.code.template(#{any(String)})")
// .imports("your.imports.Here")
// .build();
/**
* Override visit methods for the LST elements you want to transform.
* Common visit methods:
* - visitCompilationUnit() - entire file
* - visitClassDeclaration() - class declarations
* - visitMethodDeclaration() - method declarations
* - visitMethodInvocation() - method calls
* - visitVariableDeclarations() - variable declarations
* - visitAssignment() - assignments
* - visitBinary() - binary operations
* - visitImport() - imports
*/
@Override
public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
// Step 1: ALWAYS call super to traverse the subtree
J.ClassDeclaration cd = super.visitClassDeclaration(classDecl, ctx);
// Step 2: Check if this element needs to be changed
// DO NO HARM: If unsure, return unchanged
if (!shouldChange(cd)) {
return cd;
}
// Step 3: Make your changes
// Never mutate the LST - always use .withX() methods
cd = makeYourChanges(cd, ctx);
// Step 4: Optional - chain other visitors
// doAfterVisit(new SomeOtherRecipe().getVisitor());
// Step 5: Optional - add/remove imports
// maybeAddImport("java.util.List");
// maybeRemoveImport("old.package.Type");
return cd;
}
/**
* Helper method to determine if changes are needed
*/
private boolean shouldChange(J.ClassDeclaration classDecl) {
// TODO: Implement your logic
// Check if change is necessary and safe
// Example: Check if class matches criteria
// Check for null type (avoid NPE)
if (classDecl.getType() == null) {
return false;
}
// Example: Check fully qualified name
// if (!classDecl.getType().getFullyQualifiedName().equals(parameterName)) {
// return false;
// }
// Example: Check if change already exists
// if (alreadyHasTheChange(classDecl)) {
// return false;
// }
return true;
}
/**
* Helper method to perform the transformation
*/
private J.ClassDeclaration makeYourChanges(J.ClassDeclaration classDecl, ExecutionContext ctx) {
// TODO: Implement your transformation logic
// Example: Using a JavaTemplate
// classDecl = classDecl.withBody(
// template.apply(
// new Cursor(getCursor(), classDecl.getBody()),
// classDecl.getBody().getCoordinates().lastStatement(),
// someParameter
// )
// );
// Example: Modifying using LST methods
// classDecl = classDecl.withName(classDecl.getName().withSimpleName("NewName"));
// Example: Using ListUtils to avoid mutation
// classDecl = classDecl.withModifiers(
// ListUtils.concat(classDecl.getModifiers(), newModifier)
// );
return classDecl;
}
/**
* Optional: Example of another visit method
*/
// @Override
// public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
// J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
//
// // Your logic here
//
// return m;
// }
}
}

View File

@@ -0,0 +1,292 @@
package com.yourorg;
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.test.RecipeSpec;
import org.openrewrite.test.RewriteTest;
import static org.openrewrite.java.Assertions.java;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test class for TemplateRecipe
*
* Best Practices:
* - Test both cases where changes ARE made and where they ARE NOT made
* - Test edge cases and boundary conditions
* - Use meaningful test names that describe what is being tested
* - Add @DocumentExample to one test to generate documentation
*/
class TemplateRecipeTest implements RewriteTest {
/**
* Configure defaults for all tests in this class.
* This is called before each test method.
*/
@Override
public void defaults(RecipeSpec spec) {
// Set the recipe to test with default parameters
spec.recipe(new TemplateRecipe("parameter-value"));
// Optional: Configure parser with classpath dependencies
// spec.parser(JavaParser.fromJavaVersion()
// .classpath("library-name")
// .logCompilationWarningsAndErrors(true));
// Optional: Configure for specific Java version
// spec.allSources(s -> s.markers(javaVersion(17)));
}
/**
* Test that the recipe makes the expected change.
* @DocumentExample marks this as the primary example for documentation.
*/
@DocumentExample
@Test
void makesExpectedChange() {
rewriteRun(
// First argument: before state
// Second argument: after state
java(
"""
package com.example;
class BeforeExample {
// TODO: Add code that should be changed
}
""",
"""
package com.example;
class AfterExample {
// TODO: Add expected code after transformation
}
"""
)
);
}
/**
* Test that the recipe does NOT make changes when they are not needed.
* This is crucial - recipes must not make unnecessary changes!
*/
@Test
void doesNotChangeWhenNotNeeded() {
rewriteRun(
java(
"""
package com.example;
class AlreadyCorrect {
// TODO: Add code that should NOT be changed
}
"""
// No second argument means we expect NO changes
)
);
}
/**
* Test edge case or boundary condition
*/
@Test
void handlesEdgeCase() {
rewriteRun(
java(
"""
package com.example;
class EdgeCase {
// TODO: Add edge case scenario
}
""",
"""
package com.example;
class EdgeCase {
// TODO: Add expected result for edge case
}
"""
)
);
}
/**
* Example: Test with multiple files
* Demonstrates that some files change and others don't
*/
@Test
void handlesMultipleFiles() {
rewriteRun(
// First file: should change
java(
"""
package com.example;
class ShouldChange {
}
""",
"""
package com.example;
class DidChange {
}
"""
),
// Second file: should NOT change
java(
"""
package com.example;
class ShouldNotChange {
}
"""
)
);
}
/**
* Example: Test with custom recipe spec for this specific test
*/
@Test
void testWithCustomConfiguration() {
rewriteRun(
// Customize the spec for just this test
spec -> spec
.recipe(new TemplateRecipe("different-parameter"))
// Add specific classpath for this test
// .parser(JavaParser.fromJavaVersion().classpath("specific-library"))
,
java(
"""
package com.example;
class Example {
}
""",
"""
package com.example;
class Example {
// Changes based on different parameter
}
"""
)
);
}
/**
* Example: Test with afterRecipe callback for additional assertions
*/
@Test
void testWithCallback() {
rewriteRun(
java(
"""
package com.example;
class Example {
}
""",
"""
package com.example;
class Example {
// Some change
}
""",
// Callback to perform additional assertions after recipe runs
spec -> spec.afterRecipe(cu -> {
// Custom assertions on the compilation unit
assertThat(cu.getClasses()).hasSize(1);
// Add more assertions as needed
})
)
);
}
/**
* Example: Test for declarative YAML recipe
*/
// @Test
// void testDeclarativeRecipe() {
// rewriteRun(
// spec -> spec
// .recipeFromResources("com.yourorg.TemplateRecipe")
// .parser(JavaParser.fromJavaVersion()
// .classpath("dependencies-needed-for-before-code")),
// java(
// """
// package com.example;
//
// class Before {
// }
// """,
// """
// package com.example;
//
// class After {
// }
// """
// )
// );
// }
/**
* Example: Test with specific file path
*/
// @Test
// void testWithSpecificPath() {
// rewriteRun(
// java(
// """
// server.port=8080
// """,
// """
// server.port=80
// """,
// spec -> spec.path("src/main/resources/application.properties")
// )
// );
// }
/**
* Example: Test with Java version marker
*/
// @Test
// void testWithJavaVersion() {
// rewriteRun(
// spec -> spec.allSources(s -> s.markers(javaVersion(17))),
// java(
// """
// package com.example;
//
// class Example {
// // Java 17 specific code
// }
// """
// )
// );
// }
/**
* Example: Test combining different source file types
*/
// @Test
// void testMultipleFileTypes() {
// rewriteRun(
// java(
// """
// package com.example;
// class Example { }
// """
// ),
// // You can mix java(), xml(), yaml(), properties(), etc.
// // yaml(
// // """
// // key: value
// // """
// // )
// );
// }
}

View File

@@ -0,0 +1,134 @@
package com.yourorg;
import com.google.errorprone.refaster.annotation.AfterTemplate;
import com.google.errorprone.refaster.annotation.BeforeTemplate;
import org.openrewrite.java.template.RecipeDescriptor;
/**
* Refaster template for simple expression/statement replacements.
*
* Refaster templates provide a middle ground between declarative YAML and imperative Java recipes:
* - Faster than imperative recipes
* - Type-aware matching
* - Concise syntax
* - Good for API migrations
*
* Usage:
* 1. Define @BeforeTemplate with the code pattern to match
* 2. Define @AfterTemplate with the replacement code
* 3. OpenRewrite generates a recipe that performs the transformation
*
* Example usage in YAML:
* ```yaml
* type: specs.openrewrite.org/v1beta/recipe
* name: com.yourorg.MyRefasterRecipe
* recipeList:
* - com.yourorg.TemplateRefaster
* ```
*/
@RecipeDescriptor(
name = "Your Refaster recipe name",
description = "Clear description of what this Refaster template accomplishes."
)
public class TemplateRefaster {
/**
* Example 1: Simple method call replacement
* Replaces StringUtils.equals() with Objects.equals()
*/
public static class ReplaceStringUtilsEquals {
@BeforeTemplate
boolean before(String s1, String s2) {
return org.apache.commons.lang3.StringUtils.equals(s1, s2);
}
@AfterTemplate
boolean after(String s1, String s2) {
return java.util.Objects.equals(s1, s2);
}
}
/**
* Example 2: Expression replacement with type awareness
* Replaces new ArrayList<>() with List.of() for immutable lists (Java 9+)
*/
public static class ReplaceArrayListWithListOf {
@BeforeTemplate
<T> java.util.List<T> before() {
return new java.util.ArrayList<>();
}
@AfterTemplate
<T> java.util.List<T> after() {
return java.util.List.of();
}
}
/**
* Example 3: Statement replacement
* Replaces traditional for loop with enhanced for loop
*/
public static class ReplaceTraditionalForWithEnhanced {
@BeforeTemplate
void before(java.util.List<String> items) {
for (int i = 0; i < items.size(); i++) {
String item = items.get(i);
System.out.println(item);
}
}
@AfterTemplate
void after(java.util.List<String> items) {
for (String item : items) {
System.out.println(item);
}
}
}
/**
* Example 4: API migration with different parameters
* Migrates from old API to new API with parameter reordering
*/
public static class MigrateOldApiToNew {
@BeforeTemplate
void before(String value, int timeout) {
com.oldapi.Client.connect(value, timeout);
}
@AfterTemplate
void after(String value, int timeout) {
com.newapi.Client.connect(timeout, value);
}
}
/**
* TODO: Add your Refaster templates here
*
* Tips:
* - Keep templates simple - complex logic should use imperative recipes
* - Use type parameters for generic matching (<T>, <S>, etc.)
* - Method names (before/after) can be anything - only annotations matter
* - Return types and parameters must match between before and after for type safety
* - You can have multiple nested template classes in one file
*/
public static class YourRefasterTemplate {
/**
* Define what code pattern to match
*/
@BeforeTemplate
void before() {
// TODO: Add the code pattern you want to match and replace
// Example: someOldMethod()
}
/**
* Define what to replace it with
*/
@AfterTemplate
void after() {
// TODO: Add the replacement code
// Example: someNewMethod()
}
}
}