Skip to content

Commit 4564d96

Browse files
SONARJAVA-5553 Raise only when returned mutable actually reaches outside the class for S2384 (#5136)
1 parent 98324d0 commit 4564d96

5 files changed

Lines changed: 97 additions & 22 deletions

File tree

its/ruling/src/test/resources/eclipse-jetty-similar-to-main/java-S2384.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@
8888
196
8989
],
9090
"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java": [
91-
354,
92-
441
91+
354
9392
],
9493
"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java": [
9594
343,

its/ruling/src/test/resources/eclipse-jetty/java-S2384.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@
8888
196
8989
],
9090
"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/SecureRequestCustomizer.java": [
91-
354,
92-
441
91+
354
9392
],
9493
"org.eclipse.jetty:jetty-project:jetty-server/src/main/java/org/eclipse/jetty/server/handler/ContextHandler.java": [
9594
343,

its/ruling/src/test/resources/sonar-server/java-S2384.json

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,6 @@
278278
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/metric/ws/MetricsWs.java": [
279279
31
280280
],
281-
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/metric/ws/SearchAction.java": [
282-
98
283-
],
284281
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/notification/DefaultNotificationManager.java": [
285282
59
286283
],
@@ -353,15 +350,9 @@
353350
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/Visibility.java": [
354351
68
355352
],
356-
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/ws/GhostsAction.java": [
357-
149
358-
],
359353
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/ws/ProjectsWs.java": [
360354
32
361355
],
362-
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/ws/ProvisionedAction.java": [
363-
150
364-
],
365356
"org.sonarsource.sonarqube:sonar-server:src/main/java/org/sonar/server/project/ws/SearchMyProjectsData.java": [
366357
106,
367358
111,

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

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -606,16 +606,51 @@ static SetThroughPrivateMethods ofMap(final Map<String, String> map) {
606606
return new SetThroughPrivateMethods(null, map);
607607
}
608608

609-
public void cycle(List<Integer> l){
609+
public void cycle(List<Integer> l) {
610610
cycleA(l, 5);
611611
}
612-
private void cycleA(List<Integer> l, int i){
612+
613+
private void cycleA(List<Integer> l, int i) {
613614
cycleB(l, i);
614615
}
615-
private void cycleB(List<Integer> l, int i){
616+
617+
private void cycleB(List<Integer> l, int i) {
616618
if (i > 0) {
617619
cycleA(l, i - 1);
618620
}
619621
list = l; // Noncompliant
620622
}
621623
}
624+
625+
class ReturnedAndPassedThrough {
626+
private byte[] secureData = new byte[10];
627+
private byte[] data = new byte[10];
628+
629+
private byte[] getDataInternal() {
630+
return data; // Noncompliant
631+
}
632+
633+
private byte[] getSecureDataInternal() {
634+
return data; // Compliant: only called by `getSecureData()` which performs a copy.
635+
}
636+
637+
public byte[] getSecureData() {
638+
return Arrays.copyOf(getSecureDataInternal(), secureData.length);
639+
}
640+
641+
public byte[] getData() {
642+
if (secureData.length > 0) {
643+
return getDataInternal();
644+
} else {
645+
return getData();
646+
}
647+
}
648+
649+
public byte[] getDataWithTwoReturns() {
650+
if (data.length > 5) {
651+
return data; // Noncompliant
652+
}
653+
return data; // Noncompliant
654+
}
655+
}
656+

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

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,14 @@ public class MutableMembersUsageCheck extends BaseTreeVisitor implements JavaFil
9393

9494
private final Deque<String> methodSignatureStack = new ArrayDeque<>();
9595

96-
/** In the context of a method call, this means the argument at {@code argumentIndex}, is given by the
96+
/**
97+
* In the context of a method call, this means the argument at {@code argumentIndex}, is given by the
9798
* calling method parameter at {@code parameterIndex}. For instance, in {@code int f(int a, int b) { g(0, 1, a); }}, we would have
98-
* a mapping (0, 2): parameter 0 of `f`, i.e. a, is used as argument 2 of `g`. */
99+
* a mapping (0, 2): parameter 0 of `f`, i.e. a, is used as argument 2 of `g`.
100+
*/
99101
@VisibleForTesting
100-
record ArgumentParameterMapping(int parameterIndex, int argumentIndex) {}
102+
record ArgumentParameterMapping(int parameterIndex, int argumentIndex) {
103+
}
101104

102105
/**
103106
* Maps index of arguments at the call site to parameters of the calling method.
@@ -150,10 +153,22 @@ private static class MutableDataPropagationGraph {
150153
*/
151154
private final Set<String> nonPrivateMethods = new HashSet<>();
152155

156+
/**
157+
* Maps method that return the result of another method
158+
*/
159+
private final Map<String, List<String>> passingThroughMethod = new HashMap<>();
160+
161+
/**
162+
* Map methods that return private mutable values to the identifier of the mutable they are returning.
163+
*/
164+
private final Map<String, List<IdentifierTree>> methodsReturningPrivateMutable = new HashMap<>();
165+
153166
public void clear() {
154167
mutableStoredByMethod.clear();
155168
callGraph.clear();
156169
nonPrivateMethods.clear();
170+
passingThroughMethod.clear();
171+
methodsReturningPrivateMutable.clear();
157172
}
158173

159174
private static final List<CallSite> EMPTY_CALL_SITE_LIST = new ArrayList<>();
@@ -220,6 +235,26 @@ public void reportMutableStoreReachableByOutsideCall(JavaFileScannerContext cont
220235
}
221236
}
222237

238+
private void reportMutableFieldReachingToOutside(JavaFileScannerContext context, JavaFileScanner check) {
239+
Set<IdentifierTree> mutableValuesToReport = new HashSet<>();
240+
ArrayDeque<String> queue = new ArrayDeque<>(nonPrivateMethods);
241+
Set<String> explored = new HashSet<>();
242+
243+
while (!queue.isEmpty()) {
244+
String current = queue.pop();
245+
if (methodsReturningPrivateMutable.containsKey(current)) {
246+
mutableValuesToReport.addAll(methodsReturningPrivateMutable.get(current));
247+
}
248+
if (explored.add(current)) {
249+
queue.addAll(passingThroughMethod.getOrDefault(current, new ArrayList<>()));
250+
}
251+
}
252+
253+
for (IdentifierTree identifierTree : mutableValuesToReport) {
254+
context.reportIssue(check, identifierTree, "Return a copy of \"" + identifierTree.name() + "\".");
255+
}
256+
}
257+
223258
/**
224259
* Record whether the method is callable from outside the class.
225260
*/
@@ -238,7 +273,6 @@ public void addMethodInvocation(Symbol.MethodSymbol methodSymbol, Arguments argu
238273
.add(new CallSite(methodSymbol.signature(), mutableParameters));
239274
}
240275

241-
242276
/**
243277
* Returns the indexes of arguments to the indexes of mutable parameters that correspond.
244278
*/
@@ -263,6 +297,17 @@ public void addStore(String methodSignature, IdentifierTree assignedMember, int
263297
mutableStoredByMethod.computeIfAbsent(methodSignature, k -> new ArrayList<>())
264298
.add(new ParameterStore(assignedMember, parameterIndex));
265299
}
300+
301+
private void addPassingThroughMethod(String callingMethodSignature, String calledMethodSignature) {
302+
passingThroughMethod.computeIfAbsent(callingMethodSignature, key -> new ArrayList<>())
303+
.add(calledMethodSignature);
304+
}
305+
306+
private void addMethodReturningPrivateMutable(String methodSignature, IdentifierTree mutableIdentifier) {
307+
methodsReturningPrivateMutable
308+
.computeIfAbsent(methodSignature, key -> new ArrayList<>())
309+
.add(mutableIdentifier);
310+
}
266311
}
267312

268313
private final MutableDataPropagationGraph dataPropagationGraph = new MutableDataPropagationGraph();
@@ -272,6 +317,7 @@ public void scanFile(final JavaFileScannerContext context) {
272317
this.context = context;
273318
scan(context.getTree());
274319
dataPropagationGraph.reportMutableStoreReachableByOutsideCall(context, this);
320+
dataPropagationGraph.reportMutableFieldReachingToOutside(context, this);
275321
dataPropagationGraph.clear();
276322
}
277323

@@ -366,10 +412,15 @@ private void checkReturnedExpression(ExpressionTree expression) {
366412
}
367413
if (expression.is(Tree.Kind.IDENTIFIER)) {
368414
IdentifierTree identifierTree = (IdentifierTree) expression;
369-
if (identifierTree.symbol().isPrivate() && !isOnlyAssignedImmutableVariable((Symbol.VariableSymbol) identifierTree.symbol())) {
370-
context.reportIssue(this, identifierTree, "Return a copy of \"" + identifierTree.name() + "\".");
415+
if (identifierTree.symbol().isPrivate()
416+
&& !isOnlyAssignedImmutableVariable((Symbol.VariableSymbol) identifierTree.symbol())
417+
&& !methodSignatureStack.isEmpty()) {
418+
dataPropagationGraph.addMethodReturningPrivateMutable(methodSignatureStack.peek(), identifierTree);
371419
}
372420
}
421+
if (expression instanceof MethodInvocationTree methodInvocationTree && !methodSignatureStack.isEmpty()) {
422+
dataPropagationGraph.addPassingThroughMethod(methodSignatureStack.peek(), methodInvocationTree.methodSymbol().signature());
423+
}
373424
}
374425

375426
private static boolean isOnlyAssignedImmutableVariable(Symbol.VariableSymbol symbol) {

0 commit comments

Comments
 (0)