OpenRewrite recipe authoring workshop
OpenRewrite is a framework for writing and running code transformations. Recipes are the unit of work in OpenRewrite, and can be written in YAML, Refaster, or imperative Java.
In this workshop, we'll walk through everything you need to know to get started with recipe development – from writing simple YAML recipes, to creating complex imperative recipes, along with all of the steps in between such as testing or debugging recipes.
This workshop is designed to be hands-on, so you can follow along with the examples in your own environment. The workshop consists of the materials you see here, and a set of exercises to help you practice what you've learned.
Be sure to also follow links to the OpenRewrite documentation for more in-depth information. Feel free to skip around to the sections that interest you most, based on your needs and experience level.
If you get stuck, or have questions, feel free to ask in the OpenRewrite Slack or Discord.
Running existing recipes
Before you begin writing your own recipes, you should make sure you are aware of what recipes already exist and how to run them. This is beneficial for two reasons: you won't spend time creating a recipe that someone else has already made, and you will gain a better understanding of how people will actually use any recipe you write.
There are two main locations for discovering recipes: the OpenRewrite recipe catalog and the Moderne Platform. The former contains all of the information you'll need to run the recipe with Gradle, Maven, or the command line – whereas the latter is more of a rich viewer that you can run recipes directly from.
There are various ways to run recipes, depending on your needs:
The open-source Rewrite Maven plugin and Rewrite Gradle plugin allow you to run recipes against a single local project.
Both plugins are free, open source, and can be used on any project – without any connection to Moderne.
These plugins build up an in memory model of your project for every recipe run (which may be problematic for very large repositories or if you want to run recipes against a considerable number of repositories).
The OpenRewrite IntelliJ IDEA plugin allows you to run recipes against a single project, and to write and run recipes in the IDE.
Free to use on any project, without any connection to Moderne.
Only supports writing and running YAML recipes, for now.
Requires the Ultimate edition of IntelliJ.
Can't be used in combination with any of the above plugins.
The Moderne CLI allows you to run recipes against multiple projects locally, and to debug recipes at scale.
Free to use on open-source projects, but requires a Moderne CLI license for private projects.
Serializes the LST of your project to disk, and runs recipes against that serialized LST. Larger projects that won't work well with OpenRewrite can use the CLI instead.
The Moderne Platform offers a UI that allows you to run recipes at scale, create data visualizations, and track progress over time.
Supports over 37,000 open-source projects and organizations for free.
Requires a company subscription for private projects.
Similar to the CLI, it can handle projects of any size.
Learn more about the differences between OpenRewrite and Moderne.
Exercise 1: Run a recipe from the OpenRewrite recipe catalog against your own project.
To get comfortable running recipes, let's walk through running a recipe from the OpenRewrite recipe catalog against one of your own projects.
Goals for this exercise
See what recipes are already available in the OpenRewrite recipe catalog.
See the types of changes that can be made to your code.
Explore the options available to run recipes against your own project(s).
Steps
Pick a project you'd like to run a recipe against.
Ideally this would be a smallish Java project that uses Maven or Gradle.
If you don't have a project handy, you can use the Spring PetClinic repository.
Ensure you have a fresh checkout of your project, with no uncommitted changes.
Choose a recipe from the OpenRewrite recipe catalog that you'd like to run.
For example, upgrade to Java 21.
Or order imports.
Or migrate to AssertJ, from Hamcrest & JUnit.
Resolve common static analysis issues, as seen in the SonarQube rules.
Or apply Spring Boot 3.x best practices.
Any of the other popular recipes guides.
Run the recipe.
If you want to use the Moderne Platform, please note that you need to sign in with a GitHub account before you can run a recipe.
If you want to use the Moderne CLI, you will need to run the mod build command to serialize the LST of your project before you can
mod run
recipes.If you want to use the OpenRewrite IntelliJ plugin, you'll want to create a
rewrite.yml
file similar to the one below.The OpenRewrite IntelliJ IDEA plugin shows a runnable icon next to recipes in a
rewrite.yml
file.
Review the changes made by the recipe, and ensure they are what you expected.
Feel free to commit the changes made by the recipe to a new branch in your project. Or, if you were mainly just testing the recipe, feel free to discard the changes.
As future exercises use this repository, you may find it beneficial to ensure it's in a clean state prior to moving on.
Before we wrap up this exercise, let's take a look at the source code for the recipe you ran. Go back to the recipe page that you found in step 3 and click on the
GitHub
link to view the related source code on GitHub.See if you can correlate the changes made by the recipe with the code in the recipe.
Takeaways
There are over 2500 recipes already available to run that cover a wide range of use cases.
Recipes can make changes to Java source files, properties files, XML files, build files and more.
It's not necessary to change your build to run recipes, but it can be helpful to add the plugins when running recipes repeatedly.
Any recipe page in the docs links to the source code of the recipe on GitHub, so you can see how it's implemented.
The tests for the recipe are also available, so you can see how the recipe behaves in various scenarios.
The Moderne CLI and Platform allow you to run recipes at scale, to see how recipes behave in practice.
Notice how most recipes are packaged into separate rewrite recipe modules, that you add as plugin dependency or provide to the Maven command line via
-Drewrite.recipeArtifactCoordinates
.There's separate modules for static code analysis, Spring recipes, Java recipes, testing recipes, logging recipes, and many more under the OpenRewrite GitHub organization.
If you're specifically interested in migrating Spring Boot applications, check out our blogpost on migrating to Spring Boot 3.x. You may also be interested in looking at the migrate to Spring Boot 3.x recipe.
Recipe development environment
Now that you've seen how to run recipes, let's look at how to write your own recipes.
You'll want to have the following installed:
Java 17 or higher, as our RewriteTests use text blocks
Recipes use Java 8 source level, such that they can run on Java 8 and higher
IntelliJ IDEA Ultimate 2024.1+
In particular the OpenRewrite plugin, to run and write YAML recipes (This comes pre-installed with IntelliJ versions 2024.1 or later)
Optionally, the Moderne plugin, for faster recipe development
A local git clone of the rewrite-recipe-starter repository, as a starting point for your own recipe module
Optionally, the Moderne CLI, to run recipes at scale locally, and debug against serialized LSTs
Exercise 2: Create your own recipe module
Goals for this exercise:
Set up a new recipe module in your IDE, based on the rewrite-recipe-starter project.
Run the unit tests for the recipe module, to ensure everything is set up correctly.
Install your recipe module to your local Maven repository, to run it against your own projects.
Steps
Git clone the rewrite-recipe-starter.
You can either clone the project at is, or use it as a template to create a new GitHub repository.
Open the project in IntelliJ IDEA.
You have the option to import the project as a Maven project, or as a Gradle project. Pick the one you're most comfortable with.
(Optional) Consider updating the way you run tests in IntelliJ to speed up test responsiveness.
Run the unit tests in the project, to ensure everything is set up correctly.
All tests should pass, and you should see a message that the project was successfully built.
(Optional) Customize the project's group ID and artifact ID in the
pom.xml
file, orbuild.gradle
andsettings.gradle
file. Also consider updating the Java package names to reflect these changes as well.This helps make the project your own, and allows you to version and share your recipes without conflicts.
For the purposes of this workshop, this isn't required, though. Feel free to continue using
com.yourorg
throughout.
Install the project to your local Maven repository.
Run
mvn install
from the root of the project, or./gradlew publishToMavenLocal
if you're using Gradle.You should see a message that the project was successfully installed to your local Maven repository.
Run recipe
com.yourorg.UseApacheStringUtils
against your project from Exercise 1.You'll need to add a dependency on your recipe module to your project, or provide
-Drewrite.recipeArtifactCoordinates=com.yourorg:rewrite-recipe-starter:LATEST
on the command line:You should see limited changes to your project if you were using JUnit's
Assertions assertEquals(..)
method.
Briefly look over the various recipes and tests in the starter project. We will visit these in more details in upcoming exercises.
Takeaways
The Rewrite recipe starter project is a good starting point for your own recipe module.
There are various types of recipes included in the starter project, to give you a feel for how they're implemented.
The unit tests in the starter project take in text blocks that assert the state before and after running a recipe.
Fundamental concepts
Before we dive into writing recipes, let's take a look at some fundamental concepts that underpin OpenRewrite.
Read up on the following concepts in the OpenRewrite documentation, to get a better understanding of how OpenRewrite works:
These concepts should give you some sense as to the importance of exact type attribution, and how visitors are used to traverse and modify the LST. Without these, it would be next to impossible to write recipes that make changes to your code reliably.
Three types of recipes
It's important to note there are different types of recipes, each with their own trade-offs.
Declarative recipes are the simplest to write, and are the most common type of recipe. They are written in YAML, and often tie together existing recipe building blocks with some light configuration.
Refaster rules bring you the benefit of compiler support, and work best for straightforward replacements. They generate recipes that can also be used as a starting point for more complex recipe implementations.
Imperative recipes are the most powerful, and allow you to write Java code to implement your recipe. By using the
JavaTemplate
builder, you can keep complexity down, as you define arbitrary code changes.
No matter which method of recipe development you choose, you can always write unit tests for your recipe. Beyond that, there are best practices for writing recipes, such as ensuring idempotence, and avoiding harmful changes.
Declarative YAML recipes
As a best practice, if your recipe can be declarative (meaning it can be built out of other recipes), then you should make it declarative. You can make some truly powerful migration recipes by combining many tiny recipes together (which have been vetted to handle specific tasks correctly, such as only adding dependencies as needed).
Exercise 3: Write a declarative YAML recipe
Let's have a look at a simple declarative YAML recipe, and expand that to cover an additional use case.
Goals for this exercise
Write a declarative YAML recipe that ties together existing recipes.
Learn how to configure a recipe with options.
Gain an understanding of the order that recipes are executed and what that means for your recipe options.
Steps
If you don't have IntelliJ IDEA 2024.1 Ultimate, you'll lack bundled editor support for writing and running recipes. Some of the below steps will not work for you without this.
Open the rewrite-recipe-starter project in IntelliJ IDEA
You can also compose recipes in the Moderne Platform recipe builder, and run them against open-source projects.
Open the
UseApacheStringUtils
recipe which is defined in a YAML file: src/main/resources/META-INF/rewrite/stringutils.yml.Notice how the file is structured, with a
type
,name
,displayName
,description
, andrecipeList
fields.Comment out the
type:
and see how that disables the OpenRewrite support.
Note how the
recipeList
field is a list of fully qualified class names of recipes along with their options (if any exist).Click through on the
AddDependency
andChangeType
recipes to open their definition.Have your IDE suggest options to existing recipes by triggering auto-completion (ctrl + space by default). You should see that the recipe doesn't have every option by default (e.g., it's missing
estimatedEffortPerOccurrence
).
The migration recipe is a great start, but far from complete. Let's add a recipe to change from Spring's
trimWhitepace(String)
to Apache Common'sStringUtils.strip(String)
.Begin by adding the org.openrewrite.java.ChangeMethodName recipe to the end of the
recipeList
field.Make sure to pass in
methodPattern: org.apache.commons.lang3.StringUtils trimWhitespace(java.lang.String)
andnewMethodName: strip
such as in this example gist.Please note that the method pattern refers to a method that does not exist. Apache Commons does not have a
trimWhitespace
method, but Spring does. That's because recipes in therecipeList
are executed in order. The ChangeType recipe comes before our newChangeMethodName
recipe. That means that when ourChangeMethodName
recipe is run, there will no longer be a SpringtrimWhitespace
method. This is important to keep in mind when chaining recipes together.
Open the unit test src/test/java/com/yourorg/UseApacheStringUtilsTest.java.
Notice how we implement
RewriteTest
, overridedefaults(RecipeSpec)
to run our recipe, and configure a classpath for the tests that has bothcommons-lang3
andspring-core
on it.Run the first test. Note that we invoke
rewriteRun(SourceSpecs...)
and pass in a singlejava(String, String)
source specification, that takes in a before and after text block.The
//language=java
language injection enables syntax highlighting and code completion in the text block.All together, this asserts that when we run the recipe, code that matches the
before
block will be converted to code that matches theafter
block.
Add a unit test for the
ChangeMethodName
recipe we added that convertstrimWhitespace
tostrip
.Run the new unit test, and verify that the correct changes are indeed made.
Takeaways
Declarative recipes are the simplest to write, and are the most common type of recipe.
Common building blocks can be configured and combined to compose more complex migrations.
Recipes can be chained together, to make multiple changes to your code in a single run.
When changing types, keep in mind the order of recipes as subsequent recipes in the
recipeList
will need to use the new type.Unit tests are a great way to ensure your recipe behaves as expected.
Preconditions
Preconditions are recipes that run before other recipes to limit which source files the recipe will run on. Preconditions are often used to ensure a recipe only runs against certain files or directories – but any recipe which is not a ScanningRecipe
can technically be used as a precondition.
When a recipe is used as a precondition, any file it would make a change to is considered to meet the precondition. When more than one recipe is used, all of them must make a change to the file for it to be considered to "meet the precondition".
One substantial benefit of preconditions is that other recipes don't need to individually support options to limit themselves to a particular path.
Exercise 4: Adding preconditions to a recipe
Let's update the stringutils.yml
recipe to only run on sources that are likely tests, by adding a precondition that uses the org.openrewrite.java.search.IsLikelyTest
recipe.
Goals for this exercise
Discover common preconditions, and learn how to combine those with recipes.
Steps
Open the
UseApacheStringUtils
YAML file (src/main/resources/META-INF/rewrite/stringutils.yml
) once again.Add a
preconditions
field to the recipe, in between thedescription
andrecipeList
fields.Add a single
org.openrewrite.java.search.IsLikelyTest
recipe to the list of preconditions, with no options.Ignore the
Schema validation: Property 'precondition' is not allowed
from IntelliJ; it very much is allowed.
Open the unit test
src/test/java/com/yourorg/UseApacheStringUtilsTest.java
.Run the tests; verify that neither test makes any changes right now.
Add a static import on
org.openrewrite.java.Assertions.srcTestJava
.Wrap the
java(String, String)
methods withsrcTestJava()
to indicate that the sources are tests.Run the tests again, and verify that they now pass.
Explore other
Find
recipes in the OpenRewrite recipe catalog:org.openrewrite.FindSourceFiles, to match specific files or directories.
org.openrewrite.java.migrate.search.FindJavaVersion, to match specific Java versions.
org.openrewrite.java.search.FindTypes, to find type references by name.
Takeaways
Preconditions are used to limit which source files a recipe is run on.
Common preconditions can be used to target specific files or directories.
When a recipe is used as a precondition, any file it would make a change to is considered to "meet the precondition" – which means the main recipe will run against it.
Preconditions themselves do not make changes to the source code, but are used to limit which files a recipe is run on.
Testing recipes
When developing recipes, it's very important to test them to ensure that they not only make the expected changes but that they also don't make unnecessary changes.
OpenRewrite has extensive support for testing recipes through the RewriteTest
interface, which allows you to write tests that assert the state of the LST before and after running a recipe.
In addition to verifying the textual output, the unit testing framework also makes assertions on the underlying types and structure of the LST, as that might otherwise negatively affect recipe composition. The testing framework will tell you when there are issues with the type information, and help you to correct them.
There are various ways to provide a recipe specification – from reading a recipe from any YAML resource through recipeFromResources
, to reading a recipe from a specific resource, to constructing a Java recipe directly and passing that in.
Similarly, there are various ways to pass in source specifications. Each parser has an Assertions
class to provide test source files of a specific type. Source specifications can take in a single text block to assert no changes are made to a file, or a pair of text blocks to assert that a file is changed from one state to another. An optional final argument can consume a SourceSpec
to provide additional configuration, or make assertions before or after the recipe is run.
Exercise 5: Explore the various unit tests in the starter project
Let's explore the unit tests in the starter project, to see what elements you can take from each for your own tests.
Goals for this exercise
Understand how to write unit tests for your recipes.
Learn how to assert the state of the LST before and after running a recipe.
Explore the various ways to provide recipe and source specifications.
Steps
Open src/test/java/com/yourorg/AppendToReleaseNotesTest.java.
Notice how the recipe specification directly constructs a
JavaRecipe
and passes that in. This is most convenient when testing imperative recipes.Notice how
@Test void createNewReleaseNotes() { ... }
usesorg.openrewrite.test.SourceSpecs.text(java.lang.String, java.lang.String)
to provide a before and after text block.The before text block is
null
to indicate that the file does not exist initially. Conversely, you can pass innull
as the second argument to indicate that the file should be deleted.@Test void editExistingReleaseNotes()
uses an additionalspec -> spec.path(Paths.get("RELEASE.md")
to set the source file, such that the recipe will match.
Open src/test/java/com/yourorg/AssertEqualsToAssertThatTest.java.
Note how
.parser(JavaParser.fromJavaVersion().classpath("junit-jupiter-api"))
is called on the recipe specification.Try commenting out the
classpath("junit-jupiter-api")
and run the test.The resulting
java.lang.IllegalStateException: LST contains missing or invalid type information
indicates that the type information is missing, and that the test classpath is likely not correctly set up.
Open src/test/java/com/yourorg/NoGuavaListsNewArrayListTest.java.
Read the various comments throughout this test class.
Try to make minimal changes in the recipe and see how they affect the tests.
Open src/test/java/com/yourorg/ClassHierarchyTest.java.
Note how each
rewriteRun
consumes aRecipeSpec
to assert thedataTable
rows produced in the recipe run.Correlate this to the
insertRow
calls in the recipe, to see how the recipe produces the expected output.
Open src/test/java/com/yourorg/SimplifyTernaryTest.java.
Note how the instantiated recipe is a generated class, not the Refaster template class itself.
See how
@Test void unchanged() { ... }
asserts no changes are made where those would be unsafe to make.
Takeaways
The
RewriteTest
interface allows you to write tests that assert the state of the LST before and after running a recipe.RecipeSpecs
can be constructed in various ways, for each of the recipe types.SourceSpecs
can take one, two or three arguments, depending on the type of assertion you want to make.Each parser has an
Assertions
class to provide test source files of a specific type.The testing framework will tell you when there are issues with the type information, and help you to correct them.
Refaster recipes
OpenRewrite has support for writing Refaster recipes, which are a way to write code transformations in Java, and have them run as recipes.
Refaster recipes are an easy step-up to writing imperative recipes, as they are written in Java, and can be run as recipes. Your compiler will help you catch syntax errors, and you can use your IDE to navigate to definitions and references. The generated recipes can also be used as a starting point for more complex recipe implementations.
Exercise 6: Explore Refaster recipe support
Let's look at an existing Refaster recipe in the starter project, and see how it's implemented.
Goals for this exercise
Understand how OpenRewrite supports Refaster recipes.
See how Refaster recipes are written in Java, and how they can be run as recipes.
Steps
Open the Refaster template src/main/java/com/yourorg/SimplifyTernary.java
Read through the template, and see how it matches a ternary expression that can be simplified.
Note how we're only using a limited subset of Refaster's capabilities, as not everything is supported yet.
Open the unit test src/test/java/com/yourorg/SimplifyTernaryTest.java
Read through the test, and see how each ternary is simplified, and wrapped as necessary.
Click through on the
com.yourorg.SimplifyTernaryRecipes
class, to see the generated recipe.Note how the
SimplifyTernaryRecipes
extendsRecipe
and overridesgetRecipeList()
to return two recipes.Each inner recipe returns a visitor that extends
AbstractRefasterJavaVisitor
.There are before and after
JavaTemplates
that are used to match and replace the ternary expressions.The
visitTernary
method is overridden to match the ternary expressions, and replace them with the simplified version.
Click through on the
org.openrewrite.java.template.internal.AbstractRefasterJavaVisitor
.Notice the embedding options, and how those call out to subsequent visitors for cleanups and simplifications.
Now look back at the visitors in
SimplifyTernaryRecipes
to see which embedding options are enabled there.
Open the build files
pom.xml
andbuild.gradle
to see how the Refaster recipes are generated.Notice the
rewrite-templating
dependency and annotation processor. This is what enables generating the Refaster template recipes.
Takeaways
Refaster templates are converted into regular OpenRewrite recipes, and can be run as such.
Common base classes, and embedding options lighten the load in implementing Refaster templates.
Some 400+ recipes from Picnic's ErrorProne Support have made it into rewrite-third-party and the app.moderne.io marketplace.
Exercise 7: Create a Refaster recipe
Let's create a Refaster recipe that standardizes various ways to check if a String is empty or not.
Goals for this exercise
Explore IDE support for generating Refaster recipes.
Write a Refaster template that matches various ways to check if a String is empty.
Customize the generated recipe, using the tests to cover the various aspects.
Steps
Open the unit test src/test/java/com/yourorg/StringIsEmptyTest.java
Read through the test, to get a feel for the cases you should cover.
Remove the
@Disabled
annotation, and run the test to see that it fails.Uncomment the
spec.recipe(new StringIsEmptyRecipe());
line, and see that the class is missing.
If you have the Moderne plugin 4.0+ for IntelliJ IDEA installed, you can generate Refaster recipes directly from the IDE.
Right click on any Java element in your editor, and select "Generate... > Create Recipe (Refaster Style)"
A scratch file will be created that you can customize, and add to your recipe module.
Open the Refaster template src/main/java/com/yourorg/StringIsEmpty.java
Using the knowledge gained in Exercise 6, and the requirements from the test, write a Refaster recipe that matches various ways to check if a String is empty.
Think about if your methods should take in any argument, and what the type of that argument should be.
Add your first
@BeforeTemplate
and@AfterTemplate
annotated methods, to match and replace the first way to check for an empty string.
Trigger an explicit build of your project to generate the Recipe class with Ctrl + F9, or equivalent.
Notice how the unit test now compiles; compare the generated recipe with the template you wrote.
Run the test to see where you stand, and add additional
@BeforeTemplate
annotated methods to cover all cases.
Follow the instructions in the tests to add a name and description to your recipe.
These will be visible in any generated documentation, when folks run and discover recipes, and in Moderne.
Takeaways
Refaster templates can be generated from the IDE, and used as a starting point for more complex recipe implementations.
A Refaster rule can contain more than one before template, to match different ways to check for an empty string.
You can customize the Recipe name and description, with the help of the
@RecipeDescriptor
annotation.
Imperative recipes
For use cases beyond what declarative recipes and Refaster templates can handle, you'll want to look at writing a Java refactoring recipe.
You might want to refresh your memory on visitor pattern and Lossless Semantic Trees before you dive in.
These imperative recipes use the visitor pattern to traverse the LSTs, and make changes to the code. The JavaTemplate
class is used to create new LST elements, that can replace existing LST elements.
Exercise 8: Explore an imperative recipe
Let's look at an existing imperative recipe in the starter project, and see how it's implemented.
Goals for this exercise
Understand LST elements and how to traverse them.
See how JavaTemplates are used to create new LST elements.
Make small adjustments and see how they affect the recipe.
Steps
Open
src/main/java/com/yourorg/NoGuavaListsNewArrayList.java
in IntelliJ IDEA.Read through the recipe, and see how it matches three variants of Guava's
Lists.newArrayList()
.Three replacement
JavaTemplate
s are provided, to replace each of the Guava calls withnew ArrayList<>(..)
.
We override
visitCompilationUnit
to print the tree.Notice the call to
super.visitCompilationUnit
, which is necessary to traverse the tree.Click through on
super.visitCompilationUnit
to see how the tree is traversed.Comment out the
super.visitCompilationUnit
and see how the recipe fails to make any changes.
We override
visitMethodInvocation
to replace each of the Guava calls.See how we apply Preconditions here too, through the Java API, to limit which source files are visited.
Notice how we pass in a
Cursor
andJavaCoordinates
when we apply theJavaTemplate
. This is necessary to ensure that the changes are made in the correct location. Briefly explore the other coordinates available.Notice the type parameters passed in to the
JavaTemplate
s, and how those match the arguments passing intoapply
.The calls to
maybeAddImport
andmaybeRemoveImport
are necessary to ensure that the imports are correctly updated. These will only be added or removed if the first or last LST element using the import is added/removed.
The returned value of
visitMethodInvocation
is the result of theJavaTemplate
application, which is used to determine if the recipe made any changes.When none of the methods are matched, we still call
super.visitMethodInvocation
to ensure that the tree is traversed. Replace this withreturn method;
and see which of the test cases fails to make changes.You can intentionally return the original LST element in cases where you don't want to traverse further down the tree.
Open
src/test/java/com/yourorg/NoGuavaListsNewArrayListTest.java
.Recall the structure of the test class, how it extends
RewriteTest
, and uses recipe and source specifications.Notice how
@Test void noChangeNecessary()
asserts that no changes are made if the desired state is already reached. A common mistake we see in recipe development is that folks unconditionally make changes, which a test like this guards against.
Set a breakpoint in the
visitMethodInvocation
method, and run each of the tests.Explore the LST in the debugger, and see all the elements present on the current element.
Compare the LST printed to the console with the diagrams in our Java LST examples doc.
Add a
TreeVisitingPrinter.printTreeAll(method)
to thevisitMethodInvocation
method, to see elements in more detail.Run the tests again, and see the tree printed to the console.
Takeaways
Imperative recipes use the visitor pattern to traverse the LSTs, and make changes to the code.
You are in full control of tree traversal, and can decide whether to traverse further down the tree.
JavaTemplates are used to create new LST elements, that can replace existing LST elements.
The
maybeAddImport
andmaybeRemoveImport
methods are necessary to ensure that the imports are correctly updated.The
TreeVisitingPrinter
can be used to print the LST elements in more detail, to help you understand the structure of the tree.
Advanced recipe development
Beyond the basics of writing recipes, there are a number of advanced topics that you might want to explore on your own.
Working with dependencies
When writing recipes, you might want to add dependencies to your project, to use types from those dependencies in your recipes. When you need to support recipes across multiple major versions, you'll want to look at using multiple versions of a library in a project.
Scanning recipes
When creating new recipes, you may find it desirable to examine multiple source files, potentially of different types, to make key decisions in your visitor. For example, you may want to look for a particular condition to be present in a Maven POM file and, if that condition is met, alter an application property in a YAML file. This is where scanning recipes come in.
The rewrite-recipe starter contains an example in the form of src/main/java/com/yourorg/AppendToReleaseNotes.java that you might want to explore.
Data tables
Sometimes you're more interested in extracting insights from across your projects, rather than directly making code changes. In those cases data tables come in handy, as they allow you to extract data from your projects, and analyze it in a tabular format.
The src/main/java/com/yourorg/ClassHierarchy.java recipe in the starter project is a good example of how to use data tables.
Debugging recipes
When you're developing recipes, you might want to debug them to see how they behave in practice against real projects.
The Moderne IntelliJ IDEA plugin has support for running recipes in debug mode, to see how they behave in practice. This leverages the Moderne CLI for recipe runs against a serialized LST, skipping the time-consuming parsing step.
Running at scale
Once you have your recipes developed, you'll likely want to run them against not just one project, but many projects. You have two main options for this:
The Moderne CLI is a great way to run recipes across many projects from your local machine. This uses serialized LSTs to allow repeated recipe runs against the same model, and create commits and push up changes across many repositories.
The Moderne Platform allows you to run recipes against open-source projects, and see how they behave in practice. You can preview the changes and choose to create a pull request, or discard the changes. You can also generate reports and visualizations, and track progress towards migration goals across time through the DevCenter.
Check out the Apache Maven DevCenter for an example of goals being tracked and made actionable through recipes.
Recipe conventions and best practices
We've documented the most important recipe conventions and best practices to help you write recipes that are safe, idempotent, and efficient. Where possible, we've automated these checks in the unit testing framework, to help you catch issues early.
You can also run best practice recipes against your rewrite recipe module, to resolve issues automatically where possible. These are based on a collection of best practices for writing OpenRewrite recipes.
You can apply these recommendations to your recipes by running the following command:
Contributing to OpenRewrite
Now that you've written your own recipes, you might want to contribute back to the OpenRewrite community. For any new contribution, the first thing to check is whether there is already a corresponding issue on the backlog, perhaps with some pointers on an implementation. If not, you can create a new issue to discuss the recipe you'd like to develop.
We have some good first issues in particular that are great when you're just starting out and want feedback on your work to help improve your skills.
Note that there are separate modules for Spring recipes, Java recipes, testing recipes, logging recipes, and many more. It helps to browse the existing modules for any related work that might be similar and start from there.
For any further questions, feel free to ask in the OpenRewrite Slack or Discord. Hope to see you there!
Last updated