Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "openrewrite",
|
||||||
|
"description": "Claude Code plugin",
|
||||||
|
"version": "0.0.0-2025.11.28",
|
||||||
|
"author": {
|
||||||
|
"name": "Scott",
|
||||||
|
"email": "scott.jungling@gmail.com"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
"./skills"
|
||||||
|
]
|
||||||
|
}
|
||||||
117
plugin.lock.json
Normal file
117
plugin.lock.json
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||||
|
"pluginId": "gh:sjungling/claude-plugins:plugins/openrewrite",
|
||||||
|
"normalized": {
|
||||||
|
"repo": null,
|
||||||
|
"ref": "refs/tags/v20251128.0",
|
||||||
|
"commit": "59d3c9205611bca47a9101d92d65052375cf4ed2",
|
||||||
|
"treeHash": "c6d9af92a961ff7c5242b74ad3322250c29c8c6f973a6863f2e4e59effb0bc6c",
|
||||||
|
"generatedAt": "2025-11-28T10:28:23.454536Z",
|
||||||
|
"toolVersion": "publish_plugins.py@0.2.0"
|
||||||
|
},
|
||||||
|
"origin": {
|
||||||
|
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||||
|
"branch": "master",
|
||||||
|
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||||
|
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||||
|
},
|
||||||
|
"manifest": {
|
||||||
|
"name": "openrewrite",
|
||||||
|
"description": "Claude Code plugin",
|
||||||
|
"version": null
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "README.md",
|
||||||
|
"sha256": "cbed6a9ebeb3cd9ddd3110880b2b808291888e41e198efd9fbf068289bb2964f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": ".claude-plugin/plugin.json",
|
||||||
|
"sha256": "dd229ba7883bf4e7d96961bb61f4c40a1ffa477277633f1ce19133758a569dbd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/SKILL.md",
|
||||||
|
"sha256": "ea021984f43c33e7006699e202d594b70f9c181eb78529e2b9e61bf30afeffad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/references/yaml-lst-reference.md",
|
||||||
|
"sha256": "0caec5f36e881788ede3a812c1282b809f47408eb07c5a26772e792e695ed43e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/references/checklist-recipe-development.md",
|
||||||
|
"sha256": "98966975f7e17b603d2fbb9c42935ad4f00162fc486841cf05b53a2b2013414e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/references/java-lst-reference.md",
|
||||||
|
"sha256": "18e9a8c8c44371fd5833dc360674a0bb40ec4b235dd165b342b0ab374a5bc1b7"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/references/common-patterns.md",
|
||||||
|
"sha256": "3db2b41ff3a520267be613a4dc5b03c394cd301d5a5079e019c2fcae8a46af2b"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/references/trait-implementation-guide.md",
|
||||||
|
"sha256": "01cf52ae5563aa7e2bf7444382a73d656acd972e4bdf080db4c7465fbeee475c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/references/jsonpath-patterns.md",
|
||||||
|
"sha256": "a0c9d1d442bacfae7f826453500e7ad972e91f27856b12e9a6a9d27d268447d4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/examples/example-say-hello-recipe.java",
|
||||||
|
"sha256": "12b384c30934e1ff2d874cc6f65a10d0cbd699453cb079ebc379ec9d7949f120"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/examples/example-yaml-github-actions.java",
|
||||||
|
"sha256": "1bde1be463c3c592b932aec72adfd4ceb14b5407c17c30de323d8611db128e18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/examples/example-scanning-recipe.java",
|
||||||
|
"sha256": "c87fe6c1dc09ceda8ba75e4099504eddd89da1db52cc17ee2bb79de96dc48809"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/examples/example-declarative-migration.yml",
|
||||||
|
"sha256": "ec94034dea27e0e647fe66f4dd6420c57fb1b64d926abe187232d81b69bdb388"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/scripts/validate_recipe.py",
|
||||||
|
"sha256": "c151691b679d3fed7acc0c48e72dd484288c895af1aeaf177de84bb5c13b7909"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/scripts/add_license_header.sh",
|
||||||
|
"sha256": "68e49928677590a3d2b3019b9db1241c904d46c6614eccbe2e700af6a86c4d04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/scripts/init_recipe.py",
|
||||||
|
"sha256": "ca1799803b8ebc95a8f61cdf35d953b3129d715027a3c44d9eecd88c0c6a4b4f"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/templates/template-declarative-recipe.yml",
|
||||||
|
"sha256": "a149b6067cf18e294b97491acc3ff69ef4583adf345e933743f64409185f67c2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/templates/template-recipe-test.java",
|
||||||
|
"sha256": "2d14970ad7fc2bed1116d597acc210b58f3f49449b43cfd175b840aa90e28f9c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/templates/template-imperative-recipe.java",
|
||||||
|
"sha256": "62d03d6bc123da32a6b15a8db3769cbf550554933d248b38175901b76ac8bd6c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/templates/license-header.txt",
|
||||||
|
"sha256": "a6715cfa80f257d614b735b70b703bea21b5ddbe006e1532c6354d4542d4a032"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "skills/recipe-writer/templates/template-refaster-template.java",
|
||||||
|
"sha256": "cc8cef2c45479f5cad81494013532a2cfe5813c1219910bc2c8aa15ffe2a07d6"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dirSha256": "c6d9af92a961ff7c5242b74ad3322250c29c8c6f973a6863f2e4e59effb0bc6c"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"scannedAt": null,
|
||||||
|
"scannerVersion": null,
|
||||||
|
"flags": []
|
||||||
|
}
|
||||||
|
}
|
||||||
1198
skills/recipe-writer/SKILL.md
Normal file
1198
skills/recipe-writer/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
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.
|
||||||
|
*/
|
||||||
198
skills/recipe-writer/references/checklist-recipe-development.md
Normal file
198
skills/recipe-writer/references/checklist-recipe-development.md
Normal 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
|
||||||
356
skills/recipe-writer/references/common-patterns.md
Normal file
356
skills/recipe-writer/references/common-patterns.md
Normal 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);
|
||||||
|
```
|
||||||
420
skills/recipe-writer/references/java-lst-reference.md
Normal file
420
skills/recipe-writer/references/java-lst-reference.md
Normal 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
|
||||||
524
skills/recipe-writer/references/jsonpath-patterns.md
Normal file
524
skills/recipe-writer/references/jsonpath-patterns.md
Normal 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/
|
||||||
328
skills/recipe-writer/references/trait-implementation-guide.md
Normal file
328
skills/recipe-writer/references/trait-implementation-guide.md
Normal 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
|
||||||
736
skills/recipe-writer/references/yaml-lst-reference.md
Normal file
736
skills/recipe-writer/references/yaml-lst-reference.md
Normal 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
|
||||||
144
skills/recipe-writer/scripts/add_license_header.sh
Executable file
144
skills/recipe-writer/scripts/add_license_header.sh
Executable 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
|
||||||
299
skills/recipe-writer/scripts/init_recipe.py
Executable file
299
skills/recipe-writer/scripts/init_recipe.py
Executable 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()
|
||||||
331
skills/recipe-writer/scripts/validate_recipe.py
Executable file
331
skills/recipe-writer/scripts/validate_recipe.py
Executable 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()
|
||||||
13
skills/recipe-writer/templates/license-header.txt
Normal file
13
skills/recipe-writer/templates/license-header.txt
Normal 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.
|
||||||
122
skills/recipe-writer/templates/template-declarative-recipe.yml
Normal file
122
skills/recipe-writer/templates/template-declarative-recipe.yml
Normal 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
|
||||||
200
skills/recipe-writer/templates/template-imperative-recipe.java
Normal file
200
skills/recipe-writer/templates/template-imperative-recipe.java
Normal 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;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
292
skills/recipe-writer/templates/template-recipe-test.java
Normal file
292
skills/recipe-writer/templates/template-recipe-test.java
Normal 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
|
||||||
|
// // """
|
||||||
|
// // )
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
}
|
||||||
134
skills/recipe-writer/templates/template-refaster-template.java
Normal file
134
skills/recipe-writer/templates/template-refaster-template.java
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user