@@ -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