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
+ * 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;
+ }
+ """
+ )
+ );
+ }
+}