Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:45:33 +08:00
commit 480d09eec9
27 changed files with 8336 additions and 0 deletions

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.YourRecipeName:
* parameterName: value
* ```
*/
@Value
@EqualsAndHashCode(callSuper = false)
public class YourRecipeName 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 YourRecipeName(
@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 YourRecipeVisitor()
// );
// }
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
// IMPORTANT: Always return a NEW instance (no caching)
return new YourRecipeVisitor();
}
/**
* The visitor implements the actual transformation logic.
* Use JavaIsoVisitor when always returning the same LST type.
*/
public class YourRecipeVisitor 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 YourRecipeName
*
* 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 YourRecipeNameTest 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 YourRecipeName("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 YourRecipeName("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.YourRecipeName")
// .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
// // """
// // )
// );
// }
}