Initial commit
This commit is contained in:
257
skills/recipe-writer/examples/example-declarative-migration.yml
Normal file
257
skills/recipe-writer/examples/example-declarative-migration.yml
Normal 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
|
||||
186
skills/recipe-writer/examples/example-say-hello-recipe.java
Normal file
186
skills/recipe-writer/examples/example-say-hello-recipe.java
Normal 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
|
||||
*/
|
||||
294
skills/recipe-writer/examples/example-scanning-recipe.java
Normal file
294
skills/recipe-writer/examples/example-scanning-recipe.java
Normal 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
|
||||
*/
|
||||
282
skills/recipe-writer/examples/example-yaml-github-actions.java
Normal file
282
skills/recipe-writer/examples/example-yaml-github-actions.java
Normal 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.
|
||||
*/
|
||||
Reference in New Issue
Block a user