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 getTags() { + return singleton("RSPEC-S1066"); + } + + @Override + public TreeVisitor getVisitor() { + return new JavaIsoVisitor() { + @Override + public J.If visitIf(J.If iff, ExecutionContext ctx) { + J.If outerIf = super.visitIf(iff, ctx); + + if (outerIf.getElsePart() == null) { + // thenPart is either a single if or a block with a single if + J.Block outerBlock = null; + J.If innerIf = null; + if (outerIf.getThenPart() instanceof J.If) { + innerIf = (J.If) outerIf.getThenPart(); + } else if (outerIf.getThenPart() instanceof J.Block) { + outerBlock = (J.Block) outerIf.getThenPart(); + List statements = outerBlock.getStatements(); + if (statements.size() == 1 && statements.get(0) instanceof J.If) { + innerIf = (J.If) statements.get(0); + } + } + + if (innerIf != null && innerIf.getElsePart() == null) { + // thenPart of outer if is replaced with thenPart of innerIf + // combine conditions with logical AND : correct parenthesizing is handled by JavaTemplate + Expression outerCondition = outerIf.getIfCondition().getTree(); + Expression innerCondition = innerIf.getIfCondition().getTree(); + + 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.apply( + String.format("#{any()} /*%s,%s,%s*/&& #{any()}", CONTINUATION_KEY, innerIfId, outerBlockId), + getCursor(), + outerCondition.getCoordinates().replace(), + outerCondition, + innerCondition) + .withThenPart(indent(innerIf.getThenPart(), getCursor(), -1)); + } + } + + return outerIf; + } + }; + } + + @RequiredArgsConstructor + private static class MergedConditionalVisitor

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 innerIfComments = Optional.ofNullable(getCursor().getRoot().>pollMessage(innerIfId)).orElse(emptyList()); + List outerBlockComments = Optional.ofNullable(getCursor().getRoot().>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 comments = getCursor().pollMessage(CONTINUATION_KEY); + if (comments != null) { + final String outerIfIndent = getCursor().firstEnclosingOrThrow(J.If.class).getPrefix().getIndent(); + final String continuationIndent = continuationIndent(requireNonNull(tabsAndIndentsStyle), outerIfIndent); + + if (comments.isEmpty()) { + b = b.withRight(b.getRight() + .withPrefix(Space.format("\n" + continuationIndent))); + } else { + b = b.withRight(b.getRight() + .withComments(ListUtils.map(comments, c -> replaceIndent(c, continuationIndent)))); + } + } + + return b; + } + + private Comment replaceIndent(Comment comment, String newIndent) { + Comment c = comment.withSuffix(replaceLastLineWithIndent(comment.getSuffix(), newIndent)); + if (c.isMultiline() && c instanceof TextComment) { + TextComment tc = (TextComment) c; + c = tc.withText(replaceTextIndent(tc.getText(), newIndent)); + } + return c; + } + + private String replaceTextIndent(final String text, final String newIndent) { + final StringBuilder sb = new StringBuilder(); + boolean found = false; + for (final char ch : text.toCharArray()) { + if (ch == ' ' || ch == '\t') { + if (!found) { + sb.append(ch); + } + } else if (ch == '\r' || ch == '\n') { + sb.append(ch); + found = true; + } else { + if (found) { + sb.append(newIndent); + if (ch == '*') { + sb.append(' '); + } + } + sb.append(ch); + found = false; + } + } + if (found) { + sb.append(newIndent); + sb.append(' '); + } + return sb.toString(); + } + + private String replaceLastLineWithIndent(String whitespace, String indent) { + int idx = whitespace.length() - 1; + while (idx >= 0) { + char c = whitespace.charAt(idx); + if (c == '\r' || c == '\n') { + break; + } + idx--; + } + if (idx >= 0) { + return whitespace.substring(0, idx + 1) + indent; + } + return whitespace; + } + + 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 new file mode 100644 index 0000000000..f18b3c760c --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/CombineMergeableIfStatementsTest.java @@ -0,0 +1,633 @@ +/* + * 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.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"); + } + } + } + """ + ) + ); + } +}