Links

Rewrite 7 to 8 migration. 8.1.2 Release (2023-06-13)

OpenRewrite 8 makes some substantial changes in order to support large monorepos that can not fit into memory. All recipes will need to be migrated in order for them to keep working. Specific migration instructions can be found in the migrating your recipes section.
Below, we'll detail the key changes being made and provide guidance for how you can update your recipes to ensure they keep working. At the bottom you'll find the standard new/changed/deleted recipe list.

Key Changes

Applicability tests have been replaced

Recipe no longer exposes a getSingleSourceApplicableTest or a getApplicableTest method. Recipe authors should, instead, use Preconditions.check(TreeVisitor check, TreeVisitor visitor) to conditionally apply the visitor only if the check makes sense.
As this is semantically equivalent to the OpenRewrite 7 single source applicability test, recipe authors will generally just have to copy the body of their old applicability test method into the first argument of the call to Preconditions.check().
In OpenRewrite 7, Recipe.getApplicabilityTest() was rarely used as it was confusing to most users. If your recipe uses it, you will need to convert your recipe to a ScanningRecipe.
There is, unfortunately, no way for a YAML recipe to use Preconditions. We hope to support such a feature for them eventually, though.
Example:
Before
After
package org.openrewrite.java.cleanup;
import org.openrewrite.Applicability;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.TreeVisitor;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.*;
import org.openrewrite.java.PartProvider;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public class ChainStringBuilderAppendCalls extends Recipe {
private static final MethodMatcher STRING_BUILDER_APPEND = new MethodMatcher("java.lang.StringBuilder append(String)");
private static J.Binary additiveBinaryTemplate = null;
@Override
public String getDisplayName() {
return "Chain `StringBuilder.append()` calls";
}
@Override
protected @Nullable TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
return Applicability.or(new UsesMethod<>(STRING_BUILDER_APPEND),
new UsesMethod<>(STRING_BUILDER_APPEND));
}
@Override
protected JavaIsoVisitor<ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
// do something
return m;
}
};
}
}
package org.openrewrite.java.cleanup;
import org.openrewrite.*;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.MethodMatcher;
import org.openrewrite.java.search.UsesMethod;
import org.openrewrite.java.tree.*;
import org.openrewrite.java.PartProvider;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
public class ChainStringBuilderAppendCalls extends Recipe {
private static final MethodMatcher STRING_BUILDER_APPEND = new MethodMatcher("java.lang.StringBuilder append(String)");
private static J.Binary additiveBinaryTemplate = null;
@Override
public String getDisplayName() {
return "Chain `StringBuilder.append()` calls";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.or(new UsesMethod<>(STRING_BUILDER_APPEND),
new UsesMethod<>(STRING_BUILDER_APPEND)), new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
// do something
return m;
}
});
}
}

New approach to recipe visiting

In the previous version of OpenRewrite, developers could use Recipe.visit(List ...) to randomly access different files when their recipes needed them. Unfortunately, this random access took a considerable amount of time and was not scalable for large source sets. To address that problem, OpenRewrite 8 introduces a new type of recipe that separates the run functionality into three, scalable, phases.
For recipes that only change a single source file, you won't need to use the new recipe type. Instead, you will need to update the Recipe.getVisitor() method to be public instead of protected.
For recipes that change many source files, you will need to convert the recipe to be a ScanningRecipe.

What is a ScanningRecipe?

A ScanningRecipe extends the normal Recipe and adds two key objects: an accumulator and a scanner. The accumulator object is a custom data structure defined by the recipe itself to store any information the recipe needs to function. The scanner object is a visitor which populates the accumulator with data.
Scanning recipes have three phases:
  1. 1.
    A scanning phase that collects information while making no new code changes. In this phase, the scanner is called for each source file and information is added to the accumulator that the recipe will need for future steps.
    • For example, a recipe might want to detect whether a project is a Maven project or not. The scanner could detect a pom.xml file and add a flag to the accumulator so that future steps know this.
  2. 2.
    An optional generating phase where new files are created (if any are needed). In this phase, the accumulator can be accessed to determine whether or not a file should be created.
  3. 3.
    An editing phase where the recipe makes changes (as it would before). Like the generating phase, an accumulator can be accessed to make changes – but you can not randomly access other source code or files.

Example

As converting a Recipe to a ScanningRecipe is a substantial change, the migration recipe is not able to automate this. You will need to rewrite your recipes that need to be ScanningRecipes on your own.
Below is an example of what a recipe might look like before/after this change. You can find the converted recipe in it's entirety here.
Before
After
// imports
@Value
@EqualsAndHashCode(callSuper = true)
public class AddManagedDependency extends Recipe {
// Standard methods such as displayName and description
@Override
protected List<SourceFile> visit(List<SourceFile> before, ExecutionContext ctx) {
List<SourceFile> rootPoms = new ArrayList<>();
for (SourceFile source : before) {
source.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(mavenResolutionResult -> {
if (mavenResolutionResult.getParent() == null) {
rootPoms.add(source);
}
});
}
return ListUtils.map(before, s -> s.getMarkers().findFirst(MavenResolutionResult.class)
.map(javaProject -> (Tree) new MavenVisitor<ExecutionContext>() {
@Override
public Xml visitDocument(Xml.Document document, ExecutionContext executionContext) {
Xml maven = super.visitDocument(document, executionContext);
if (!Boolean.TRUE.equals(addToRootPom) || rootPoms.contains(document)) {
Validated versionValidation = Semver.validate(version, versionPattern);
if (versionValidation.isValid()) {
VersionComparator versionComparator = requireNonNull(versionValidation.getValue());
try {
String versionToUse = findVersionToUse(versionComparator, ctx);
if (!Objects.equals(versionToUse, existingManagedDependencyVersion())) {
doAfterVisit(new AddManagedDependencyVisitor(groupId, artifactId,
versionToUse, scope, type, classifier));
maybeUpdateModel();
}
} catch (MavenDownloadingException e) {
return e.warn(document);
}
}
}
return maven;
}
@Nullable
private String existingManagedDependencyVersion() {
return getResolutionResult().getPom().getDependencyManagement().stream()
.map(resolvedManagedDep -> {
if (resolvedManagedDep.matches(groupId, artifactId, type, classifier)) {
return resolvedManagedDep.getGav().getVersion();
} else if (resolvedManagedDep.getRequestedBom() != null
&& resolvedManagedDep.getRequestedBom().getGroupId().equals(groupId)
&& resolvedManagedDep.getRequestedBom().getArtifactId().equals(artifactId)) {
return resolvedManagedDep.getRequestedBom().getVersion();
}
return null;
})
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
@Nullable
private String findVersionToUse(VersionComparator versionComparator, ExecutionContext ctx) throws MavenDownloadingException {
MavenMetadata mavenMetadata = metadataFailures.insertRows(ctx, () -> downloadMetadata(groupId, artifactId, ctx));
LatestRelease latest = new LatestRelease(versionPattern);
return mavenMetadata.getVersioning().getVersions().stream()
.filter(v -> versionComparator.isValid(null, v))
.filter(v -> !Boolean.TRUE.equals(releasesOnly) || latest.isValid(null, v))
.max((v1, v2) -> versionComparator.compare(null, v1, v2))
.orElse(null);
}
}.visit(s, ctx))
.map(SourceFile.class::cast)
.orElse(s)
);
}
}
// imports
@Value
@EqualsAndHashCode(callSuper = true)
public class AddManagedDependency extends ScanningRecipe<AddManagedDependency.Scanned> {
// Standard methods such as displayName and description
static class Scanned {
boolean usingType;
List<SourceFile> rootPoms = new ArrayList<>();
}
@Override
public Scanned getInitialValue(ExecutionContext ctx) {
Scanned scanned = new Scanned();
scanned.usingType = onlyIfUsing == null;
return scanned;
}
@Override
public TreeVisitor<?, ExecutionContext> getScanner(Scanned acc) {
return Preconditions.check(acc.usingType || (!StringUtils.isNullOrEmpty(onlyIfUsing) && onlyIfUsing.contains(":")), new MavenIsoVisitor<ExecutionContext>() {
@Override
public Xml.Document visitDocument(Xml.Document document, ExecutionContext ctx) {
document.getMarkers().findFirst(MavenResolutionResult.class).ifPresent(mavenResolutionResult -> {
if (mavenResolutionResult.getParent() == null) {
acc.rootPoms.add(document);
}
});
if(acc.usingType) {
return SearchResult.found(document);
}
return super.visitDocument(document, ctx);
}
@Override
public Xml.Tag visitTag(Xml.Tag tag, ExecutionContext ctx) {
Xml.Tag t = super.visitTag(tag, ctx);
if (isDependencyTag()) {
ResolvedDependency dependency = findDependency(t, null);
if (dependency != null) {
String[] ga = requireNonNull(onlyIfUsing).split(":");
ResolvedDependency match = dependency.findDependency(ga[0], ga[1]);
if (match != null) {
acc.usingType = true;
}
}
}
return t;
}
});
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor(Scanned acc) {
return Preconditions.check(acc.usingType, new MavenVisitor<ExecutionContext>() {
@Override
public Xml visitDocument(Xml.Document document, ExecutionContext ctx) {
Xml maven = super.visitDocument(document, ctx);
if (!Boolean.TRUE.equals(addToRootPom) || acc.rootPoms.contains(document)) {
Validated versionValidation = Semver.validate(version, versionPattern);
if (versionValidation.isValid()) {
VersionComparator versionComparator = requireNonNull(versionValidation.getValue());
try {
String versionToUse = findVersionToUse(versionComparator, ctx);
if (!Objects.equals(versionToUse, existingManagedDependencyVersion())) {
doAfterVisit(new AddManagedDependencyVisitor(groupId, artifactId,
versionToUse, scope, type, classifier));
maybeUpdateModel();
}
} catch (MavenDownloadingException e) {
return e.warn(document);
}
}
}
return maven;
}
@Nullable
private String existingManagedDependencyVersion() {
return getResolutionResult().getPom().getDependencyManagement().stream()
.map(resolvedManagedDep -> {
if (resolvedManagedDep.matches(groupId, artifactId, type, classifier)) {
return resolvedManagedDep.getGav().getVersion();
} else if (resolvedManagedDep.getRequestedBom() != null
&& resolvedManagedDep.getRequestedBom().getGroupId().equals(groupId)
&& resolvedManagedDep.getRequestedBom().getArtifactId().equals(artifactId)) {
return resolvedManagedDep.getRequestedBom().getVersion();
}
return null;
})
.filter(Objects::nonNull)
.findFirst().orElse(null);
}
@Nullable
private String findVersionToUse(VersionComparator versionComparator, ExecutionContext ctx) throws MavenDownloadingException {
MavenMetadata mavenMetadata = metadataFailures.insertRows(ctx, () -> downloadMetadata(groupId, artifactId, ctx));
LatestRelease latest = new LatestRelease(versionPattern);
return mavenMetadata.getVersioning().getVersions().stream()
.filter(v -> versionComparator.isValid(null, v))
.filter(v -> !Boolean.TRUE.equals(releasesOnly) || latest.isValid(null, v))
.max((v1, v2) -> versionComparator.compare(null, v1, v2))
.orElse(null);
}
});
}
}

JavaTemplate API has been updated

Up until now, JavaTemplate has had a single purpose: for creating LST subtrees based on template code and input parameters. In OpenRewrite 8, however, a second use case has been added – JavaTemplate can now be defined as a pattern that matches against an LST subtree, similar to a regular expression (regexp). To make this work, the template needs to be context-free, meaning it doesn't refer to surrounding code but, instead, only to template parameters and statically available elements like classes and static members.
This context-free nature allows us to embed the template into a skeleton Java class, compile it once to an LST, and then compare it with other LSTs to determine if they match. The template parameters in the LST act as wildcards in a regexp-like manner. This approach significantly improves efficiency compared to replacing each subtree separately.
The context-free templates also benefit the first use case of LST substitution, although the impact may not be as significant since template application is relatively infrequent. However, for template matching, where potentially thousands of subtrees in a single source file need to be matched, the context-free nature is crucial.
To support this new functionality, we've had to redesign the JavaTemplate API. New methods, such as matches() and matcher(), have been introduced for templating matching (similar to Java's regex functionality). Furthermore, the builder now produces context-free templates by default. If a template requires context sensitivity, the contextSensitive() method of the builder must be invoked.
You will need to update all references to JavaTemplate in your recipes. You will need to:
  1. 1.
    Double-check whether or not contextSensitive() makes sense for your recipe. It is added by default – but if your recipe doesn't refer to the surrounding code and, instead, only refers to template parameters or statically available elements like classes and static methods, you can remove it.
  2. 2.
    Determine what type of Cursor should go into the JavaTemplate.apply() method. Typically this should be getCursor(). However, if the J instance is updated in the method, the cursor on this visitor will need to be updated. In that case, you should use updateCursor instead.
The migration recipe will help you get started with this.
Example:
Before
After
package org.openrewrite.java;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import java.util.List;
@Value
@EqualsAndHashCode(callSuper = true)
public class AddOrUpdateAnnotationAttribute extends Recipe {
@Override
public String getDisplayName() {
return "Add or update annotation attribute";
}
@Override
public String getDescription() {
return "Add or update annotation attribute.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext context) {
String param1 = "test parameter 1";
String param2 = "test parameter 2";
List<Expression> currentArgs = a.getArguments();
if (currentArgs == null || currentArgs.isEmpty()) {
return a.withTemplate(
JavaTemplate.builder(this::getCursor, "#{}")
.build(),
a.getCoordinates().replaceArguments(),
param1,
param2);
}
return a;
}
};
}
}
package org.openrewrite.java;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.ExecutionContext;
import org.openrewrite.Recipe;
import org.openrewrite.java.tree.Expression;
import org.openrewrite.java.tree.J;
import java.util.List;
@Value
@EqualsAndHashCode(callSuper = true)
public class AddOrUpdateAnnotationAttribute extends Recipe {
@Override
public String getDisplayName() {
return "Add or update annotation attribute";
}
@Override
public String getDescription() {
return "Add or update annotation attribute.";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new JavaIsoVisitor<ExecutionContext>() {
@Override
public J.Annotation visitAnnotation(J.Annotation a, ExecutionContext context) {
String param1 = "test parameter 1";
String param2 = "test parameter 2";
List<Expression> currentArgs = a.getArguments();
if (currentArgs == null || currentArgs.isEmpty()) {
return JavaTemplate.builder("#{}")/*[Rewrite8 migration]`contextSensitive()` could be unnecessary and can be removed, please double-check manually*/.contextSensitive()
.build().apply(/*[Rewrite8 migration] please double-check correctness of this parameter manually, it could be updateCursor() if the value is updated somewhere*/getCursor(),
a.getCoordinates().replaceArguments(),
param1,
param2);
}
return a;
}
};
}
}

Cleanup recipes have been moved

All cleanup recipes have been moved to the rewrite-static-analysis repository. The package name has also changed from org.openrewrite.java.cleanup to org.openrewrite.staticanalysis.

visitJavaSource file was removed

Example:
Before
After
package org.openrewrite.staticanalysis;
import org.openrewrite.*;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.RenameVariable;
import org.openrewrite.java.tree.Flag;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaSourceFile;
import java.time.Duration;
import java.util.*;
import static org.openrewrite.internal.NameCaseConvention.LOWER_CAMEL;
public class RenamePrivateFieldsToCamelCase extends Recipe {
@Override
public String getDisplayName() {
return "Reformat private field names to camelCase";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new RenameNonCompliantNames();
}
private static class RenameNonCompliantNames extends JavaIsoVisitor<ExecutionContext> {
@Override
public JavaSourceFile visitJavaSourceFile(JavaSourceFile cu, ExecutionContext ctx) {
Map<J.VariableDeclarations.NamedVariable, String> renameVariablesMap = new LinkedHashMap<>();
Set<String> hasNameSet = new HashSet<>();
getCursor().putMessage("RENAME_VARIABLES_KEY", renameVariablesMap);
getCursor().putMessage("HAS_NAME_KEY", hasNameSet);
super.visitJavaSourceFile(cu, ctx);
renameVariablesMap.forEach((key, value) -> {
if (!hasNameSet.contains(value) && !hasNameSet.contains(key.getSimpleName())) {
doAfterVisit(new RenameVariable<>(key, value));
hasNameSet.add(value);
}
});
return cu;
}
}
}
package org.openrewrite.staticanalysis;
import org.openrewrite.*;
import org.openrewrite.internal.lang.Nullable;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.RenameVariable;
import org.openrewrite.java.tree.Flag;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaSourceFile;
import java.time.Duration;
import java.util.*;
import static org.openrewrite.internal.NameCaseConvention.LOWER_CAMEL;
public class RenamePrivateFieldsToCamelCase extends Recipe {
@Override
public String getDisplayName() {
return "Reformat private field names to camelCase";
}
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return new RenameNonCompliantNames();
}
private static class RenameNonCompliantNames extends JavaIsoVisitor<ExecutionContext> {
@Override
public @Nullable J visit(@Nullable Tree tree, ExecutionContext ctx) {
if (tree instanceof JavaSourceFile) {
JavaSourceFile cu = (JavaSourceFile) tree;
Map<J.VariableDeclarations.NamedVariable, String> renameVariablesMap = new LinkedHashMap<>();
Set<String> hasNameSet = new HashSet<>();
getCursor().putMessage("RENAME_VARIABLES_KEY", renameVariablesMap);
getCursor().putMessage("HAS_NAME_KEY", hasNameSet);
super.visit(cu, ctx);
renameVariablesMap.forEach((key, value) -> {
if (!hasNameSet.contains(value) && !hasNameSet.contains(key.getSimpleName())) {
doAfterVisit(new RenameVariable<>(key, value));
hasNameSet.add(value);
}
});
}
return super.visit(tree, ctx);
}
}
}

searchResult methods have been removed

The deprecated Markers#searchResult methods have been removed in favor of the SearchResult#found methods.
Example:
Before
After
package org.openrewrite.kubernetes.resource;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
@Value
@EqualsAndHashCode(callSuper = true)
public class FindExceedsResourceRatio extends Recipe {
@Override
public String getDisplayName() {
return "Find exceeds resource ratio";
}
@Override
protected TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
return e.withMarkers(e.getMarkers().searchResult("foo"));
}
};
}
}
package org.openrewrite.kubernetes.resource;
import lombok.EqualsAndHashCode;
import lombok.Value;
import org.openrewrite.*;
import org.openrewrite.marker.SearchResult;
import org.openrewrite.yaml.YamlIsoVisitor;
import org.openrewrite.yaml.tree.Yaml;
@Value
@EqualsAndHashCode(callSuper = true)
public class FindExceedsResourceRatio extends Recipe {
@Override
public String getDisplayName() {
return "Find exceeds resource ratio";
}
@Override
protected TreeVisitor<?, ExecutionContext> getVisitor() {
return new YamlIsoVisitor<ExecutionContext>() {
@Override
public Yaml.Mapping.Entry visitMappingEntry(Yaml.Mapping.Entry entry, ExecutionContext ctx) {
Yaml.Mapping.Entry e = super.visitMappingEntry(entry, ctx);
return SearchResult.found(e, "foo");
}
};
}
}

The doNext method has been removed

The org.openrewrite.Recipe doNext(..) method has been removed. In most situations, it should be replaced with TreeVisitor#doAfterVisit(Visitor).
However, as the doAfterVisit method only changes the current source file, if your recipe needs to change other files, it will need to be rewritten as a ScanningRecipe.
Similarly, the doAfterVisit(Recipe) method has been removed in favor of doAfterVisit(Visitor).

Migrating your recipes

We've created a migration recipe that will assist you with migrating your recipes to the latest version. You should run this recipe against all of your existing recipes. If the migration recipe is not able to fully migrate your recipe, comments will be added to the code to request a human to review it.
Do not attempt to bump the version of OpenRewrite or any of your dependencies before running this recipe. The flow for the upgrade should look like this:
  1. 1.
    Go to the MigrateToRewrite8 recipe doc. In the usage section, there are instructions for how to add this recipe to your repository. Either add it directly to your Maven or Gradle project or use the Maven command line.
  2. 2.
    Run the recipe.
  3. 3.
    Look over the recipe. Some pieces may have been directly changed whereas other parts may have just had comments added.
  4. 4.
    Bump your dependencies to the latest version and attempt to address the comments.
  5. 5.
    Run your tests and recipe to ensure it continues working as expected. Make changes until it works.
  6. 6.
    If you have questions or run into issues, please reach out in the community Slack
You can find some examples of how this migration recipe works in the Migrate Rewrite recipes from 7 to 8 recipe page

New Artifacts

  • rewrite-analysis
  • rewrite-cucumber-jvm
  • rewrite-hibernate
  • rewrite-recommendations
  • rewrite-sql
  • rewrite-static-analysis

New recipes