Skip to content

Commit c771db2

Browse files
SONARJAVA-5146 Fix S5411 IndexOutOfBoundsException for lambda parameter (#5026)
1 parent 5899138 commit c771db2

4 files changed

Lines changed: 121 additions & 7 deletions

File tree

java-checks-test-sources/default/src/main/java/checks/BoxedBooleanExpressionsCheckSample.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import java.util.ArrayList;
44
import java.util.List;
55
import java.util.Optional;
6+
import java.util.function.BiFunction;
7+
import java.util.function.Consumer;
68
import java.util.function.Function;
79
import javax.annotation.CheckForNull;
810
import javax.annotation.Nonnull;
@@ -16,6 +18,19 @@ public String mapOptionalBoolean() {
1618
.orElse("mystery");
1719
}
1820

21+
public void ifPresentOrElseOptionalBoolean() {
22+
optionalBoolean()
23+
.ifPresentOrElse(b -> {
24+
if (b) { // Compliant, b can not be null in the context of ifPresentOrElse
25+
}
26+
}, () -> {
27+
});
28+
// coverage
29+
List<Runnable> zeroArg = List.of(() -> {}, () -> {});
30+
List<Consumer<String>> oneArg = List.of(x -> {}, x -> {});
31+
List<BiFunction<Boolean, Boolean, Boolean>> twoArg = List.of((a, b) -> a ? b : false); // Noncompliant
32+
}
33+
1934
public String lambdaWithBooleanParameter() {
2035
Function<Boolean, String> function = b -> b ? "truthy" : "falsey"; // Noncompliant
2136
return "foo";

java-checks/src/main/java/org/sonar/java/checks/BoxedBooleanExpressionsCheck.java

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import org.sonar.plugins.java.api.semantic.Symbol;
3737
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
3838
import org.sonar.plugins.java.api.semantic.Type.Primitives;
39-
import org.sonar.plugins.java.api.tree.Arguments;
4039
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
4140
import org.sonar.plugins.java.api.tree.BinaryExpressionTree;
4241
import org.sonar.plugins.java.api.tree.ConditionalExpressionTree;
@@ -51,8 +50,11 @@
5150
import org.sonar.plugins.java.api.tree.Tree.Kind;
5251
import org.sonar.plugins.java.api.tree.TypeCastTree;
5352
import org.sonar.plugins.java.api.tree.UnaryExpressionTree;
53+
import org.sonar.plugins.java.api.tree.VariableTree;
5454
import org.sonar.plugins.java.api.tree.WhileStatementTree;
5555

56+
import static org.sonar.java.checks.helpers.MethodTreeUtils.lamdaArgumentAt;
57+
import static org.sonar.java.checks.helpers.MethodTreeUtils.parentMethodInvocationOfArgumentAtPos;
5658
import static org.sonar.plugins.java.api.semantic.MethodMatchers.ANY;
5759

5860
@Rule(key = "S5411")
@@ -135,12 +137,13 @@ public void visitIfStatement(IfStatementTree tree) {
135137

136138
@Override
137139
public void visitLambdaExpression(LambdaExpressionTree lambdaExpressionTree) {
138-
// parameters of lambdas applied on an Optional are always non-null
139-
if (lambdaExpressionTree.parent() instanceof Arguments arguments &&
140-
arguments.parent() instanceof MethodInvocationTree methodInvocationTree
141-
&& OPTIONAL_METHODS_WITH_LAMBDA_CONSUMING_NON_NULL.matches(methodInvocationTree)) {
142-
Symbol parameterName = lambdaExpressionTree.parameters().get(0).symbol();
143-
safeSymbols.put(parameterName, true);
140+
VariableTree lambdaFistParameter = lamdaArgumentAt(lambdaExpressionTree, 0);
141+
if (lambdaFistParameter != null) {
142+
MethodInvocationTree methodInvocationTree = parentMethodInvocationOfArgumentAtPos(lambdaExpressionTree, 0);
143+
// parameters of lambdas applied on an Optional are always non-null
144+
if (methodInvocationTree != null && OPTIONAL_METHODS_WITH_LAMBDA_CONSUMING_NON_NULL.matches(methodInvocationTree)) {
145+
safeSymbols.put(lambdaFistParameter.symbol(), true);
146+
}
144147
}
145148
super.visitLambdaExpression(lambdaExpressionTree);
146149
}

java-checks/src/main/java/org/sonar/java/checks/helpers/MethodTreeUtils.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@
2626
import org.sonar.java.model.ModifiersUtils;
2727
import org.sonar.plugins.java.api.semantic.MethodMatchers;
2828
import org.sonar.plugins.java.api.semantic.Symbol;
29+
import org.sonar.plugins.java.api.tree.Arguments;
2930
import org.sonar.plugins.java.api.tree.ArrayTypeTree;
3031
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
3132
import org.sonar.plugins.java.api.tree.ClassTree;
33+
import org.sonar.plugins.java.api.tree.ExpressionTree;
3234
import org.sonar.plugins.java.api.tree.LambdaExpressionTree;
3335
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
3436
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
3537
import org.sonar.plugins.java.api.tree.MethodTree;
3638
import org.sonar.plugins.java.api.tree.Modifier;
3739
import org.sonar.plugins.java.api.tree.NewClassTree;
40+
import org.sonar.plugins.java.api.tree.ParenthesizedTree;
3841
import org.sonar.plugins.java.api.tree.PrimitiveTypeTree;
3942
import org.sonar.plugins.java.api.tree.Tree;
4043
import org.sonar.plugins.java.api.tree.TypeTree;
@@ -63,6 +66,42 @@ private static boolean isParameterStringArray(MethodTree m) {
6366
return result;
6467
}
6568

69+
/**
70+
* @return null when:
71+
* - argumentCandidate is null
72+
* - the parent is not a method invocation (ignoring parent parentheses)
73+
* - argumentCandidate is not at the expected argument position
74+
* Otherwise, returns the parent method invocation.
75+
*/
76+
@Nullable
77+
public static MethodInvocationTree parentMethodInvocationOfArgumentAtPos(@Nullable ExpressionTree argumentCandidate, int expectedArgumentPosition) {
78+
if (argumentCandidate == null) {
79+
return null;
80+
}
81+
while (argumentCandidate.parent() instanceof ParenthesizedTree parenthesizedTree) {
82+
argumentCandidate = parenthesizedTree;
83+
}
84+
if (argumentCandidate.parent() instanceof Arguments arguments &&
85+
expectedArgumentPosition < arguments.size() &&
86+
arguments.get(expectedArgumentPosition) == argumentCandidate &&
87+
arguments.parent() instanceof MethodInvocationTree methodInvocationTree) {
88+
return methodInvocationTree;
89+
}
90+
return null;
91+
}
92+
93+
@Nullable
94+
public static VariableTree lamdaArgumentAt(@Nullable LambdaExpressionTree lambdaExpressionTree, int argumentPosition) {
95+
if (lambdaExpressionTree == null) {
96+
return null;
97+
}
98+
List<VariableTree> parameters = lambdaExpressionTree.parameters();
99+
if (parameters.size() > argumentPosition) {
100+
return parameters.get(argumentPosition);
101+
}
102+
return null;
103+
}
104+
66105
public static boolean isEqualsMethod(MethodTree m) {
67106
boolean hasEqualsSignature = isNamed(m, "equals") && returnsPrimitive(m, "boolean") && hasObjectParameter(m);
68107
return isPublic(m) && !isStatic(m) && hasEqualsSignature;

java-checks/src/test/java/org/sonar/java/checks/helpers/MethodTreeUtilsTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,27 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121
import org.junit.jupiter.api.Test;
22+
import org.sonar.java.model.expression.LiteralTreeImpl;
2223
import org.sonar.plugins.java.api.semantic.MethodMatchers;
2324
import org.sonar.plugins.java.api.tree.BaseTreeVisitor;
2425
import org.sonar.plugins.java.api.tree.ClassTree;
2526
import org.sonar.plugins.java.api.tree.CompilationUnitTree;
2627
import org.sonar.plugins.java.api.tree.IdentifierTree;
28+
import org.sonar.plugins.java.api.tree.LambdaExpressionTree;
29+
import org.sonar.plugins.java.api.tree.LiteralTree;
2730
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
2831
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
2932
import org.sonar.plugins.java.api.tree.MethodTree;
33+
import org.sonar.plugins.java.api.tree.NewClassTree;
34+
import org.sonar.plugins.java.api.tree.ParenthesizedTree;
3035
import org.sonar.plugins.java.api.tree.Tree;
36+
import org.sonar.plugins.java.api.tree.VariableTree;
3137

3238
import static org.assertj.core.api.Assertions.assertThat;
3339
import static org.junit.jupiter.api.Assertions.assertFalse;
3440
import static org.junit.jupiter.api.Assertions.assertTrue;
41+
import static org.sonar.java.checks.helpers.MethodTreeUtils.lamdaArgumentAt;
42+
import static org.sonar.java.checks.helpers.MethodTreeUtils.parentMethodInvocationOfArgumentAtPos;
3543

3644
class MethodTreeUtilsTest {
3745

@@ -139,6 +147,55 @@ public void visitMethodInvocation(MethodInvocationTree tree) {
139147
assertThat(MethodTreeUtils.consecutiveMethodInvocation(((MemberSelectExpressionTree) toStringMethod.parent()).identifier())).isEmpty();
140148
}
141149

150+
@Test
151+
void parent_method_invocation_of_argument_at_pos() {
152+
CompilationUnitTree compilationUnitTree = JParserTestUtils.parse("""
153+
class A {
154+
int field1 = 1;
155+
int field2 = Math.max((2), 3);
156+
Thread field3 = new Thread("4");
157+
}
158+
""");
159+
ClassTree classTree = (ClassTree) compilationUnitTree.types().get(0);
160+
List<Tree> members = classTree.members();
161+
LiteralTree literal1 = (LiteralTree) ((VariableTree) members.get(0)).initializer();
162+
MethodInvocationTree mathMax = (MethodInvocationTree) ((VariableTree) members.get(1)).initializer();
163+
LiteralTree literal2 = (LiteralTree) ((ParenthesizedTree) mathMax.arguments().get(0)).expression();
164+
LiteralTree literal3 = (LiteralTree) mathMax.arguments().get(1);
165+
NewClassTree newThread = (NewClassTree) ((VariableTree) members.get(2)).initializer();
166+
LiteralTree literal4 = (LiteralTree) newThread.arguments().get(0);
167+
LiteralTreeImpl expressionWithoutParent = new LiteralTreeImpl(Tree.Kind.STRING_LITERAL, null);
168+
169+
assertThat(parentMethodInvocationOfArgumentAtPos(null, 0)).isNull();
170+
assertThat(parentMethodInvocationOfArgumentAtPos(expressionWithoutParent, 0)).isNull();
171+
assertThat(parentMethodInvocationOfArgumentAtPos(literal1, 0)).isNull();
172+
assertThat(parentMethodInvocationOfArgumentAtPos(literal2, 0)).isSameAs(mathMax);
173+
assertThat(parentMethodInvocationOfArgumentAtPos(literal2, 1)).isNull();
174+
assertThat(parentMethodInvocationOfArgumentAtPos(literal2, 2)).isNull();
175+
assertThat(parentMethodInvocationOfArgumentAtPos(literal3, 0)).isNull();
176+
assertThat(parentMethodInvocationOfArgumentAtPos(literal3, 1)).isSameAs(mathMax);
177+
assertThat(parentMethodInvocationOfArgumentAtPos(literal4, 0)).isNull();
178+
}
179+
180+
@Test
181+
void lamda_argument_ar() {
182+
CompilationUnitTree compilationUnitTree = JParserTestUtils.parse("""
183+
class A {
184+
Runnable lambda1 = () -> {};
185+
java.util.function.Consumer<String> lambda2 = a -> {};
186+
}
187+
""");
188+
ClassTree classTree = (ClassTree) compilationUnitTree.types().get(0);
189+
List<Tree> members = classTree.members();
190+
LambdaExpressionTree lambda1 = (LambdaExpressionTree) ((VariableTree) members.get(0)).initializer();
191+
LambdaExpressionTree lambda2 = (LambdaExpressionTree) ((VariableTree) members.get(1)).initializer();
192+
193+
assertThat(lamdaArgumentAt(null, 0)).isNull();
194+
assertThat(lamdaArgumentAt(lambda1, 0)).isNull();
195+
assertThat(lamdaArgumentAt(lambda2, 0)).isSameAs(lambda2.parameters().get(0));
196+
assertThat(lamdaArgumentAt(lambda2, 1)).isNull();
197+
}
198+
142199
private MethodTree parseMethod(String code) {
143200
CompilationUnitTree compilationUnitTree = JParserTestUtils.parse(code);
144201
ClassTree classTree = (ClassTree) compilationUnitTree.types().get(0);

0 commit comments

Comments
 (0)