OpenRewrite recipe authoring patterns and API best practices. Use when writing or editing OpenRewrite recipe Java source code (visitors, matchers, type checks, templates, metadata, YAML config, list transformations, at-scale validation).
// BAD: separate matcher per variant
private static final MethodMatcher IS_TRACE_ENABLED = new MethodMatcher("org.slf4j.Logger isTraceEnabled()");
private static final MethodMatcher IS_DEBUG_ENABLED = new MethodMatcher("org.slf4j.Logger isDebugEnabled()");
// ... repeated for info, warn, error, plus marker overloads = 10 matchers
// GOOD: single wildcard matcher
private static final MethodMatcher IS_X_ENABLED = new MethodMatcher("org.slf4j.Logger is*Enabled(..)");
This also simplifies Preconditions.check():
// BAD
Preconditions.check(or(
new UsesMethod<>(IS_TRACE_ENABLED),
new UsesMethod<>(IS_DEBUG_ENABLED),
// ... 8 more
), visitor);
// GOOD
Preconditions.check(new UsesMethod<>(IS_X_ENABLED), visitor);
Instead of manually checking method name and receiver type:
// BAD
if (!"getMessage".equals(method.getSimpleName())) return false;
Expression select = method.getSelect();
if (select == null) return false;
return TypeUtils.isAssignableTo("java.lang.Throwable", select.getType());
// GOOD
private static final MethodMatcher GET_MESSAGE = new MethodMatcher("java.lang.Throwable getMessage()");
// then: GET_MESSAGE.matches(argument)
isOfClassType() instead of manual FQN comparison// BAD
JavaType.FullyQualified type = TypeUtils.asFullyQualified(select.getType());
return type != null && "org.slf4j.Logger".equals(type.getFullyQualifiedName());
// GOOD
TypeUtils.isOfClassType(select.getType(), "org.slf4j.Logger")
TypeUtils.isOfType() instead of FQN string equality// BAD
currentType.getFullyQualifiedName().equals(targetType.getFullyQualifiedName())
// GOOD
TypeUtils.isOfType(currentType, targetType)
isAssignableTo()When matching a member's declaring type against the current class, check both exact match and subtype relationship. Without this, inherited members get incorrectly attributed to the superclass:
// BAD: misses inherited members
if (TypeUtils.isOfType(currentType, declaringType)) { ... }
// GOOD: handles both direct and inherited members
if (TypeUtils.isOfType(currentType, declaringType) ||
TypeUtils.isAssignableTo(declaringType.getFullyQualifiedName(), currentType)) { ... }
Use the FQN-based isAssignableTo overload to handle parameterized types correctly.
instanceof JavaType.FullyQualified not JavaType.ClassJavaType.Class extends JavaType.FullyQualified, so checking for the parent type is broader and more correct:
// BAD: too narrow
if (fieldType.getOwner() instanceof JavaType.Class)
// GOOD: covers more cases
if (fieldType.getOwner() instanceof JavaType.FullyQualified)
ListUtils.flatMap() instead of manual ArrayList + modified flag// BAD
List<Statement> newStatements = new ArrayList<>();
boolean modified = false;
for (Statement stmt : visited.getStatements()) {
if (shouldTransform(stmt)) {
newStatements.addAll(extractStatements(stmt));
modified = true;
} else {
newStatements.add(stmt);
}
}
if (modified) return visited.withStatements(newStatements);
return visited;
// GOOD
return visited.withStatements(ListUtils.flatMap(visited.getStatements(), stmt -> {
if (shouldTransform(stmt)) {
return extractStatements(stmt); // return List = replace with multiple
}
return stmt; // return single item = keep as-is
}));
ListUtils.map() and ListUtils.mapFirst() for whitespace adjustmentsList<Statement> bodyStatements = ListUtils.map(
extractStatements(ifStmt.getThenPart()),
st -> st.withPrefix(Space.build(whitespace, emptyList())));
return ListUtils.mapFirst(bodyStatements,
first -> first.withPrefix(ifStmt.getPrefix()));
When recipes run in a composition (e.g., Slf4jBestPractices), earlier recipes transform the code before later ones see it. Don't handle cases that earlier recipes already cover.
Example: RemoveUnnecessaryLogLevelGuards should NOT treat string concatenation ("Name: " + name) as safe to unguard. The ParameterizedLogging recipe runs first and converts concatenation to parameterized form. If concatenation still exists when the guard-removal recipe runs, the guard is still needed for performance.
Always test that the recipe correctly preserves code that should not be changed, not just that it transforms code that should be changed.
JavaElementFactory for common nodes// BAD: verbose manual construction
new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, emptyList(), "this", ownerType, null)
// GOOD: factory method
JavaElementFactory.newThis(ownerType)
Flag enum and modifier helpers instead of magic bitmasks// BAD: magic number
(fieldType.getFlagsBitMap() & 0x0008L) != 0
// GOOD: readable API
fieldType.hasFlags(Flag.Static)
method.hasModifier(J.Modifier.Type.Static)
Java-specific recipes will also run on Kotlin files unless explicitly excluded:
@Override
public TreeVisitor<?, ExecutionContext> getVisitor() {
return Preconditions.check(
Preconditions.not(new KotlinFileChecker<>()),
new MyJavaVisitor()
);
}
JavaTemplate template = JavaTemplate.builder("#{any()}.toArray(new #{}[0])")
.imports(fqn) // Declare the import
.build();
maybeAddImport() after applying a templateAfter applying a template that uses a type, add the import to the compilation unit:
Expression result = template.apply(...);
maybeAddImport(fqn); // Add the import to the source file
Use JavaIsoVisitor when returning the same LST element type you're visiting (most common for simple transformations):
@Override
public J.TypeCast visitTypeCast(J.TypeCast typeCast, ExecutionContext ctx) {
J.TypeCast tc = super.visitTypeCast(typeCast, ctx);
// ... transform ...
return tc; // Still a J.TypeCast
}
Use JavaVisitor when you need to return a different LST element type (e.g., unwrapping parentheses):
@Override
public J visitParentheses(J.Parentheses parentheses, ExecutionContext ctx) {
// ... some logic ...
return someExpression; // Not a J.Parentheses
}
When dealing with expressions that might be parenthesized, visit J.Parentheses nodes too.
return visitedParentheses.withTree(result); // Preserves parentheses structure and prefix
@Override
public Set<String> getTags() {
return Collections.singleton("RSPEC-S3020");
}
Use the same time estimate from the SonarQube definition:
@Override
public Duration getEstimatedEffortPerOccurrence() {
return Duration.ofMinutes(2);
}
Don't forget to add new recipes to relevant YAML files:
recipeList:
- org.openrewrite.staticanalysis.CollectionToArrayShouldHaveProperType
Common collections:
common-static-analysis.yml - General static analysis fixesjava-best-practices.yml - Java-specific best practicesstatic-analysis.yml - Broader static analysis recipesBefore submitting, run at scale against large codebases (e.g., Spring, Netflix orgs). This catches bugs unit tests miss:
SuperClass.this.method() instead of this.method())java.util.List not java.util.*)Collections.emptyList() and singletonList()@Nullable from org.jspecify.annotations on methods that can return null@NonNull on parameters (non-null is the default)