From b8f8c71188be6cb8b60a67cf6b08f8817457d492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Kaplan?= <228053663+olwispe@users.noreply.github.com> Date: Sat, 30 Aug 2025 10:02:44 +0200 Subject: [PATCH 1/7] Mergeable `if` statements should be combined - RSPEC-S1066 --- .../CombineMergeableIfStatements.java | 97 ++++ .../CombineMergeableIfStatementsTest.java | 420 ++++++++++++++++++ 2 files changed, 517 insertions(+) create mode 100644 src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java create mode 100644 src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java 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 000000000..bea4b46bc --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java @@ -0,0 +1,97 @@ +/* + * 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 org.openrewrite.ExecutionContext;
+import org.openrewrite.Recipe;
+import org.openrewrite.TreeVisitor;
+import org.openrewrite.internal.ListUtils;
+import org.openrewrite.java.JavaIsoVisitor;
+import org.openrewrite.java.JavaTemplate;
+import org.openrewrite.java.tree.Expression;
+import org.openrewrite.java.tree.J;
+import org.openrewrite.java.tree.Statement;
+
+import java.util.List;
+import java.util.Set;
+
+import static java.util.Collections.singleton;
+import static org.openrewrite.java.format.ShiftFormat.indent;
+
+public class CombineMergeableIfStatements extends Recipe {
+ @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
+ * 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 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) {
+ if (condition1) /* Comment 0 */ { // Comment 1
+ // Comment 2
+ if (condition2) /* Comment 3 */ { // Comment 4
+ System.out.println("OK");
+ }
+ }
+ }
+ }
+ """,
+ """
+ class A {
+ void a(boolean condition1, boolean condition2) {
+ /* Comment 0 */ // Comment 1
+ // Comment 2
+ if (condition1 && condition2) /* Comment 3 */ { // Comment 4
+ System.out.println("OK");
+ }
+ }
+ }
+ """
+ )
+ );
+ }
+}
From 866ae68867945700e76dc5a1033223bf3143b072 Mon Sep 17 00:00:00 2001
From: Tim te Beek 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() &&
+ CONTINUATION_KEY.equals(onlyComment.getText()) &&
+ getCursor().firstEnclosingOrThrow(J.Binary.class).getOperator() == J.Binary.Type.And) {
+ getCursor().putMessageOnFirstEnclosing(J.Binary.class, CONTINUATION_KEY, true);
+ s = s.withComments(emptyList());
+ }
+ }
+
+ return s;
+ }
+
+ @Override
+ public J.Binary visitBinary(J.Binary binary, P p) {
+ J.Binary b = super.visitBinary(binary, p);
+
+ if (Boolean.TRUE.equals(getCursor().getMessage(CONTINUATION_KEY))) {
+ J.If outerIf = getCursor().firstEnclosingOrThrow(J.If.class);
+ final String outerIfIndent = outerIf.getPrefix().getIndent();
+ b = b.withRight(b.getRight().withPrefix(
+ Space.format("\n" + continuationIndent(requireNonNull(tabsAndIndentsStyle), outerIfIndent))));
+ }
+
+ return b;
+ }
+
+ private String continuationIndent(TabsAndIndentsStyle tabsAndIndents, String currentIndent) {
+ char c;
+ int len;
+ if (tabsAndIndents.getUseTabCharacter()) {
+ c = '\t';
+ len = tabsAndIndents.getContinuationIndent() / tabsAndIndents.getTabSize();
+ } else {
+ c = ' ';
+ len = tabsAndIndents.getContinuationIndent();
+ }
+
+ StringBuilder sb = new StringBuilder(currentIndent);
+ for (int i = 0; i < len; i++) {
+ sb.append(c);
+ }
+ return sb.toString();
+ }
+ }
}
diff --git a/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java b/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java
index b2493739c..6ece54a1c 100644
--- a/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java
+++ b/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java
@@ -50,7 +50,8 @@ void a(boolean condition1, boolean condition2) {
"""
class A {
void a(boolean condition1, boolean condition2) {
- if (condition1 && condition2) {
+ if (condition1 &&
+ condition2) {
System.out.println("OK");
}
}
@@ -83,7 +84,8 @@ void a(Object o) {
"""
class A {
void a(Object o) {
- if (o instanceof String s && s.isEmpty()) {
+ if (o instanceof String s &&
+ s.isEmpty()) {
System.out.println("OK");
}
}
@@ -128,9 +130,11 @@ void a(Object o1) {
class A {
void a(Object o1) {
- if (o1 instanceof List> list && !list.isEmpty()) {
+ if (o1 instanceof List> list &&
+ !list.isEmpty()) {
Object o2 = list.get(0);
- if (o2 instanceof String s && s.isEmpty()) {
+ if (o2 instanceof String s &&
+ s.isEmpty()) {
System.out.println("OK");
}
}
@@ -158,7 +162,8 @@ void a(boolean condition1, boolean condition2) {
"""
class A {
void a(boolean condition1, boolean condition2) {
- if (condition1 && condition2)
+ if (condition1 &&
+ condition2)
System.out.println("OK");
}
}
@@ -194,7 +199,12 @@ void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
"""
class A {
void a(boolean b1, boolean b2, boolean b3, boolean b4, boolean b5, boolean b6) {
- if (b1 && b2 && b3 && b4 && b5 && b6) {
+ if (b1 &&
+ b2 &&
+ b3 &&
+ b4 &&
+ b5 &&
+ b6) {
System.out.println("OK");
}
}
@@ -410,7 +420,8 @@ void a(boolean condition1, boolean condition2) {
// Comment -1
/* Comment 0 */ // Comment 1
// Comment 2
- if (condition1 && condition2) /* Comment 3 */ { // Comment 4
+ if (condition1 &&
+ condition2) /* Comment 3 */ { // Comment 4
System.out.println("OK");
}
}
@@ -439,7 +450,43 @@ void a(boolean condition1, boolean condition2, boolean condition3) {
"""
class A {
void a(boolean condition1, boolean condition2, boolean condition3) {
- if (condition1 && (condition2 || 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");
}
}
From 709be2c4bd65a94e6799578818eb8079ab900b53 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andr=C3=A9=20Kaplan?=
<228053663+olwispe@users.noreply.github.com>
Date: Sat, 27 Sep 2025 20:55:50 +0200
Subject: [PATCH 7/7] Handle comments associated with inner conditional
---
.../CombineMergeableIfStatements.java | 103 ++++++++++--
.../CombineMergeableIfStatementsTest.java | 149 +++++++++++++++++-
2 files changed, 230 insertions(+), 22 deletions(-)
diff --git a/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java
index 3f33389ee..64a69a5f7 100644
--- a/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java
+++ b/src/main/java/org/openrewrite/staticanalysis/CombineMergeableIfStatements.java
@@ -21,10 +21,12 @@
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;
@@ -34,12 +36,13 @@
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.internal.ListUtils.concatAll;
import static org.openrewrite.java.format.ShiftFormat.indent;
public class CombineMergeableIfStatements extends Recipe {
@@ -90,18 +93,20 @@ public J.If visitIf(J.If iff, ExecutionContext ctx) {
Expression outerCondition = outerIf.getIfCondition().getTree();
Expression innerCondition = innerIf.getIfCondition().getTree();
- innerIf = indent(innerIf, getCursor(), -1);
+ UUID innerIfId = Tree.randomId();
+ getCursor().getRoot().putMessage(innerIfId.toString(), innerIf.getComments());
+ UUID outerBlockId = Tree.randomId();
+ getCursor().getRoot().putMessage(outerBlockId.toString(),
+ Optional.ofNullable(outerBlock).map(J::getComments).orElse(emptyList()));
+
doAfterVisit(new MergedConditionalVisitor<>());
return JavaTemplate. extends JavaIsoVisitor {
+
@Nullable
private TabsAndIndentsStyle tabsAndIndentsStyle;
@@ -131,10 +137,16 @@ public Space visitSpace(@Nullable Space space, Space.Location loc, P p) {
s.getComments().get(0) instanceof TextComment) {
TextComment onlyComment = (TextComment) s.getComments().get(0);
if (onlyComment.isMultiline() &&
- CONTINUATION_KEY.equals(onlyComment.getText()) &&
+ onlyComment.getText().startsWith(CONTINUATION_KEY) &&
getCursor().firstEnclosingOrThrow(J.Binary.class).getOperator() == J.Binary.Type.And) {
- getCursor().putMessageOnFirstEnclosing(J.Binary.class, CONTINUATION_KEY, true);
- s = s.withComments(emptyList());
+ final String[] arr = onlyComment.getText().split(",");
+ final String innerIfId = arr[1];
+ final String outerBlockId = arr[2];
+ List>pollMessage(innerIfId)).orElse(emptyList());
+ List
>pollMessage(outerBlockId)).orElse(emptyList());
+
+ getCursor().putMessageOnFirstEnclosing(J.Binary.class, CONTINUATION_KEY, innerIfComments);
+ s = s.withComments(outerBlockComments);
}
}
@@ -145,16 +157,77 @@ public Space visitSpace(@Nullable Space space, Space.Location loc, P p) {
public J.Binary visitBinary(J.Binary binary, P p) {
J.Binary b = super.visitBinary(binary, p);
- if (Boolean.TRUE.equals(getCursor().getMessage(CONTINUATION_KEY))) {
- J.If outerIf = getCursor().firstEnclosingOrThrow(J.If.class);
- final String outerIfIndent = outerIf.getPrefix().getIndent();
- b = b.withRight(b.getRight().withPrefix(
- Space.format("\n" + continuationIndent(requireNonNull(tabsAndIndentsStyle), outerIfIndent))));
+ List