diff --git a/src/main/java/org/openrewrite/staticanalysis/SimplifyBooleanExpressionWithDeMorgan.java b/src/main/java/org/openrewrite/staticanalysis/SimplifyBooleanExpressionWithDeMorgan.java new file mode 100644 index 0000000000..901fcff3a4 --- /dev/null +++ b/src/main/java/org/openrewrite/staticanalysis/SimplifyBooleanExpressionWithDeMorgan.java @@ -0,0 +1,123 @@ +/* + * 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.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.JavaVisitor; +import org.openrewrite.java.ParenthesizeVisitor; +import org.openrewrite.java.tree.*; +import org.openrewrite.marker.Markers; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public class SimplifyBooleanExpressionWithDeMorgan extends Recipe { + + @Override + public String getDisplayName() { + return "Simplify boolean expressions using De Morgan's laws"; + } + + @Override + public String getDescription() { + return "Applies De Morgan's laws to simplify boolean expressions with negation. " + + "Transforms `!(a && b)` to `!a || !b` and `!(a || b)` to `!a && !b`."; + } + + @Override + public Set getTags() { + return Collections.singleton("RSPEC-1125"); + } + + @Override + public Duration getEstimatedEffortPerOccurrence() { + return Duration.ofMinutes(2); + } + + @Override + public TreeVisitor getVisitor() { + return new JavaVisitor() { + + @Override + public J visitUnary(J.Unary unary, ExecutionContext ctx) { + if (unary.getOperator() == J.Unary.Type.Not && unary.getExpression() instanceof J.Parentheses) { + J.Parentheses parentheses = (J.Parentheses) unary.getExpression(); + if (parentheses.getTree() instanceof J.Binary) { + J.Binary binary = (J.Binary) parentheses.getTree(); + J.Parentheses parenthesesBinary = (J.Parentheses) unary.getExpression(); + + J.Binary.Type newOperator = null; + if (binary.getOperator() == J.Binary.Type.And) { + newOperator = J.Binary.Type.Or; + } else if (binary.getOperator() == J.Binary.Type.Or) { + newOperator = J.Binary.Type.And; + } + Expression left = binary.getLeft(); + Expression right = binary.getRight(); + + if (newOperator != null) { + left = negate(left); + left = (Expression) new ParenthesizeVisitor<>().visit(left, ctx); + right = negate(right); + right = (Expression) new ParenthesizeVisitor<>().visit(right, ctx); + } + + left = (Expression) this.visit(left, ctx); + right = (Expression) this.visit(right, ctx); + + if (newOperator == null) { + J.Binary visitedBinary = binary.withLeft(left).withRight(right); + return unary.withExpression(parenthesesBinary.withTree(visitedBinary)); + } + Space prefix = unary.getPrefix(); + List comments = new ArrayList<>(prefix.getComments()); + comments.addAll(parenthesesBinary.getComments()); + comments.addAll(binary.getComments()); + prefix = prefix.withComments(comments); + binary = binary.withLeft(left).withRight(right).withOperator(newOperator).withPrefix(prefix); + return new ParenthesizeVisitor<>().visit(binary, ctx); + } + } + return requireNonNull(super.visitUnary(unary, ctx)); + } + + private Expression negate(Expression expression) { + if (expression instanceof J.Unary) { + J.Unary unaryExpr = (J.Unary) expression; + if (unaryExpr.getOperator() == J.Unary.Type.Not) { + return unaryExpr.getExpression().withPrefix(expression.getPrefix()); + } + } + return new J.Unary( + Tree.randomId(), + expression.getPrefix(), + Markers.EMPTY, + new JLeftPadded<>(Space.EMPTY, J.Unary.Type.Not, Markers.EMPTY), + expression.withPrefix(Space.EMPTY), + JavaType.Primitive.Boolean + ); + } + }; + } +} diff --git a/src/test/java/org/openrewrite/staticanalysis/SimplifyBooleanExpressionWithDeMorganTest.java b/src/test/java/org/openrewrite/staticanalysis/SimplifyBooleanExpressionWithDeMorganTest.java new file mode 100644 index 0000000000..4d7dbabc72 --- /dev/null +++ b/src/test/java/org/openrewrite/staticanalysis/SimplifyBooleanExpressionWithDeMorganTest.java @@ -0,0 +1,314 @@ +/* + * 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; + +class SimplifyBooleanExpressionWithDeMorganTest implements RewriteTest { + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new SimplifyBooleanExpressionWithDeMorgan()); + } + + @DocumentExample + @Test + void transformNegatedAndToOr() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(boolean a, boolean b) { + if (!(a && !b)) { + System.out.println("Not both"); + } + } + } + """, + """ + class Test { + void test(boolean a, boolean b) { + if (!a || b) { + System.out.println("Not both"); + } + } + } + """ + ) + ); + } + + @Test + void transformNegatedOrToAnd() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(boolean a, boolean b) { + if /*0*/(/*1*/!/*2*/(/*3*/!a || /*Bee Bee Bee*/ b)) { + System.out.println("Neither"); + } + } + } + """, + """ + class Test { + void test(boolean a, boolean b) { + if /*0*/(/*1*//*2*//*3*/a && /*Bee Bee Bee*/ !b) { + System.out.println("Neither"); + } + } + } + """ + ) + ); + } + + @Test + void transformMethodCallExpressions() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(String s1, String s2) { + if (!(s1.isEmpty() && s2.isEmpty())) { + System.out.println("At least one not empty"); + } + } + } + """, + """ + class Test { + void test(String s1, String s2) { + if (!s1.isEmpty() || !s2.isEmpty()) { + System.out.println("At least one not empty"); + } + } + } + """ + ) + ); + } + + @Test + void noChangeWhenNoParentheses() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(boolean a, boolean b) { + if (!a && b) { + System.out.println("Already simplified"); + } + } + } + """ + ) + ); + } + + @Test + void noChangeWhenNotNegatedBinaryExpression() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(boolean a, boolean b) { + if ((a && b)) { + System.out.println("No negation"); + } + } + } + """ + ) + ); + } + + @Test + void noChangeWhenNegatingComparison() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(int x, int y) { + if (!(x > y)) { + System.out.println("Not greater"); + } + } + } + """ + ) + ); + } + + @Test + void triple() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(boolean a, boolean b, boolean c) { + if (!(a || !b || c)) { + System.out.println("None are true"); + } + } + } + """, + """ + class Test { + void test(boolean a, boolean b, boolean c) { + if (!a && b && !c) { + System.out.println("None are true"); + } + } + } + """ + ) + ); + } + + @Test + void quadruple() { + rewriteRun( + //language=java + java( + """ + class Test { + static void test(boolean a, boolean b, boolean c, boolean d) { + return !(a && !b && c && !d); + } + } + """, + """ + class Test { + static void test(boolean a, boolean b, boolean c, boolean d) { + return !a || b || !c || d; + } + } + """ + ) + ); + } + + @Test + void nested() { + rewriteRun( + // NOTE: the parens around (z || y) in the output are not necessary, but they are not wrong. We might look into having them removed. + //language=java + java( + """ + class Test { + void test(boolean w, boolean x, boolean y, boolean z) { + boolean result = !((w && x) && !(z || y)); + } + } + """, + """ + class Test { + void test(boolean w, boolean x, boolean y, boolean z) { + boolean result = !w || !x || (z || y); + } + } + """ + ) + ); + } + + @Test + void nested2() { + rewriteRun( + //language=java + // NOTE: the parens around (z || y) in the output are not necessary, but they are not wrong. We might look into having them removed. + java( + """ + class Test { + void test(boolean w, boolean x, boolean y, boolean z) { + boolean result = !((w || x) && !(z || y)); + } + } + """, + """ + class Test { + void test(boolean w, boolean x, boolean y, boolean z) { + boolean result = !w && !x || (z || y); + } + } + """ + ) + ); + } + + @Test + void deMorganWithinNonBoolean() { + rewriteRun( + //language=java + java( + """ + class Test { + void test(boolean a, boolean b, boolean c, boolean d) { + if ((!(a && !b)) == (!(!c || !d))) { + System.out.println("Complex boolean comparison"); + } + } + } + """, + """ + class Test { + void test(boolean a, boolean b, boolean c, boolean d) { + if ((!a || b) == (c && d)) { + System.out.println("Complex boolean comparison"); + } + } + } + """ + ) + ); + } + + + @Test + void mixedOperators() { + // As a human I probably wouldn't dare to change it probably, but it's not wrong, and it's not worse than the original, so be it. + rewriteRun( + //language=java + java( + """ + class Test { + boolean a, b, c; + boolean x = !(a && !b || c); + } + """, + """ + class Test { + boolean a, b, c; + boolean x = (!a || b) && !c; + } + """ + ) + ); + } +}