+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.jspecify.annotations.NonNull;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Preconditions;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.search.FindAnnotations;
+import org.openrewrite.java.search.UsesJavaVersion;
+import org.openrewrite.java.search.UsesType;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.TypeUtils;
+
+import java.time.Duration;
+import java.util.Comparator;
+
+public class AddSerialAnnotationToSerialVersionUID extends Recipe {
+ @Override
+ public String getDisplayName() {
+ return "Add `@Serial` annotation to `serialVersionUID`";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Annotation any `serialVersionUID` fields with `@Serial` to indicate it's part of the serialization mechanism.";
+ }
+
+ @Override
+ public Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(1);
+ }
+
+ @Override
+ @NonNull
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return Preconditions.check(
+ Preconditions.and(
+ new UsesJavaVersion<>(14),
+ new UsesType<>("java.io.Serializable", true)
+ ),
+ new JavaIsoVisitor() {
+ @Override
+ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
+ if (TypeUtils.isAssignableTo("java.io.Serializable", classDecl.getType())) {
+ return super.visitClassDeclaration(classDecl, ctx);
+ }
+ return classDecl;
+ }
+
+ @Override
+ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
+ J.VariableDeclarations vd = super.visitVariableDeclarations(multiVariable, ctx);
+ if (isPrivateStaticFinalLongSerialVersionUID(vd) &&
+ FindAnnotations.find(vd, "@java.io.Serial").isEmpty()) {
+ maybeAddImport("java.io.Serial");
+ return JavaTemplate.builder("@Serial")
+ .imports("java.io.Serial")
+ .build()
+ .apply(getCursor(), vd.getCoordinates().addAnnotation(Comparator.comparing(J.Annotation::getSimpleName)));
+ }
+ return vd;
+ }
+
+ private boolean isPrivateStaticFinalLongSerialVersionUID(J.VariableDeclarations vd) {
+ return vd.hasModifier(J.Modifier.Type.Private) &&
+ vd.hasModifier(J.Modifier.Type.Static) &&
+ vd.hasModifier(J.Modifier.Type.Final) &&
+ TypeUtils.asPrimitive(vd.getType()) == JavaType.Primitive.Long &&
+ vd.getVariables().size() == 1 &&
+ "serialVersionUID".equals(vd.getVariables().get(0).getSimpleName());
+ }
+ }
+ );
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java
new file mode 100644
index 000000000..67e25a039
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializable.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.*;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.java.tree.Space;
+import org.openrewrite.java.tree.TypeUtils;
+import org.openrewrite.marker.Markers;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class AddSerialVersionUidToSerializable extends Recipe {
+
+ @Override
+ public String getDisplayName() {
+ return "Add `serialVersionUID` to a `Serializable` class when missing";
+ }
+
+ @Override
+ public String getDescription() {
+ return "A `serialVersionUID` field is strongly recommended in all `Serializable` classes. If this is not " +
+ "defined on a `Serializable` class, the compiler will generate this value. If a change is later made " +
+ "to the class, the generated value will change and attempts to deserialize the class will fail.";
+ }
+
+ @Override
+ public Set getTags() {
+ return Collections.singleton("RSPEC-S2057");
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return new JavaIsoVisitor() {
+ final JavaTemplate template = JavaTemplate.builder("private static final long serialVersionUID = 1;").build();
+
+ @Override
+ public J.MethodDeclaration visitMethodDeclaration(J.MethodDeclaration method, ExecutionContext ctx) {
+ // Anonymous classes are not of interest
+ return method;
+ }
+
+ @Override
+ public J.VariableDeclarations visitVariableDeclarations(J.VariableDeclarations multiVariable, ExecutionContext ctx) {
+ // Anonymous classes are not of interest
+ return multiVariable;
+ }
+
+ @Override
+ public J.ClassDeclaration visitClassDeclaration(J.ClassDeclaration classDecl, ExecutionContext ctx) {
+ J.ClassDeclaration c = super.visitClassDeclaration(classDecl, ctx);
+ if (c.getKind() != J.ClassDeclaration.Kind.Type.Class || !requiresSerialVersionField(classDecl.getType())) {
+ return c;
+ }
+ AtomicBoolean needsSerialVersionId = new AtomicBoolean(true);
+ J.Block body = c.getBody();
+ c = c.withBody(c.getBody().withStatements(ListUtils.map(c.getBody().getStatements(), s -> {
+ if (!(s instanceof J.VariableDeclarations)) {
+ return s;
+ }
+ J.VariableDeclarations varDecls = (J.VariableDeclarations) s;
+ for (J.VariableDeclarations.NamedVariable v : varDecls.getVariables()) {
+ if ("serialVersionUID".equals(v.getSimpleName())) {
+ needsSerialVersionId.set(false);
+ return maybeAutoFormat(varDecls, maybeFixVariableDeclarations(varDecls), ctx, new Cursor(getCursor(), body));
+ }
+ }
+ return s;
+ })));
+ if (needsSerialVersionId.get()) {
+ c = template.apply(updateCursor(c), c.getBody().getCoordinates().firstStatement());
+ }
+ return c;
+ }
+
+ private J.VariableDeclarations maybeFixVariableDeclarations(J.VariableDeclarations varDecls) {
+ List modifiers = varDecls.getModifiers();
+ if (!J.Modifier.hasModifier(modifiers, J.Modifier.Type.Private) ||
+ !J.Modifier.hasModifier(modifiers, J.Modifier.Type.Static) ||
+ !J.Modifier.hasModifier(modifiers, J.Modifier.Type.Final)) {
+ varDecls = varDecls.withModifiers(Arrays.asList(
+ new J.Modifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, null, J.Modifier.Type.Private, Collections.emptyList()),
+ new J.Modifier(Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, null, J.Modifier.Type.Static, Collections.emptyList()),
+ new J.Modifier(Tree.randomId(), Space.SINGLE_SPACE, Markers.EMPTY, null, J.Modifier.Type.Final, Collections.emptyList())
+ ));
+ }
+ if (TypeUtils.asPrimitive(varDecls.getType()) != JavaType.Primitive.Long) {
+ varDecls = varDecls.withTypeExpression(new J.Primitive(Tree.randomId(), Space.EMPTY, Markers.EMPTY, JavaType.Primitive.Long));
+ }
+ return varDecls;
+ }
+
+ private boolean requiresSerialVersionField(@Nullable JavaType type) {
+ if (type == null) {
+ return false;
+ } else if (type instanceof JavaType.Primitive) {
+ return true;
+ } else if (type instanceof JavaType.Array) {
+ return requiresSerialVersionField(((JavaType.Array) type).getElemType());
+ } else if (type instanceof JavaType.Parameterized) {
+ JavaType.Parameterized parameterized = (JavaType.Parameterized) type;
+ if (parameterized.isAssignableTo("java.util.Collection") || parameterized.isAssignableTo("java.util.Map")) {
+ //If the type is either a collection or a map, make sure the type parameters are serializable. We
+ //force all type parameters to be checked to correctly scoop up all non-serializable candidates.
+ boolean typeParametersSerializable = true;
+ for (JavaType typeParameter : parameterized.getTypeParameters()) {
+ typeParametersSerializable = typeParametersSerializable && requiresSerialVersionField(typeParameter);
+ }
+ return typeParametersSerializable;
+ }
+ //All other parameterized types fall through
+ } else if (type instanceof JavaType.FullyQualified) {
+ JavaType.FullyQualified fq = (JavaType.FullyQualified) type;
+ if (fq.getKind() == JavaType.Class.Kind.Enum) {
+ return false;
+ }
+
+ if (fq.getKind() != JavaType.Class.Kind.Interface &&
+ !fq.isAssignableTo("java.lang.Throwable")) {
+ return fq.isAssignableTo("java.io.Serializable");
+ }
+ }
+ return false;
+ }
+ };
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java
new file mode 100644
index 000000000..911d39ffd
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCalls.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.*;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaParser;
+import org.openrewrite.java.MethodMatcher;
+import org.openrewrite.java.search.UsesMethod;
+import org.openrewrite.java.tree.*;
+
+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)");
+
+ @SuppressWarnings("ALL") // Stop NoMutableStaticFieldsInRecipes from suggesting to remove this mutable static field
+ private static J.Binary additiveBinaryTemplate = null;
+
+ @Override
+ public String getDisplayName() {
+ return "Chain `StringBuilder.append()` calls";
+ }
+
+ @Override
+ public String getDescription() {
+ return "String concatenation within calls to `StringBuilder.append()` causes unnecessary memory allocation. Except for concatenations of String literals, which are joined together at compile time. Replaces inefficient concatenations with chained calls to `StringBuilder.append()`.";
+ }
+
+ @Override
+ public @Nullable Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(2);
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return Preconditions.check(new UsesMethod<>(STRING_BUILDER_APPEND), Repeat.repeatUntilStable(new JavaIsoVisitor() {
+ @Override
+ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
+ J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
+
+ if (STRING_BUILDER_APPEND.matches(m)) {
+ List arguments = m.getArguments();
+ if (arguments.size() != 1) {
+ return m;
+ }
+
+ List flattenExpressions = new ArrayList<>();
+ boolean flattenable = flatAdditiveExpressions(arguments.get(0).unwrap(), flattenExpressions);
+ if (!flattenable) {
+ return m;
+ }
+
+ if (flattenExpressions.stream().allMatch(J.Literal.class::isInstance)) {
+ return m;
+ }
+
+ // group expressions
+ List groups = new ArrayList<>();
+ List group = new ArrayList<>();
+ boolean appendToString = false;
+ for (Expression exp : flattenExpressions) {
+ if (appendToString) {
+ if (exp instanceof J.Literal &&
+ (((J.Literal) exp).getType() == JavaType.Primitive.String)
+ ) {
+ group.add(exp);
+ } else {
+ addToGroups(group, groups);
+ groups.add(exp);
+ }
+ } else {
+ if (exp instanceof J.Literal &&
+ (((J.Literal) exp).getType() == JavaType.Primitive.String)) {
+ addToGroups(group, groups);
+ appendToString = true;
+ } else if ((exp instanceof J.Identifier || exp instanceof J.MethodInvocation) && exp.getType() != null) {
+ JavaType.FullyQualified fullyQualified = TypeUtils.asFullyQualified(exp.getType());
+ if (fullyQualified != null && fullyQualified.getFullyQualifiedName().equals("java.lang.String")) {
+ addToGroups(group, groups);
+ appendToString = true;
+ }
+ }
+ group.add(exp);
+
+ }
+ }
+ addToGroups(group, groups);
+
+ J.MethodInvocation chainedMethods = m.withArguments(singletonList(groups.get(0)));
+ for (int i = 1; i < groups.size(); i++) {
+ chainedMethods = chainedMethods.withSelect(chainedMethods)
+ .withArguments(singletonList(groups.get(i).unwrap()))
+ .withPrefix(Space.EMPTY);
+ }
+
+ return chainedMethods;
+ }
+
+ return m;
+ }
+ }));
+ }
+
+ /**
+ * Concat two literals to an expression with '+' and surrounded with single space.
+ */
+ public static J.Binary concatAdditionBinary(Expression left, Expression right) {
+ J.Binary b = getAdditiveBinaryTemplate();
+ return b.withPrefix(b.getLeft().getPrefix())
+ .withLeft(left)
+ .withRight(right.withPrefix(Space.build(" " + right.getPrefix().getWhitespace(), emptyList())));
+ }
+
+ /**
+ * Concat expressions to an expression with '+' connected.
+ */
+ public static @Nullable Expression additiveExpression(Expression... expressions) {
+ Expression expression = null;
+ for (Expression element : expressions) {
+ if (element != null) {
+ expression = (expression == null) ? element : concatAdditionBinary(expression, element);
+ }
+ }
+ return expression;
+ }
+
+ public static @Nullable Expression additiveExpression(List expressions) {
+ return additiveExpression(expressions.toArray(new Expression[0]));
+ }
+
+ public static J.Binary getAdditiveBinaryTemplate() {
+ if (additiveBinaryTemplate == null) {
+ //noinspection OptionalGetWithoutIsPresent
+ J.CompilationUnit cu = JavaParser.fromJavaVersion()
+ .build()
+ .parse("class A { String s = \"A\" + \"B\";}")
+ .map(J.CompilationUnit.class::cast)
+ .findFirst()
+ .get();
+ additiveBinaryTemplate = (J.Binary) ((J.VariableDeclarations) cu.getClasses().get(0)
+ .getBody()
+ .getStatements().get(0))
+ .getVariables().get(0)
+ .getInitializer();
+ assert additiveBinaryTemplate != null;
+ }
+ return additiveBinaryTemplate;
+ }
+
+ /**
+ * Concat an additive expression in a group and add to groups
+ */
+ private static void addToGroups(List group, List groups) {
+ if (!group.isEmpty()) {
+ groups.add(additiveExpression(group));
+ group.clear();
+ }
+ }
+
+ public static boolean flatAdditiveExpressions(Expression expression, List expressionList) {
+ if (expression instanceof J.Binary) {
+ J.Binary b = (J.Binary) expression;
+ if (b.getOperator() != J.Binary.Type.Addition) {
+ return false;
+ }
+
+ return flatAdditiveExpressions(b.getLeft(), expressionList) &&
+ flatAdditiveExpressions(b.getRight(), expressionList);
+ } else if (expression instanceof J.Literal ||
+ expression instanceof J.Identifier ||
+ expression instanceof J.MethodInvocation ||
+ expression instanceof J.Parentheses) {
+ expressionList.add(expression.withPrefix(Space.EMPTY));
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java
new file mode 100644
index 000000000..5306421f6
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/InstanceOfPatternMatch.java
@@ -0,0 +1,527 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.*;
+import org.openrewrite.java.JavaVisitor;
+import org.openrewrite.java.VariableNameUtils;
+import org.openrewrite.java.search.SemanticallyEqual;
+import org.openrewrite.java.search.UsesJavaVersion;
+import org.openrewrite.java.tree.*;
+import org.openrewrite.java.tree.J.VariableDeclarations.NamedVariable;
+import org.openrewrite.marker.Markers;
+import org.openrewrite.staticanalysis.groovy.GroovyFileChecker;
+import org.openrewrite.staticanalysis.kotlin.KotlinFileChecker;
+
+import java.time.Duration;
+import java.util.*;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+import static org.openrewrite.Tree.randomId;
+import static org.openrewrite.java.VariableNameUtils.GenerationStrategy.INCREMENT_NUMBER;
+
+public class InstanceOfPatternMatch extends Recipe {
+
+ @Override
+ public String getDisplayName() {
+ return "Changes code to use Java 17's `instanceof` pattern matching";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Adds pattern variables to `instanceof` expressions wherever the same (side effect free) expression is referenced in a corresponding type cast expression within the flow scope of the `instanceof`." +
+ " Currently, this recipe supports `if` statements and ternary operator expressions.";
+ }
+
+ @Override
+ public Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(1);
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ TreeVisitor, ExecutionContext> preconditions = Preconditions.and(
+ new UsesJavaVersion<>(17),
+ Preconditions.not(new KotlinFileChecker<>()),
+ Preconditions.not(new GroovyFileChecker<>())
+ );
+
+ return Preconditions.check(preconditions, new JavaVisitor() {
+ @Override
+ public @Nullable J postVisit(J tree, ExecutionContext ctx) {
+ J result = super.postVisit(tree, ctx);
+ InstanceOfPatternReplacements original = getCursor().getMessage("flowTypeScope");
+ if (original != null && !original.isEmpty()) {
+ return UseInstanceOfPatternMatching.refactor(result, original, getCursor().getParentOrThrow());
+ }
+ return result;
+ }
+
+ @Override
+ public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, ExecutionContext ctx) {
+ instanceOf = (J.InstanceOf) super.visitInstanceOf(instanceOf, ctx);
+ if (instanceOf.getPattern() != null || !instanceOf.getSideEffects().isEmpty()) {
+ return instanceOf;
+ }
+
+ Cursor maybeReplacementRoot = null;
+ J additionalContext = null;
+ boolean flowScopeBreakEncountered = false;
+ for (Iterator it = getCursor().getPathAsCursors(); it.hasNext(); ) {
+ Cursor next = it.next();
+ Object value = next.getValue();
+ if (value instanceof J.Binary) {
+ J.Binary binary = (J.Binary) value;
+ if (!flowScopeBreakEncountered && binary.getOperator() == J.Binary.Type.And) {
+ additionalContext = binary;
+ } else {
+ flowScopeBreakEncountered = true;
+ }
+ } else if (value instanceof J.Unary && ((J.Unary) value).getOperator() == J.Unary.Type.Not) {
+ // TODO this could be improved (the pattern variable may be applicable in the else case
+ // or even in subsequent statements (due to the flow scope semantics)
+ flowScopeBreakEncountered = true;
+ } else if (value instanceof Statement) {
+ maybeReplacementRoot = next;
+ break;
+ }
+ }
+
+ if (maybeReplacementRoot != null) {
+ J root = maybeReplacementRoot.getValue();
+ Set contexts = new HashSet<>();
+ if (!flowScopeBreakEncountered) {
+ if (root instanceof J.If) {
+ contexts.add(((J.If) root).getThenPart());
+ } else if (root instanceof J.Ternary) {
+ contexts.add(((J.Ternary) root).getTruePart());
+ }
+ }
+ if (additionalContext != null) {
+ contexts.add(additionalContext);
+ }
+
+ if (!contexts.isEmpty()) {
+ InstanceOfPatternReplacements replacements = maybeReplacementRoot
+ .computeMessageIfAbsent("flowTypeScope", k -> new InstanceOfPatternReplacements(root));
+ replacements.registerInstanceOf(instanceOf, contexts);
+ }
+ }
+ return instanceOf;
+ }
+
+ @Override
+ public J visitTypeCast(J.TypeCast typeCast, ExecutionContext ctx) {
+ J result = super.visitTypeCast(typeCast, ctx);
+ if (result instanceof J.TypeCast) {
+ InstanceOfPatternReplacements replacements = getCursor().getNearestMessage("flowTypeScope");
+ if (replacements != null) {
+ replacements.registerTypeCast((J.TypeCast) result, getCursor());
+ }
+ }
+ return result;
+ }
+ });
+ }
+
+ private static class ExpressionAndType {
+ private final Expression expression;
+ private final JavaType type;
+ public ExpressionAndType(Expression expression, JavaType type) {
+ this.expression = expression;
+ this.type = type;
+ }
+ public Expression getExpression() {
+ return expression;
+ }
+ public JavaType getType() {
+ return type;
+ }
+
+ }
+
+ private static class VariableAndTypeTree {
+ private final J.VariableDeclarations.NamedVariable variable;
+ private final TypeTree type;
+ public VariableAndTypeTree(NamedVariable variable, TypeTree type) {
+ this.variable = variable;
+ this.type = type;
+ }
+ public J.VariableDeclarations.NamedVariable getVariable() {
+ return variable;
+ }
+ public TypeTree getType() {
+ return type;
+ }
+
+ }
+
+ private static class InstanceOfPatternReplacements {
+ private final J root;
+ private final Map instanceOfs = new HashMap<>();
+ private final Map> contexts = new HashMap<>();
+ private final Map> contextScopes = new HashMap<>();
+ private final Map replacements = new HashMap<>();
+ private final Map variablesToDelete = new HashMap<>();
+
+ public InstanceOfPatternReplacements(J root) {
+ this.root = root;
+ }
+
+ public void registerInstanceOf(J.InstanceOf instanceOf, Set contexts) {
+ Expression expression = instanceOf.getExpression();
+ JavaType type = ((TypedTree) instanceOf.getClazz()).getType();
+ if (type == null) {
+ return;
+ }
+
+ Optional existing = instanceOfs.keySet().stream()
+ .filter(k -> TypeUtils.isAssignableTo(type, k.getType()) &&
+ SemanticallyEqual.areEqual(k.getExpression(), expression))
+ .findAny();
+ if (!existing.isPresent()) {
+ instanceOfs.put(new ExpressionAndType(expression, type), instanceOf);
+ this.contexts.put(instanceOf, contexts);
+ }
+ }
+
+ public void registerTypeCast(J.TypeCast typeCast, Cursor cursor) {
+ Expression expression = typeCast.getExpression();
+ JavaType type = typeCast.getClazz().getTree().getType();
+
+ Optional match = instanceOfs.keySet().stream()
+ .filter(k -> TypeUtils.isAssignableTo(type, k.getType()) &&
+ SemanticallyEqual.areEqual(k.getExpression(), expression))
+ .findAny();
+ if (match.isPresent()) {
+ Cursor parent = cursor.getParentTreeCursor();
+ J.InstanceOf instanceOf = instanceOfs.get(match.get());
+ Set validContexts = contexts.get(instanceOf);
+ for (Iterator> it = cursor.getPath(); it.hasNext(); ) {
+ Object next = it.next();
+ if (validContexts.contains(next)) {
+ if (isAcceptableTypeCast(typeCast) && isTheSameAsOtherTypeCasts(typeCast, instanceOf)) {
+ if (parent.getValue() instanceof J.VariableDeclarations.NamedVariable &&
+ !variablesToDelete.containsKey(instanceOf)) {
+ variablesToDelete.put(instanceOf, new VariableAndTypeTree(parent.getValue(),
+ requireNonNull(parent.firstEnclosing(J.VariableDeclarations.class).getTypeExpression())));
+ } else {
+ replacements.put(typeCast, instanceOf);
+ }
+ contextScopes.computeIfAbsent(instanceOf, k -> new HashSet<>()).add(cursor);
+ } else {
+ replacements.entrySet().removeIf(e -> e.getValue() == instanceOf);
+ variablesToDelete.remove(instanceOf);
+ contextScopes.remove(instanceOf);
+ contexts.remove(instanceOf);
+ instanceOfs.entrySet().removeIf(e -> e.getValue() == instanceOf);
+ }
+ break;
+ } else if (root == next) {
+ break;
+ }
+ }
+ }
+ }
+
+ private boolean isAcceptableTypeCast(J.TypeCast typeCast) {
+ TypeTree typeTree = typeCast.getClazz().getTree();
+ if (typeTree instanceof J.ParameterizedType) {
+ return requireNonNull(((J.ParameterizedType) typeTree).getTypeParameters()).stream().allMatch(J.Wildcard.class::isInstance);
+ }
+ return true;
+ }
+
+ private boolean isTheSameAsOtherTypeCasts(J.TypeCast typeCast, J.InstanceOf instanceOf) {
+ return replacements
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue() == instanceOf)
+ .findFirst()
+ .map(e -> e.getKey().getType().equals(typeCast.getType()))
+ .orElse(true);
+ }
+
+ public boolean isEmpty() {
+ return replacements.isEmpty() && variablesToDelete.isEmpty();
+ }
+
+ public J.InstanceOf processInstanceOf(J.InstanceOf instanceOf, Cursor cursor) {
+ if (!contextScopes.containsKey(instanceOf)) {
+ return instanceOf;
+ }
+ JavaType type = ((TypedTree) instanceOf.getClazz()).getType();
+ String name = patternVariableName(instanceOf, cursor);
+ J.InstanceOf result = instanceOf.withPattern(new J.Identifier(
+ randomId(),
+ Space.build(" ", emptyList()),
+ Markers.EMPTY,
+ emptyList(),
+ name,
+ type,
+ null));
+
+ J currentTypeTree = instanceOf.getClazz();
+ TypeTree typeCastTypeTree = computeTypeTreeFromTypeCasts(instanceOf);
+ // If type tree from type cast is not parameterized then NVM. Instance of should already have proper type
+ if (typeCastTypeTree instanceof J.ParameterizedType) {
+ J.ParameterizedType parameterizedType = (J.ParameterizedType) typeCastTypeTree;
+ result = result.withClazz(parameterizedType.withId(Tree.randomId()).withPrefix(currentTypeTree.getPrefix()));
+ }
+
+ // update entry in replacements to share the pattern variable name
+ for (Map.Entry entry : replacements.entrySet()) {
+ if (entry.getValue() == instanceOf) {
+ entry.setValue(result);
+ }
+ }
+ return result;
+ }
+
+ private TypeTree computeTypeTreeFromTypeCasts(J.InstanceOf instanceOf) {
+ TypeTree typeCastTypeTree = replacements
+ .entrySet()
+ .stream()
+ .filter(e -> e.getValue() == instanceOf)
+ .findFirst()
+ .map(e -> e.getKey().getClazz().getTree())
+ .orElse(null);
+ if (typeCastTypeTree == null) {
+ VariableAndTypeTree variable = variablesToDelete.get(instanceOf);
+ if (variable != null) {
+ typeCastTypeTree = variable.getType();
+ }
+ }
+ return typeCastTypeTree;
+ }
+
+ private String patternVariableName(J.InstanceOf instanceOf, Cursor cursor) {
+ VariableNameStrategy strategy;
+ if (root instanceof J.If) {
+ VariableAndTypeTree variableData = variablesToDelete.get(instanceOf);
+ strategy = variableData != null ?
+ VariableNameStrategy.exact(variableData.getVariable().getSimpleName()) :
+ VariableNameStrategy.normal(contextScopes.get(instanceOf));
+ } else {
+ strategy = VariableNameStrategy.short_();
+ }
+ String baseName = strategy.variableName(((TypeTree) instanceOf.getClazz()).getType());
+ return VariableNameUtils.generateVariableName(baseName, cursor, INCREMENT_NUMBER);
+ }
+
+ public @Nullable J processTypeCast(J.TypeCast typeCast, Cursor cursor) {
+ J.InstanceOf instanceOf = replacements.get(typeCast);
+ if (instanceOf != null && instanceOf.getPattern() != null) {
+ String name = ((J.Identifier) instanceOf.getPattern()).getSimpleName();
+ TypedTree owner = cursor.firstEnclosing(J.MethodDeclaration.class);
+ owner = owner != null ? owner : cursor.firstEnclosingOrThrow(J.ClassDeclaration.class);
+ JavaType.Variable fieldType = new JavaType.Variable(null, Flag.Default.getBitMask(), name, owner.getType(), typeCast.getType(), emptyList());
+ return new J.Identifier(
+ randomId(),
+ typeCast.getPrefix(),
+ Markers.EMPTY,
+ emptyList(),
+ name,
+ typeCast.getType(),
+ fieldType);
+ }
+ return null;
+ }
+
+ public @Nullable J processVariableDeclarations(J.VariableDeclarations multiVariable) {
+ return multiVariable.getVariables().stream().anyMatch(v -> variablesToDelete.values().stream().anyMatch(vd -> vd.getVariable() == v)) ? null : multiVariable;
+ }
+ }
+
+ private static class UseInstanceOfPatternMatching extends JavaVisitor {
+
+ private final InstanceOfPatternReplacements replacements;
+
+ public UseInstanceOfPatternMatching(InstanceOfPatternReplacements replacements) {
+ this.replacements = replacements;
+ }
+
+ static @Nullable J refactor(@Nullable J tree, InstanceOfPatternReplacements replacements, Cursor cursor) {
+ return new UseInstanceOfPatternMatching(replacements).visit(tree, 0, cursor);
+ }
+
+ @Override
+ public J visitBinary(J.Binary original, Integer integer) {
+ Expression newLeft = (Expression) super.visitNonNull(original.getLeft(), integer);
+ if (newLeft != original.getLeft()) {
+ // The left side changed, so the right side should see any introduced variable names
+ J.Binary replacement = original.withLeft(newLeft);
+ Cursor widenedCursor = updateCursor(replacement);
+
+ Expression newRight;
+ if (original.getRight() instanceof J.InstanceOf) {
+ newRight = replacements.processInstanceOf((J.InstanceOf) original.getRight(), widenedCursor);
+ } else if (original.getRight() instanceof J.Parentheses &&
+ ((J.Parentheses>) original.getRight()).getTree() instanceof J.InstanceOf) {
+ @SuppressWarnings("unchecked")
+ J.Parentheses originalRight = (J.Parentheses) original.getRight();
+ newRight = originalRight.withTree(replacements.processInstanceOf(originalRight.getTree(), widenedCursor));
+ } else {
+ newRight = (Expression) super.visitNonNull(original.getRight(), integer, widenedCursor);
+ }
+ return replacement.withRight(newRight);
+ }
+ // The left side didn't change, so the right side doesn't need to see any introduced variable names
+ return super.visitBinary(original, integer);
+ }
+
+ @Override
+ public J.InstanceOf visitInstanceOf(J.InstanceOf instanceOf, Integer executionContext) {
+ instanceOf = (J.InstanceOf) super.visitInstanceOf(instanceOf, executionContext);
+ instanceOf = replacements.processInstanceOf(instanceOf, getCursor());
+ return instanceOf;
+ }
+
+ @Override
+ public J visitParentheses(J.Parentheses parens, Integer executionContext) {
+ if (parens.getTree() instanceof J.TypeCast) {
+ J replacement = replacements.processTypeCast((J.TypeCast) parens.getTree(), getCursor());
+ if (replacement != null) {
+ return replacement.withPrefix(parens.getPrefix());
+ }
+ }
+ return super.visitParentheses(parens, executionContext);
+ }
+
+ @Override
+ public J visitTypeCast(J.TypeCast typeCast, Integer executionContext) {
+ typeCast = (J.TypeCast) super.visitTypeCast(typeCast, executionContext);
+ J replacement = replacements.processTypeCast(typeCast, getCursor());
+ if (replacement != null) {
+ return replacement;
+ }
+ return typeCast;
+ }
+
+ @Override
+ public @Nullable J visitVariableDeclarations(J.VariableDeclarations multiVariable, Integer integer) {
+ multiVariable = (J.VariableDeclarations) super.visitVariableDeclarations(multiVariable, integer);
+ return replacements.processVariableDeclarations(multiVariable);
+ }
+ }
+
+ private static class VariableNameStrategy {
+ public static final Pattern NAME_SPLIT_PATTERN = Pattern.compile("[$._]*(?=\\p{Upper}+[\\p{Lower}\\p{Digit}]*)");
+ private final Style style;
+
+ @Nullable
+ private final String name;
+
+ private final Set contextScopes;
+
+ enum Style {
+ SHORT, NORMAL, EXACT
+ }
+
+ private VariableNameStrategy(Style style, @Nullable String exactName, Set contextScopes) {
+ this.style = style;
+ this.name = exactName;
+ this.contextScopes = contextScopes;
+ }
+
+ static VariableNameStrategy short_() {
+ return new VariableNameStrategy(Style.SHORT, null, Collections.emptySet());
+ }
+
+ static VariableNameStrategy normal(Set contextScopes) {
+ return new VariableNameStrategy(Style.NORMAL, null, contextScopes);
+ }
+
+ static VariableNameStrategy exact(String name) {
+ return new VariableNameStrategy(Style.EXACT, name, Collections.emptySet());
+ }
+
+ public String variableName(@Nullable JavaType type) {
+ // the instanceof operator only accepts classes (without generics) and arrays
+ if (style == Style.EXACT) {
+ //noinspection DataFlowIssue
+ return name;
+ } else if (type instanceof JavaType.FullyQualified) {
+ String className = ((JavaType.FullyQualified) type).getClassName();
+ className = className.substring(className.lastIndexOf('.') + 1);
+ String baseName = null;
+ switch (style) {
+ case SHORT:
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < className.length(); i++) {
+ char c = className.charAt(i);
+ if (Character.isUpperCase(c)) {
+ builder.append(Character.toLowerCase(c));
+ }
+ }
+ baseName = builder.length() > 0 ? builder.toString() : "o";
+ break;
+ case NORMAL:
+ Set namesInScope = contextScopes.stream()
+ .flatMap(c -> VariableNameUtils.findNamesInScope(c).stream())
+ .collect(Collectors.toSet());
+ List nameSegments = Stream.of(NAME_SPLIT_PATTERN.split(className))
+ .filter(s -> !s.isEmpty()).collect(Collectors.toList());
+ for (int i = nameSegments.size() - 1; i >= 0; i--) {
+ String name = String.join("", nameSegments.subList(i, nameSegments.size()));
+ if (name.length() < 2) {
+ continue;
+ }
+ name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
+ if (!namesInScope.contains(name)) {
+ baseName = name;
+ break;
+ }
+ }
+ if (baseName == null) {
+ baseName = Character.toLowerCase(className.charAt(0)) + className.substring(1);
+ }
+ break;
+ default:
+ baseName = "obj";
+ }
+ String candidate = baseName;
+ OUTER:
+ while (true) {
+ for (Cursor scope : contextScopes) {
+ String newCandidate = VariableNameUtils.generateVariableName(candidate, scope, INCREMENT_NUMBER);
+ if (!newCandidate.equals(candidate)) {
+ candidate = newCandidate;
+ continue OUTER;
+ }
+ }
+ break;
+ }
+ return candidate;
+ } else if (type instanceof JavaType.Primitive) {
+ String keyword = ((JavaType.Primitive) type).getKeyword();
+ return style == Style.SHORT ? keyword.substring(0, 1) : keyword;
+ } else if (type instanceof JavaType.Array) {
+ JavaType elemType = ((JavaType.Array) type).getElemType();
+ while (elemType instanceof JavaType.Array) {
+ elemType = ((JavaType.Array) elemType).getElemType();
+ }
+ return variableName(elemType) + 's';
+ }
+ return style == Style.SHORT ? "o" : "obj";
+ }
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java
new file mode 100644
index 000000000..129960d7b
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/RemoveUnneededBlock.java
@@ -0,0 +1,100 @@
+//
+// Source code recreated from a .class file by IntelliJ IDEA
+// (powered by FernFlower decompiler)
+//
+
+package org.openrewrite.staticanalysis;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Incubating;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.java.JavaVisitor;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.Statement;
+
+@Incubating(
+ since = "7.21.0"
+)
+public class RemoveUnneededBlock extends Recipe {
+ public RemoveUnneededBlock() {
+ }
+
+ public String getDisplayName() {
+ return "Remove unneeded block";
+ }
+
+ public String getDescription() {
+ return "Flatten blocks into inline statements when possible.";
+ }
+
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return new RemoveUnneededBlockStatementVisitor();
+ }
+
+ static class RemoveUnneededBlockStatementVisitor extends JavaVisitor {
+ RemoveUnneededBlockStatementVisitor() {
+ }
+
+ public J.Block visitBlock(J.Block block, ExecutionContext ctx) {
+ J.Block bl = (J.Block)super.visitBlock(block, ctx);
+ J directParent = (J)this.getCursor().getParentTreeCursor().getValue();
+ return !(directParent instanceof J.NewClass) && !(directParent instanceof J.ClassDeclaration) ? this.maybeInlineBlock(bl, ctx) : bl;
+ }
+
+ private J.Block maybeInlineBlock(J.Block block, ExecutionContext ctx) {
+ List statements = block.getStatements();
+ if (statements.isEmpty()) {
+ return block;
+ } else {
+ Statement lastStatement = (Statement)statements.get(statements.size() - 1);
+ J.Block flattened = block.withStatements(ListUtils.flatMap(statements, (i, stmt) -> {
+ J.Block nested;
+ if (stmt instanceof J.Try) {
+ J.Try _try = (J.Try)stmt;
+ if (_try.getResources() != null || !_try.getCatches().isEmpty() || _try.getFinally() == null || !_try.getFinally().getStatements().isEmpty()) {
+ return stmt;
+ }
+
+ nested = _try.getBody();
+ } else {
+ if (!(stmt instanceof J.Block)) {
+ return stmt;
+ }
+
+ nested = (J.Block)stmt;
+ }
+
+ if (i < statements.size() - 1) {
+ Stream var10000 = nested.getStatements().stream();
+ Objects.requireNonNull(J.VariableDeclarations.class);
+ if (var10000.anyMatch(J.VariableDeclarations.class::isInstance)) {
+ return stmt;
+ }
+ }
+
+ return ListUtils.map(nested.getStatements(), (j, inlinedStmt) -> {
+ if (j == 0) {
+ inlinedStmt = (Statement)inlinedStmt.withPrefix(inlinedStmt.getPrefix().withComments(ListUtils.concatAll(nested.getComments(), inlinedStmt.getComments())));
+ }
+
+ return (Statement)this.autoFormat(inlinedStmt, ctx, this.getCursor());
+ });
+ }));
+ if (flattened == block) {
+ return block;
+ } else {
+ if (lastStatement instanceof J.Block) {
+ flattened = flattened.withEnd(flattened.getEnd().withComments(ListUtils.concatAll(((J.Block)lastStatement).getEnd().getComments(), flattened.getEnd().getComments())));
+ }
+
+ return flattened;
+ }
+ }
+ }
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java
new file mode 100644
index 000000000..fb3e293db
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/ReplaceDeprecatedRuntimeExecMethods.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.openrewrite.*;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.MethodMatcher;
+import org.openrewrite.java.search.UsesJavaVersion;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaType;
+
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+public class ReplaceDeprecatedRuntimeExecMethods extends Recipe {
+ private static final MethodMatcher RUNTIME_EXEC_CMD = new MethodMatcher("java.lang.Runtime exec(String)");
+ private static final MethodMatcher RUNTIME_EXEC_CMD_ENVP = new MethodMatcher("java.lang.Runtime exec(String, String[])");
+ private static final MethodMatcher RUNTIME_EXEC_CMD_ENVP_FILE = new MethodMatcher("java.lang.Runtime exec(String, String[], java.io.File)");
+
+ @Override
+ public String getDisplayName() {
+ return "Replace deprecated `Runtime#exec()` methods";
+ }
+
+ @Override
+ public String getDescription() {
+ return "Replace `Runtime#exec(String)` methods to use `exec(String[])` instead because the former is deprecated " +
+ "after Java 18 and is no longer recommended for use by the Java documentation.";
+ }
+
+ @Override
+ public Duration getEstimatedEffortPerOccurrence() {
+ return Duration.ofMinutes(3);
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return Preconditions.check(new UsesJavaVersion<>(18), new JavaIsoVisitor() {
+ @Override
+ public J.MethodInvocation visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
+ J.MethodInvocation m = super.visitMethodInvocation(method, ctx);
+
+ if (RUNTIME_EXEC_CMD.matches(m) || RUNTIME_EXEC_CMD_ENVP.matches(m) || RUNTIME_EXEC_CMD_ENVP_FILE.matches(m)) {
+ Expression command = m.getArguments().get(0);
+ List commands = new ArrayList<>();
+ boolean flattenAble = ChainStringBuilderAppendCalls.flatAdditiveExpressions(command, commands);
+
+ StringBuilder sb = new StringBuilder();
+ if (flattenAble) {
+ for (Expression e : commands) {
+ if (e instanceof J.Literal && ((J.Literal) e).getType() == JavaType.Primitive.String) {
+ sb.append(((J.Literal) e).getValue());
+ } else {
+ flattenAble = false;
+ break;
+ }
+ }
+ }
+
+ updateCursor(m);
+ if (flattenAble) {
+ String[] cmds = sb.toString().split(" ");
+ String templateCode = String.format("new String[] {%s}", toStringArguments(cmds));
+ JavaTemplate template = JavaTemplate.builder(templateCode).build();
+
+ List args = m.getArguments();
+ Cursor cursor = new Cursor(getCursor(), args.get(0));
+ args.set(0, template.apply(cursor, args.get(0).getCoordinates().replace()));
+
+ if (m.getMethodType() != null) {
+ List parameterTypes = m.getMethodType().getParameterTypes();
+ parameterTypes.set(0, JavaType.ShallowClass.build("java.lang.String[]"));
+
+ return m.withArguments(args)
+ .withMethodType(m.getMethodType().withParameterTypes(parameterTypes));
+ }
+ } else {
+ // replace argument to 'command.split(" ")'
+ List args = m.getArguments();
+ boolean needWrap = false;
+ Expression arg0 = args.get(0);
+ if (!(arg0 instanceof J.Identifier) &&
+ !(arg0 instanceof J.Literal) &&
+ !(arg0 instanceof J.MethodInvocation)) {
+ needWrap = true;
+ }
+
+ String code = needWrap ? "(#{any()}).split(\" \")" : "#{any()}.split(\" \")";
+ JavaTemplate template = JavaTemplate.builder(code).contextSensitive().build();
+ Cursor cursor = new Cursor(getCursor(), args.get(0));
+ arg0 = template.apply(cursor, args.get(0).getCoordinates().replace(), args.get(0));
+ args.set(0, arg0);
+
+ if (m.getMethodType() != null) {
+ List parameterTypes = m.getMethodType().getParameterTypes();
+ parameterTypes.set(0, JavaType.ShallowClass.build("java.lang.String[]"));
+
+ return m.withArguments(args).withMethodType(m.getMethodType().withParameterTypes(parameterTypes));
+ }
+ return m;
+ }
+ }
+
+ return m;
+ }
+ });
+ }
+
+ private static String toStringArguments(String[] cmds) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < cmds.length; i++) {
+ String token = cmds[i];
+ if (i != 0) {
+ sb.append(", ");
+ }
+ sb.append("\"")
+ .append(token)
+ .append("\"");
+ }
+ return sb.toString();
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java
new file mode 100644
index 000000000..acfe60807
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/groovy/GroovyFileChecker.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis.groovy;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.Tree;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.groovy.tree.G;
+import org.openrewrite.marker.SearchResult;
+
+/**
+ * Add a search marker if vising a Groovy file
+ */
+public class GroovyFileChecker
extends TreeVisitor {
+ @Override
+ public @Nullable Tree visit(@Nullable Tree tree, P p) {
+ if (tree instanceof G.CompilationUnit) {
+ return SearchResult.found(tree);
+ }
+ return tree;
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java
new file mode 100644
index 000000000..8f6e89e81
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/openrewrite/staticanalysis/kotlin/KotlinFileChecker.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis.kotlin;
+
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.Tree;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.marker.SearchResult;
+
+/**
+ * Add a search marker if vising a Kotlin file
+ */
+public class KotlinFileChecker
extends TreeVisitor {
+ @Override
+ public @Nullable Tree visit(@Nullable Tree tree, P p) {
+ if (tree instanceof J.CompilationUnit cu) {
+ if (cu.getSourcePath() != null ) {
+ String fileName = cu.getSourcePath().getFileName().toString();
+ if (fileName.endsWith(".kt") || fileName.endsWith(".kts")) {
+ return SearchResult.found(cu);
+ }
+ }
+ }
+ return tree;
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java
new file mode 100644
index 000000000..5e562810e
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/JavaFileChecker.java
@@ -0,0 +1,34 @@
+package org.springframework.ide.vscode.commons.rewrite.java;
+
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.NlsRewrite;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.marker.SearchResult;
+
+public class JavaFileChecker extends Recipe {
+ @Override
+ public @NlsRewrite.DisplayName String getDisplayName() {
+ return "Checks if source is a Java file";
+ }
+
+ @Override
+ public @NlsRewrite.Description String getDescription() {
+ return "Checks if source is a Java file.";
+ }
+
+ @Override
+ public TreeVisitor, ExecutionContext> getVisitor() {
+ return new JavaIsoVisitor() {
+ @Override
+ public J.CompilationUnit visitCompilationUnit(J.CompilationUnit cu, ExecutionContext executionContext) {
+ if (cu.getSourcePath() != null && cu.getSourcePath().getFileName().toString().endsWith(".java")) {
+ return SearchResult.found(cu);
+ }
+ return cu;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java
new file mode 100644
index 000000000..cb3beed75
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialAnnotationToSerialVersionUIDTest.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+import static org.openrewrite.java.Assertions.javaVersion;
+
+class AddSerialAnnotationToSerialVersionUIDTest implements RewriteTest {
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new AddSerialAnnotationToSerialVersionUID())
+ .allSources(sourceSpec -> sourceSpec.markers(javaVersion(17)));
+ }
+
+ @DocumentExample
+ @Test
+ void addSerialAnnotation() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ class Example implements Serializable {
+ private static final long serialVersionUID = 1L;
+ }
+ """,
+ """
+ import java.io.Serial;
+ import java.io.Serializable;
+
+ class Example implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void shouldAddToNewFieldWhenChained() {
+ rewriteRun(
+ spec -> spec.recipes(
+ new AddSerialVersionUidToSerializable(),
+ new AddSerialAnnotationToSerialVersionUID()),
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ class Example implements Serializable {
+ }
+ """,
+ """
+ import java.io.Serial;
+ import java.io.Serializable;
+
+ class Example implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void shouldNoopIfAlreadyPresent() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+ import java.io.Serial;
+
+ class Example implements Serializable {
+ String var1 = "first variable";
+ @Serial
+ private static final long serialVersionUID = 1L;
+ int var3 = 666;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void shouldNotAnnotateNonSerializableClass() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class Example {
+ private static final long serialVersionUID = 1L;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void shouldNotAnnotateOnJava11() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ class Example implements Serializable {
+ private static final long serialVersionUID = 1L;
+ }
+ """,
+ spec -> spec.markers(javaVersion(11))
+ )
+ );
+ }
+
+ @Test
+ void shouldNotAnnotateOtherFields() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ class Example implements Serializable {
+ static final long serialVersionUID = 1L;
+ private final long serialVersionUID = 1L;
+ private static long serialVersionUID = 1L;
+ private static final int serialVersionUID = 1L;
+ private static final long foo = 1L;
+
+ void doSomething() {
+ long serialVersionUID = 1L;
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void shouldAnnotatedFieldsInInnerClasses() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ class Outer implements Serializable {
+ private static final long serialVersionUID = 1;
+ static class Inner implements Serializable {
+ private static final long serialVersionUID = 1;
+ }
+ }
+ """,
+ """
+ import java.io.Serial;
+ import java.io.Serializable;
+
+ class Outer implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1;
+ static class Inner implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1;
+ }
+ }
+ """
+ )
+ );
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java
new file mode 100644
index 000000000..696f00248
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/AddSerialVersionUidToSerializableTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+@SuppressWarnings("MissingSerialAnnotation")
+class AddSerialVersionUidToSerializableTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new AddSerialVersionUidToSerializable());
+ }
+
+ @Test
+ void doNothingNotSerializable() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ public class Example {
+ private String fred;
+ private int numberOfFreds;
+ }
+ """
+ )
+ );
+ }
+
+ @DocumentExample
+ @Test
+ void addSerialVersionUID() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private String fred;
+ private int numberOfFreds;
+ }
+ """,
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private static final long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void fixSerialVersionUIDModifiers() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private final long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """,
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private static final long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void fixSerialVersionUIDNoModifiers() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """,
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private static final long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void fixSerialVersionUIDNoModifiersWrongType() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ Long serialVersionUID = 1L;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """,
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private static final long serialVersionUID = 1L;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void uidAlreadyPresent() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private static final long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void methodDeclarationsAreNotVisited() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private String fred;
+ private int numberOfFreds;
+ void doSomething() {
+ int serialVersionUID = 1;
+ }
+ }
+ """,
+ """
+ import java.io.Serializable;
+
+ public class Example implements Serializable {
+ private static final long serialVersionUID = 1;
+ private String fred;
+ private int numberOfFreds;
+ void doSomething() {
+ int serialVersionUID = 1;
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void doNotAlterAnInterface() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public interface Example extends Serializable {
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void doNotAlterAnException() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class MyException extends Exception implements Serializable {
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void doNotAlterARuntimeException() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+
+ public class MyException extends RuntimeException implements Serializable {
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void serializableInnerClass() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ import java.io.Serializable;
+ public class Outer implements Serializable {
+ public static class Inner implements Serializable {
+ }
+ }
+ """,
+ """
+ import java.io.Serializable;
+ public class Outer implements Serializable {
+ private static final long serialVersionUID = 1;
+ public static class Inner implements Serializable {
+ private static final long serialVersionUID = 1;
+ }
+ }
+ """
+ )
+ );
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java
new file mode 100644
index 000000000..a57b908df
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/ChainStringBuilderAppendCallsTest.java
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2023 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openrewrite.staticanalysis;
+
+import org.junit.jupiter.api.Test;
+import org.openrewrite.DocumentExample;
+import org.openrewrite.test.RecipeSpec;
+import org.openrewrite.test.RewriteTest;
+
+import static org.openrewrite.java.Assertions.java;
+
+@SuppressWarnings({"StringConcatenationInsideStringBufferAppend", "StringBufferReplaceableByString"})
+class ChainStringBuilderAppendCallsTest implements RewriteTest {
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new ChainStringBuilderAppendCalls());
+ }
+
+ @DocumentExample(value = "Chain `StringBuilder.append()` calls instead of the '+' operator to efficiently concatenate strings and numbers.")
+ @Test
+ void objectsConcatenation() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A" + op + "B");
+ sb.append(1 + op + 2);
+ }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A").append(op).append("B");
+ sb.append(1).append(op).append(2);
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void literalConcatenationIgnored() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("A" + "B" + "C");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @DocumentExample("Grouping concatenation.")
+ @Test
+ void groupedStringsConcatenation() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A" + "B" + "C" + op + "D" + "E");
+ }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A" + "B" + "C").append(op).append("D" + "E");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void unWrap() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append(("A" + op + "B"));
+ }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A").append(op).append("B");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void chainedAppend() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append(("A" + op)).append("B");
+ }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A").append(op).append("B");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void runMultipleTimes() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append(("A" + op) + "B");
+ }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append("A").append(op).append("B");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void correctlyGroupConcatenations() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append(op + 1 + 2 + "A" + "B" + 'x');
+ sb.append(1 + 2 + op + 3 + 4);
+ sb.append(1 + 2 + name() + 3 + 4);
+ sb.append(op + (1 + 2));
+ }
+ String name() { return "name"; }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ String op = "+";
+ sb.append(op).append(1).append(2).append("A" + "B").append('x');
+ sb.append(1 + 2).append(op).append(3).append(4);
+ sb.append(1 + 2).append(name()).append(3).append(4);
+ sb.append(op).append(1 + 2);
+ }
+ String name() { return "name"; }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void appendMethods() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(str1() + str2() + str3());
+ }
+
+ String str1() { return "A"; }
+ String str2() { return "B"; }
+ String str3() { return "C"; }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(str1()).append(str2()).append(str3());
+ }
+
+ String str1() { return "A"; }
+ String str2() { return "B"; }
+ String str3() { return "C"; }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void ChainedAppendWithConstructor() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder().append("A" + operator() + "B");
+ }
+
+ String operator() { return "+"; }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ StringBuilder sb = new StringBuilder().append("A").append(operator()).append("B");
+ }
+
+ String operator() { return "+"; }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void methodArgument() {
+ rewriteRun(
+ //language=java
+ java(
+ """
+ class A {
+ void method1() {
+ String op = "+";
+ print(new StringBuilder().append("A" + op + "C").toString());
+ }
+
+ void print(String str) {
+ }
+ }
+ """,
+ """
+ class A {
+ void method1() {
+ String op = "+";
+ print(new StringBuilder().append("A").append(op).append("C").toString());
+ }
+
+ void print(String str) {
+ }
+ }
+ """
+ )
+ );
+ }
+}
diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java
new file mode 100644
index 000000000..4eaee8b51
--- /dev/null
+++ b/headless-services/commons/commons-rewrite/src/test/java/org/openrewrite/staticanalysis/InstanceOfPatternMatchTest.java
@@ -0,0 +1,1296 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *