diff --git a/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/HelperPredicates.java b/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/HelperPredicates.java new file mode 100644 index 00000000000..3dc0ac27576 --- /dev/null +++ b/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/HelperPredicates.java @@ -0,0 +1,19 @@ +package org.sonar.java.checks.helpers.logic; + +import org.sonar.plugins.java.api.semantic.Symbol; + +import static org.sonar.java.checks.helpers.logic.Ternary.UNKNOWN; + +/** + * Demo for {@link Ternary} + */ +public class HelperPredicates { + private HelperPredicates() {} + + public static Ternary isUsed(Symbol symbol) { + if(symbol.isUnknown()) { + return UNKNOWN; + } + return Ternary.of(!symbol.usages().isEmpty()); + } +} diff --git a/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/Summary.java b/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/Summary.java new file mode 100644 index 00000000000..cc92cbe7e37 --- /dev/null +++ b/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/Summary.java @@ -0,0 +1,58 @@ +package org.sonar.java.checks.helpers.logic; + +import static org.sonar.java.checks.helpers.logic.Ternary.FALSE; +import static org.sonar.java.checks.helpers.logic.Ternary.TRUE; +import static org.sonar.java.checks.helpers.logic.Ternary.UNKNOWN; + +/** + * Utility to for implementing logical "and" and "or" in {@link Ternary}, both + * directly and as a {@link java.util.stream.Collector}. + */ +class Summary { + private boolean allTrue = true; + private boolean anyTrue = false; + private boolean allFalse = true; + private boolean anyFalse = false; + + void add(Ternary arg) { + allTrue &= arg.is(true); + anyTrue |= arg.is(true); + allFalse &= arg.is(false); + anyFalse |= arg.is(false); + } + + Summary addAll(Ternary... args) { + for (Ternary arg : args) { + add(arg); + } + return this; + } + + Summary combine(Summary other) { + allTrue &= other.allTrue; + anyTrue |= other.anyTrue; + allFalse &= other.allFalse; + anyFalse |= other.anyFalse; + return this; + } + + Ternary logicalAnd() { + if (allTrue) { + return TRUE; + } else if (anyFalse) { + return FALSE; + } else { + return UNKNOWN; + } + } + + Ternary logicalOr() { + if (allFalse) { + return FALSE; + } else if (anyTrue) { + return TRUE; + } else { + return UNKNOWN; + } + } +} diff --git a/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/Ternary.java b/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/Ternary.java new file mode 100644 index 00000000000..142ffca5b9d --- /dev/null +++ b/java-checks-common/src/main/java/org/sonar/java/checks/helpers/logic/Ternary.java @@ -0,0 +1,110 @@ +package org.sonar.java.checks.helpers.logic; + +import java.util.stream.Collector; + +/** + * Implementation of tree-valued logic in which a proposition can be true, false, or unknown. + * + *

This class allows us to be precise in certain predicates, for instance + * {@code type.isSubtypeOf("com.example.MySuper")}, where the exact value is unknown + * due to missing semantics. + */ +public enum Ternary { + TRUE, FALSE, UNKNOWN; + + /** + * Adapter for {@code boolean}. + */ + public static Ternary of(boolean value) { + return value ? TRUE : FALSE; + } + + /** + * Adapter for {@code boolean} where {@code null} means {@code UNKNOWN}. + */ + public static Ternary ofNullable(Boolean value) { + return value == null ? UNKNOWN : of(value); + } + + /** + * Checks whether the object is exactly {@code TRUE} or {@code FALSE}. + */ + public boolean is(boolean value) { + if (value) { + return this == TRUE; + } else { + return this == FALSE; + } + } + + /** + * The value is known and true. + */ + public boolean isTrue() { + return this == TRUE; + } + + /** + * The value is known and false. + */ + public boolean isFalse() { + return this == FALSE; + } + + public boolean maybeTrue() { + return this == TRUE || this == UNKNOWN; + } + + public boolean maybeFalse() { + return this == FALSE || this == UNKNOWN; + } + + /** + * Negation. Unknown stays unknown, otherwise the usual boolean logic. + */ + public Ternary not() { + return switch (this) { + case TRUE -> FALSE; + case FALSE -> TRUE; + case UNKNOWN -> UNKNOWN; + }; + } + + /** + * Alternative: + *

+ */ + public static Ternary or(Ternary... args) { + return new Summary().addAll(args).logicalOr(); + } + + /** + * Conjunction: + * + */ + public static Ternary and(Ternary... args) { + return new Summary().addAll(args).logicalAnd(); + } + + /** + * Returns a collector creating a new ternary value following the logic of {@link #and(Ternary...)}. + */ + public static Collector and() { + return Collector.of(Summary::new, Summary::add, Summary::combine, Summary::logicalAnd); + } + + /** + * Returns a collector creating a new ternary value following the logic of {@link #or(Ternary...)}. + */ + public static Collector or() { + return Collector.of(Summary::new, Summary::add, Summary::combine, Summary::logicalOr); + } +} diff --git a/java-checks-common/src/test/java/org/sonar/java/checks/helpers/logic/TernaryTest.java b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/logic/TernaryTest.java new file mode 100644 index 00000000000..272879a96b9 --- /dev/null +++ b/java-checks-common/src/test/java/org/sonar/java/checks/helpers/logic/TernaryTest.java @@ -0,0 +1,50 @@ +package org.sonar.java.checks.helpers.logic; + +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.java.checks.helpers.logic.Ternary.FALSE; +import static org.sonar.java.checks.helpers.logic.Ternary.TRUE; +import static org.sonar.java.checks.helpers.logic.Ternary.UNKNOWN; +import static org.sonar.java.checks.helpers.logic.Ternary.and; +import static org.sonar.java.checks.helpers.logic.Ternary.or; + + +class TernaryTest { + @Test + void test_from_boolean_conversion() { + assertThat(Ternary.of(true)).isEqualTo(TRUE); + assertThat(Ternary.ofNullable(true)).isEqualTo(TRUE); + + assertThat(Ternary.of(false)).isEqualTo(FALSE); + assertThat(Ternary.ofNullable(false)).isEqualTo(FALSE); + + assertThat(Ternary.ofNullable(null)).isEqualTo(UNKNOWN); + } + + @Test + void test_or() { + assertThat(or(TRUE, FALSE, UNKNOWN)).isEqualTo(TRUE); + assertThat(or(FALSE, UNKNOWN)).isEqualTo(UNKNOWN); + assertThat(or(FALSE, FALSE)).isEqualTo(FALSE); + } + + @Test + void test_and() { + assertThat(and(TRUE, FALSE, UNKNOWN)).isEqualTo(FALSE); + assertThat(and(TRUE, UNKNOWN)).isEqualTo(UNKNOWN); + assertThat(and(TRUE, TRUE)).isEqualTo(TRUE); + } + + @Test + void test_collectors() { + List tfu = List.of(TRUE, FALSE, UNKNOWN); + assertThat(tfu.stream().collect(and())).isEqualTo(FALSE); + assertThat(tfu.stream().collect(or())).isEqualTo(TRUE); + + List uut = List.of(UNKNOWN, UNKNOWN, TRUE); + assertThat(uut.stream().collect(and())).isEqualTo(UNKNOWN); + assertThat(uut.stream().collect(or())).isEqualTo(TRUE); + } +} diff --git a/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedTypeParameterCheck.java b/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedTypeParameterCheck.java index cc665586b05..4ee471df803 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedTypeParameterCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/unused/UnusedTypeParameterCheck.java @@ -28,6 +28,8 @@ import org.sonar.plugins.java.api.tree.TypeParameterTree; import org.sonar.plugins.java.api.tree.TypeParameters; +import static org.sonar.java.checks.helpers.logic.HelperPredicates.isUsed; + @Rule(key = "S2326") public class UnusedTypeParameterCheck extends IssuableSubscriptionVisitor { @@ -43,7 +45,7 @@ public void visitNode(Tree tree) { TypeParameters typeParameters = tree.is(Tree.Kind.METHOD) ? ((MethodTree) tree).typeParameters() : ((ClassTree) tree).typeParameters(); for (TypeParameterTree typeParameter : typeParameters) { Symbol symbol = typeParameter.symbol(); - if (!symbol.isUnknown() && symbol.usages().isEmpty()) { + if(isUsed(symbol).isFalse()) { String message = String.format(ISSUE_MESSAGE, symbol.name(), tree.kind().name().toLowerCase(Locale.ROOT)); reportIssue(typeParameter.identifier(), message); }