Skip to main content

Refaster rules

Automated code migrations are implemented using a combination of Error Prone Refaster rules and OpenRewrite recipes. This hybrid approach provides both the expressiveness of pattern matching and the powerful refactoring capabilities of OpenRewrite.

Architecture​

The migration framework consists of three main components:

  1. Refaster Rules - Define before/after code patterns using Google's Error Prone Refaster.
  2. OpenRewrite Recipes - Auto-generated from Refaster templates to perform actual transformations.
  3. Recipe Tests - Verify transformations work correctly on real code examples.

Project Structure​

The recipes are developed in the recipes/ module:

recipes/
├── pom.xml # Maven configuration
├── src/main/java/
│ └── org/openrewrite/recipes/
│ └── AssertToAssertThat.java # Refaster templates
├── src/main/resources/META-INF/rewrite/
│ ├── rewrite.yml # Recipe composition
│ └── classpath.tsv.gz # Type information
└── src/test/java/
└── org/openrewrite/recipes/
└── AssertToAssertThatTest.java # Recipe tests

How Refaster Rules Work​

Refaster rules define code transformations using a simple before/after pattern:

@RecipeDescriptor(
name = "Convert assert to AssertJ",
description = "Convert `assert` statements to AssertJ assertions."
)
static class AssertThatIsNull {
@BeforeTemplate
void before(Object actual) {
assert actual == null;
}

@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
void after(Object actual) {
assertThat(actual).isNull();
}
}

Key Annotations​

  • @RecipeDescriptor - Provides recipe name and description for documentation.
  • @BeforeTemplate - Defines the pattern to match in existing code.
  • @AfterTemplate - Defines the replacement code.
  • @UseImportPolicy - Controls how imports are added (STATIC_IMPORT_ALWAYS, IMPORT_CLASS_DIRECTLY, etc.).

Template Compilation​

When you compile the project from the recipes directory, the rewrite-templating annotation processor automatically generates OpenRewrite recipes from your Refaster rules:

mvn clean compile
# Generates: recipes/target/generated-sources/annotations/org/openrewrite/recipes/AssertToAssertThatRecipes.java

The generated class contains one Recipe for each Refaster rule.

Writing Refaster Rules​

Basic Pattern Matching​

Refaster templates can match various code patterns:

static class AssertThatIsNullWithMessage {
@BeforeTemplate
void before(Object actual, String message) {
assert actual == null : message;
}

@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
void after(Object actual, String message) {
assertThat(actual).as(message).isNull();
}
}

Type Constraints​

You can constrain templates to match only specific types:

static class AssertThatStringIsEmpty {
@BeforeTemplate
void before(String str) {
assert str.isEmpty();
}

@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
void after(String str) {
assertThat(str).isEmpty();
}
}

Multiple Overloads​

Create separate templates to match different method signatures:

// Without message
static class AssertThatIsNotNull {
@BeforeTemplate
void before(Object actual) {
assert actual != null;
}

@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
void after(Object actual) {
assertThat(actual).isNotNull();
}
}

// With message
static class AssertThatIsNotNullWithMessage {
@BeforeTemplate
void before(Object actual, String message) {
assert actual != null : message;
}

@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
void after(Object actual, String message) {
assertThat(actual).as(message).isNotNull();
}
}

Writing Recipe Tests​

Let's write tests using OpenRewrite's testing framework to verify the transformations we defined:

class AssertToAssertThatTest implements RewriteTest {

@Override
public void defaults(RecipeSpec spec) {
spec.recipe(new AssertToAssertThatRecipes());
}

@DocumentExample // Shows this test in recipe documentation.
@Test
void assertNull() {
rewriteRun(
java(
"""
class Test {
void test(Object obj) {
assert obj == null;
}
}
""",
"""
import org.assertj.core.api.Assertions;

class Test {
void test(Object obj) {
Assertions.assertThat(obj).isNull();
}
}
"""
)
);
}
}

Test Structure​

  1. Before code - First string argument shows the original code.
  2. After code - Second string argument shows the expected result.
  3. Imports - OpenRewrite automatically adds necessary imports.
  4. Formatting - Indentation and whitespace are preserved.

Best Practices for Tests​

  • Include one test per template to verify it works.
  • Use @DocumentExample on the most representative test.
  • Test edge cases (null messages, complex expressions, etc.).
  • Verify imports are added correctly.
  • Test that the transformation doesn't break valid code.

Recipe Composition​

The rewrite.yml file allows you to compose recipes and add preconditions:

---
type: specs.openrewrite.org/v1beta/recipe
name: org.openrewrite.AssertToAssertThatRecipesForTests
displayName: Convert `assert` to `assertThat` in tests.
description: Convert `assert` to `assertThat` in test methods only.
preconditions:
- org.openrewrite.java.search.IsLikelyTest
recipeList:
- org.openrewrite.recipes.AssertToAssertThatRecipes

Preconditions​

Preconditions ensure recipes only run in appropriate contexts:

  • org.openrewrite.java.search.IsLikelyTest - Only transform test files.
  • org.openrewrite.java.search.FindMethods - Only transform specific methods.
  • Custom preconditions - Create your own filters.

Development Workflow​

1. Generate Type Table​

When starting a new recipe project, generate type information for dependencies:

mvn generate-resources -Ptypetable

This creates META-INF/rewrite/classpath.tsv.gz with type information for libraries like AssertJ.

2. Write Refaster Rules​

Add templates to your Java file:

@RecipeDescriptor(
name = "Your Recipe Name",
description = "What this recipe does"
)
static class YourRecipeName {
@BeforeTemplate
void before(/* parameters */) {
// Pattern to match
}

@AfterTemplate
@UseImportPolicy(ImportPolicy.STATIC_IMPORT_ALWAYS)
void after(/* same parameters */) {
// Replacement code
}
}

3. Compile to Generate Recipes​

mvn clean compile

The annotation processor generates OpenRewrite recipes in target/generated-sources/annotations/.

4. Write Tests​

Add tests to verify your transformations:

@Test
void yourRecipeTest() {
rewriteRun(
java(
"""
// Before code
""",
"""
// After code
"""
)
);
}

5. Run Tests​

mvn test

6. Apply Recipes​

Use the OpenRewrite Maven plugin to apply recipes to a codebase:

cd /path/to/target/codebase
mvn org.openrewrite.maven:rewrite-maven-plugin:run \
--define rewrite.recipeArtifactCoordinates=org.openrewrite:recipes:1.0-SNAPSHOT \
--define rewrite.activeRecipes=org.openrewrite.AssertToAssertThatRecipesForTests

Debugging Tips​

View Generated Recipes​

Check the generated recipe code:

cat target/generated-sources/annotations/org/openrewrite/recipes/AssertToAssertThatRecipes.java

Test Individual Templates​

Create focused tests for specific transformations:

@Test
void specificCase() {
rewriteRun(
spec -> spec.recipe(new AssertToAssertThatRecipes.AssertThatIsNullRecipe()),
java(
"assert obj == null;",
"assertThat(obj).isNull();"
)
);
}

Resources​