Initial commit

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

View File

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

View File

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

View File

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

View File

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

View File

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