Help us improve
Share bugs, ideas, or general feedback.
From java
Provides patterns and best practices for authoring OpenRewrite recipes in Java, covering MethodMatchers, TypeUtils, visitors, templates, metadata, YAML config, and validation. Use when writing or editing recipe code.
npx claudepluginhub motlin/claude-code-plugins --plugin javaHow this skill is triggered — by the user, by Claude, or both
Slash command
/java:openrewrite-recipesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
```java
Maintains OpenRewrite recipe tests by fixing import ordering failures, type validation issues, IDE warnings, test failures, and writing comprehensive recipe tests.
Detects Java version from pom.xml or build.gradle and suggests refactorings like lambdas (8+), streams (8+), records (16+), pattern matching (17+), plus universal changes like method extraction and dead code removal.
Sets up ast-grep in TypeScript codebases with rules detecting anti-patterns, enforcing best practices, and preventing bugs. Creates sgconfig.yml, rule files, and tests for structural linting, legacy bans, and ratchet gates.
Share bugs, ideas, or general feedback.
// 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)