diff --git a/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java new file mode 100644 index 0000000000..3bc6544849 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java @@ -0,0 +1,248 @@ +/* + * Copyright 2025 the original author or authors. + *
+ * Licensed under the Moderne Source Available License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + *
+ * https://docs.moderne.io/licensing/moderne-source-available-license + *
+ * 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 lombok.RequiredArgsConstructor;
+import org.jspecify.annotations.Nullable;
+import org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.Tree;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.style.IntelliJ;
+import org.openrewrite.java.style.TabsAndIndentsStyle;
+import org.openrewrite.java.tree.Comment;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.JavaSourceFile;
+import org.openrewrite.java.tree.Space;
+import org.openrewrite.java.tree.Statement;
+import org.openrewrite.java.tree.TextComment;
+import org.openrewrite.style.Style;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singleton;
+import static java.util.Objects.requireNonNull;
+import static org.openrewrite.java.format.ShiftFormat.indent;
+
+public class CombineMergeableIfStatements extends Recipe {
+
+ private static final String CONTINUATION_KEY = "continuationAfterLogicalAnd";
+
+ @Override
+ public String getDisplayName() {
+ // language=markdown
+ return "Mergeable `if` statements should be combined";
+ }
+
+ @Override
+ public String getDescription() {
+ // language=markdown
+ return "Mergeable `if` statements should be combined.";
+ }
+
+ @Override
+ public Set extends JavaIsoVisitor {
+
+ @Nullable
+ private TabsAndIndentsStyle tabsAndIndentsStyle;
+
+ @Override
+ public @Nullable J visit(@Nullable Tree tree, P p) {
+ if (tree instanceof JavaSourceFile) {
+ JavaSourceFile cu = (JavaSourceFile) requireNonNull(tree);
+ tabsAndIndentsStyle = Style.from(TabsAndIndentsStyle.class, cu, IntelliJ::tabsAndIndents);
+ }
+ return super.visit(tree, p);
+ }
+
+ @Override
+ public Space visitSpace(@Nullable Space space, Space.Location loc, P p) {
+ Space s = super.visitSpace(space, loc, p);
+ if (s.getComments().size() == 1 &&
+ s.getComments().get(0) instanceof TextComment) {
+ TextComment onlyComment = (TextComment) s.getComments().get(0);
+ if (onlyComment.isMultiline() &&
+ onlyComment.getText().startsWith(CONTINUATION_KEY) &&
+ getCursor().firstEnclosingOrThrow(J.Binary.class).getOperator() == J.Binary.Type.And) {
+ final String[] arr = onlyComment.getText().split(",");
+ final String innerIfId = arr[1];
+ final String outerBlockId = arr[2];
+ List
+ * Licensed under the Moderne Source Available License (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://docs.moderne.io/licensing/moderne-source-available-license
+ *
+ * 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.version;
+
+class CombineMergeableIfStatementsTest implements RewriteTest {
+
+ @Override
+ public void defaults(RecipeSpec spec) {
+ spec.recipe(new CombineMergeableIfStatements());
+ }
+
+ @DocumentExample
+ @Test
+ void combineMergeableIfStatements() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1 &&
+ condition2) {
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void simplifyWithPatternMatchingForInstanceOf() {
+ rewriteRun(
+ spec -> spec
+ .recipes(new InstanceOfPatternMatch(), new CombineMergeableIfStatements())
+ .allSources(sourceSpec -> version(sourceSpec, 17)),
+ // language=java
+ java(
+ """
+ class A {
+ void a(Object o) {
+ if (o instanceof String) {
+ String s = (String) o;
+ if (s.isEmpty()) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(Object o) {
+ if (o instanceof String s &&
+ s.isEmpty()) {
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+
+ }
+
+ @Test
+ void simplifyWithMultiplePatternMatchingForInstanceOf() {
+ // This test doesn't fully simplify but could with an 'Inline Local Variable Used Once' recipe
+ rewriteRun(
+ spec -> spec
+ .recipes(new InstanceOfPatternMatch(), new CombineMergeableIfStatements())
+ .allSources(sourceSpec -> version(sourceSpec, 17)),
+ // language=java
+ java(
+ """
+ import java.util.List;
+
+ class A {
+ void a(Object o1) {
+ if (o1 instanceof List>) {
+ List> list = (List>) o1;
+ if (!list.isEmpty()) {
+ Object o2 = list.get(0);
+ if (o2 instanceof String) {
+ String s = (String) o2;
+ if (s.isEmpty()) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ }
+ }
+ """,
+ """
+ import java.util.List;
+
+ class A {
+ void a(Object o1) {
+ if (o1 instanceof List> list &&
+ !list.isEmpty()) {
+ Object o2 = list.get(0);
+ if (o2 instanceof String s &&
+ s.isEmpty()) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineWithoutBlocks() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1)
+ if (condition2)
+ System.out.println("OK");
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1 &&
+ condition2)
+ System.out.println("OK");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineSeveralNestedIfs() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ if (b1) {
+ if (b2) {
+ if (b3) {
+ if (b4) {
+ if (b5) {
+ if (b6) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ if (b1 &&
+ b2 &&
+ b3 &&
+ b4 &&
+ b5 &&
+ b6) {
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineSeveralNestedIfsWithLineComments() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ // Comment 1.0
+ if (b1) { // Comment 1
+ if (b2) { // Comment 2
+ // Comment 3.0
+ if (b3) { // Comment 3
+ // Comment 4.0
+ if (b4) { // Comment 4
+ // Comment 5.0
+ if (b5) { // Comment 5
+ // Comment 6.0
+ // Comment 6.1
+ // Comment 6.2
+ if (b6) { // Comment 6
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ // Comment 1.0
+ if (b1 && // Comment 1
+ b2 && // Comment 2
+ // Comment 3.0
+ b3 && // Comment 3
+ // Comment 4.0
+ b4 && // Comment 4
+ // Comment 5.0
+ b5 && // Comment 5
+ // Comment 6.0
+ // Comment 6.1
+ // Comment 6.2
+ b6) { // Comment 6
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineSeveralNestedIfsWithMultilineComments() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ /* Comment 1.0 */
+ if (b1) { /* Comment 1 */
+ if (b2) { /* Comment 2 */
+ /* Comment 3.0 */
+ if (b3) { /* Comment 3 */
+ /* Comment 4.0 */
+ if (b4) { /*
+ * Comment 4
+ */
+ /*
+ * Comment 5.0
+ Comment 5.1
+ */
+ if (b5) { /* Comment 5 */
+ /** Comment 6.0 */
+ /**
+ * Comment 6.1
+ Comment 6.2
+ */
+ if (b6) { /* Comment 6 */
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ /* Comment 1.0 */
+ if (b1 && /* Comment 1 */
+ b2 && /* Comment 2 */
+ /* Comment 3.0 */
+ b3 && /* Comment 3 */
+ /* Comment 4.0 */
+ b4 && /*
+ * Comment 4
+ */
+ /*
+ * Comment 5.0
+ Comment 5.1
+ */
+ b5 && /* Comment 5 */
+ /** Comment 6.0 */
+ /**
+ * Comment 6.1
+ Comment 6.2
+ */
+ b6) { /* Comment 6 */
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenOuterIfHasElsePart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ }
+ } else {
+ System.out.println("KO");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenOuterIfHasEmptyBlockAsElsePart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ }
+ } else {
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenOuterIfHasEmptyStatementAsElsePart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ }
+ } else;
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenOuterIfHasOneStatementInThenPartButIsNotIf() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ System.out.println("KO");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenOuterIfHasOneStatementWithoutBlockInThenPartButIsNotIf() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1)
+ System.out.println("KO");
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenOuterIfHasTwoStatementsInThenPart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ }
+ System.out.println("KO");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenInnerIfHasElsePart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ } else {
+ System.out.println("KO");
+ }
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenInnerIfHasEmptyBlockAsElsePart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ } else {
+ }
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void noSimplificationWhenInnerIfHasEmptyStatementAsElsePart() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ if (condition1) {
+ if (condition2) {
+ System.out.println("OK");
+ } else;
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineMergeableIfStatementsWithComments() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ // Comment 1.0
+ if (condition1) /* Comment 1.1 */
+ /* Comment 1.2 */ // Comment 1.3
+ /* Comment 1.4 */ { // Comment 2.0
+ // Comment 2.1
+ /*
+ * Comment 2.2
+ */ // Comment 2.3
+ if (condition2) /* Comment 3 */ { // Comment 4
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ // Comment 1.0
+ if (condition1 /* Comment 1.1 */
+ /* Comment 1.2 */ // Comment 1.3
+ /* Comment 1.4 */ && // Comment 2.0
+ // Comment 2.1
+ /*
+ * Comment 2.2
+ */ // Comment 2.3
+ condition2) /* Comment 3 */ { // Comment 4
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineBinaryConditions() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean condition1, boolean condition2, boolean condition3) {
+ if (condition1) {
+ if (condition2 || condition3) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean condition1, boolean condition2, boolean condition3) {
+ if (condition1 &&
+ (condition2 || condition3)) {
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+
+ @Test
+ void combineLogicalAndConditions() {
+ rewriteRun(
+ // language=java
+ java(
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ if (b1 && b2) {
+ if (b3 &&
+ b4) {
+ if (b5 && b6) {
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
+ if (b1 && b2 &&
+ b3 &&
+ b4 &&
+ b5 && b6) {
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+}
>pollMessage(innerIfId)).orElse(emptyList());
+ List
>pollMessage(outerBlockId)).orElse(emptyList());
+
+ getCursor().putMessageOnFirstEnclosing(J.Binary.class, CONTINUATION_KEY, innerIfComments);
+ s = s.withComments(outerBlockComments);
+ }
+ }
+
+ return s;
+ }
+
+ @Override
+ public J.Binary visitBinary(J.Binary binary, P p) {
+ J.Binary b = super.visitBinary(binary, p);
+
+ List