diff --git a/java-checks-test-sources/default/src/main/java/checks/BoxedBooleanExpressionsCheckSample.java b/java-checks-test-sources/default/src/main/java/checks/BoxedBooleanExpressionsCheckSample.java index 65bef0e0f64..6641cfbfc83 100644 --- a/java-checks-test-sources/default/src/main/java/checks/BoxedBooleanExpressionsCheckSample.java +++ b/java-checks-test-sources/default/src/main/java/checks/BoxedBooleanExpressionsCheckSample.java @@ -3,6 +3,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -16,6 +18,19 @@ public String mapOptionalBoolean() { .orElse("mystery"); } + public void ifPresentOrElseOptionalBoolean() { + optionalBoolean() + .ifPresentOrElse(b -> { + if (b) { // Compliant, b can not be null in the context of ifPresentOrElse + } + }, () -> { + }); + // coverage + List zeroArg = List.of(() -> {}, () -> {}); + List> oneArg = List.of(x -> {}, x -> {}); + List> twoArg = List.of((a, b) -> a ? b : false); // Noncompliant + } + public String lambdaWithBooleanParameter() { Function function = b -> b ? "truthy" : "falsey"; // Noncompliant return "foo"; diff --git a/java-checks/src/main/java/org/sonar/java/checks/BoxedBooleanExpressionsCheck.java b/java-checks/src/main/java/org/sonar/java/checks/BoxedBooleanExpressionsCheck.java index 7c4d61bcccf..15fc5a651be 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/BoxedBooleanExpressionsCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/BoxedBooleanExpressionsCheck.java @@ -36,7 +36,6 @@ import org.sonar.plugins.java.api.semantic.Symbol; import org.sonar.plugins.java.api.semantic.SymbolMetadata; import org.sonar.plugins.java.api.semantic.Type.Primitives; -import org.sonar.plugins.java.api.tree.Arguments; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.BinaryExpressionTree; import org.sonar.plugins.java.api.tree.ConditionalExpressionTree; @@ -51,8 +50,11 @@ import org.sonar.plugins.java.api.tree.Tree.Kind; import org.sonar.plugins.java.api.tree.TypeCastTree; import org.sonar.plugins.java.api.tree.UnaryExpressionTree; +import org.sonar.plugins.java.api.tree.VariableTree; import org.sonar.plugins.java.api.tree.WhileStatementTree; +import static org.sonar.java.checks.helpers.MethodTreeUtils.lamdaArgumentAt; +import static org.sonar.java.checks.helpers.MethodTreeUtils.parentMethodInvocationOfArgumentAtPos; import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY; @Rule(key = "S5411") @@ -135,12 +137,13 @@ public void visitIfStatement(IfStatementTree tree) { @Override public void visitLambdaExpression(LambdaExpressionTree lambdaExpressionTree) { - // parameters of lambdas applied on an Optional are always non-null - if (lambdaExpressionTree.parent() instanceof Arguments arguments && - arguments.parent() instanceof MethodInvocationTree methodInvocationTree - && OPTIONAL_METHODS_WITH_LAMBDA_CONSUMING_NON_NULL.matches(methodInvocationTree)) { - Symbol parameterName = lambdaExpressionTree.parameters().get(0).symbol(); - safeSymbols.put(parameterName, true); + VariableTree lambdaFistParameter = lamdaArgumentAt(lambdaExpressionTree, 0); + if (lambdaFistParameter != null) { + MethodInvocationTree methodInvocationTree = parentMethodInvocationOfArgumentAtPos(lambdaExpressionTree, 0); + // parameters of lambdas applied on an Optional are always non-null + if (methodInvocationTree != null && OPTIONAL_METHODS_WITH_LAMBDA_CONSUMING_NON_NULL.matches(methodInvocationTree)) { + safeSymbols.put(lambdaFistParameter.symbol(), true); + } } super.visitLambdaExpression(lambdaExpressionTree); } diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java index 88566efefce..4fcf1a75740 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java @@ -26,15 +26,18 @@ import org.sonar.java.model.ModifiersUtils; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.tree.Arguments; import org.sonar.plugins.java.api.tree.ArrayTypeTree; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; import org.sonar.plugins.java.api.tree.LambdaExpressionTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.MethodTree; import org.sonar.plugins.java.api.tree.Modifier; import org.sonar.plugins.java.api.tree.NewClassTree; +import org.sonar.plugins.java.api.tree.ParenthesizedTree; import org.sonar.plugins.java.api.tree.PrimitiveTypeTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.TypeTree; @@ -63,6 +66,42 @@ private static boolean isParameterStringArray(MethodTree m) { return result; } + /** + * @return null when: + * - argumentCandidate is null + * - the parent is not a method invocation (ignoring parent parentheses) + * - argumentCandidate is not at the expected argument position + * Otherwise, returns the parent method invocation. + */ + @Nullable + public static MethodInvocationTree parentMethodInvocationOfArgumentAtPos(@Nullable ExpressionTree argumentCandidate, int expectedArgumentPosition) { + if (argumentCandidate == null) { + return null; + } + while (argumentCandidate.parent() instanceof ParenthesizedTree parenthesizedTree) { + argumentCandidate = parenthesizedTree; + } + if (argumentCandidate.parent() instanceof Arguments arguments && + expectedArgumentPosition < arguments.size() && + arguments.get(expectedArgumentPosition) == argumentCandidate && + arguments.parent() instanceof MethodInvocationTree methodInvocationTree) { + return methodInvocationTree; + } + return null; + } + + @Nullable + public static VariableTree lamdaArgumentAt(@Nullable LambdaExpressionTree lambdaExpressionTree, int argumentPosition) { + if (lambdaExpressionTree == null) { + return null; + } + List parameters = lambdaExpressionTree.parameters(); + if (parameters.size() > argumentPosition) { + return parameters.get(argumentPosition); + } + return null; + } + public static boolean isEqualsMethod(MethodTree m) { boolean hasEqualsSignature = isNamed(m, "equals") && returnsPrimitive(m, "boolean") && hasObjectParameter(m); return isPublic(m) && !isStatic(m) && hasEqualsSignature; diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java index ed7d419bf15..b1e93c0d961 100644 --- a/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java @@ -19,19 +19,27 @@ import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; +import org.sonar.java.model.expression.LiteralTreeImpl; import org.sonar.plugins.java.api.semantic.MethodMatchers; import org.sonar.plugins.java.api.tree.BaseTreeVisitor; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.IdentifierTree; +import org.sonar.plugins.java.api.tree.LambdaExpressionTree; +import org.sonar.plugins.java.api.tree.LiteralTree; import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree; import org.sonar.plugins.java.api.tree.MethodInvocationTree; import org.sonar.plugins.java.api.tree.MethodTree; +import org.sonar.plugins.java.api.tree.NewClassTree; +import org.sonar.plugins.java.api.tree.ParenthesizedTree; import org.sonar.plugins.java.api.tree.Tree; +import org.sonar.plugins.java.api.tree.VariableTree; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.sonar.java.checks.helpers.MethodTreeUtils.lamdaArgumentAt; +import static org.sonar.java.checks.helpers.MethodTreeUtils.parentMethodInvocationOfArgumentAtPos; class MethodTreeUtilsTest { @@ -139,6 +147,55 @@ public void visitMethodInvocation(MethodInvocationTree tree) { assertThat(MethodTreeUtils.consecutiveMethodInvocation(((MemberSelectExpressionTree) toStringMethod.parent()).identifier())).isEmpty(); } + @Test + void parent_method_invocation_of_argument_at_pos() { + CompilationUnitTree compilationUnitTree = JParserTestUtils.parse(""" + class A { + int field1 = 1; + int field2 = Math.max((2), 3); + Thread field3 = new Thread("4"); + } + """); + ClassTree classTree = (ClassTree) compilationUnitTree.types().get(0); + List members = classTree.members(); + LiteralTree literal1 = (LiteralTree) ((VariableTree) members.get(0)).initializer(); + MethodInvocationTree mathMax = (MethodInvocationTree) ((VariableTree) members.get(1)).initializer(); + LiteralTree literal2 = (LiteralTree) ((ParenthesizedTree) mathMax.arguments().get(0)).expression(); + LiteralTree literal3 = (LiteralTree) mathMax.arguments().get(1); + NewClassTree newThread = (NewClassTree) ((VariableTree) members.get(2)).initializer(); + LiteralTree literal4 = (LiteralTree) newThread.arguments().get(0); + LiteralTreeImpl expressionWithoutParent = new LiteralTreeImpl(Tree.Kind.STRING_LITERAL, null); + + assertThat(parentMethodInvocationOfArgumentAtPos(null, 0)).isNull(); + assertThat(parentMethodInvocationOfArgumentAtPos(expressionWithoutParent, 0)).isNull(); + assertThat(parentMethodInvocationOfArgumentAtPos(literal1, 0)).isNull(); + assertThat(parentMethodInvocationOfArgumentAtPos(literal2, 0)).isSameAs(mathMax); + assertThat(parentMethodInvocationOfArgumentAtPos(literal2, 1)).isNull(); + assertThat(parentMethodInvocationOfArgumentAtPos(literal2, 2)).isNull(); + assertThat(parentMethodInvocationOfArgumentAtPos(literal3, 0)).isNull(); + assertThat(parentMethodInvocationOfArgumentAtPos(literal3, 1)).isSameAs(mathMax); + assertThat(parentMethodInvocationOfArgumentAtPos(literal4, 0)).isNull(); + } + + @Test + void lamda_argument_ar() { + CompilationUnitTree compilationUnitTree = JParserTestUtils.parse(""" + class A { + Runnable lambda1 = () -> {}; + java.util.function.Consumer lambda2 = a -> {}; + } + """); + ClassTree classTree = (ClassTree) compilationUnitTree.types().get(0); + List members = classTree.members(); + LambdaExpressionTree lambda1 = (LambdaExpressionTree) ((VariableTree) members.get(0)).initializer(); + LambdaExpressionTree lambda2 = (LambdaExpressionTree) ((VariableTree) members.get(1)).initializer(); + + assertThat(lamdaArgumentAt(null, 0)).isNull(); + assertThat(lamdaArgumentAt(lambda1, 0)).isNull(); + assertThat(lamdaArgumentAt(lambda2, 0)).isSameAs(lambda2.parameters().get(0)); + assertThat(lamdaArgumentAt(lambda2, 1)).isNull(); + } + private MethodTree parseMethod(String code) { CompilationUnitTree compilationUnitTree = JParserTestUtils.parse(code); ClassTree classTree = (ClassTree) compilationUnitTree.types().get(0);