25
25
import static com .google .errorprone .util .ASTHelpers .getSymbol ;
26
26
import static com .google .errorprone .util .ASTHelpers .getType ;
27
27
import static com .google .errorprone .util .ASTHelpers .hasAnnotation ;
28
+ import static com .google .errorprone .util .ASTHelpers .isSameType ;
28
29
import static com .google .errorprone .util .ASTHelpers .isSubtype ;
29
30
import static com .google .errorprone .util .ASTHelpers .targetType ;
30
31
import static java .lang .String .format ;
47
48
import com .google .errorprone .bugpatterns .BugChecker .MethodInvocationTreeMatcher ;
48
49
import com .google .errorprone .bugpatterns .BugChecker .MethodTreeMatcher ;
49
50
import com .google .errorprone .bugpatterns .BugChecker .NewClassTreeMatcher ;
50
- import com .google .errorprone .bugpatterns .threadsafety .ImmutableAnalysis .ViolationReporter ;
51
51
import com .google .errorprone .bugpatterns .threadsafety .ThreadSafety .Violation ;
52
52
import com .google .errorprone .fixes .Fix ;
53
53
import com .google .errorprone .fixes .SuggestedFix ;
@@ -100,6 +100,7 @@ public class ImmutableChecker extends BugChecker
100
100
101
101
private final WellKnownMutability wellKnownMutability ;
102
102
private final ImmutableSet <String > immutableAnnotations ;
103
+ private final boolean handleAnonymousClasses ;
103
104
104
105
ImmutableChecker (ImmutableSet <String > immutableAnnotations ) {
105
106
this (ErrorProneFlags .empty (), immutableAnnotations );
@@ -112,6 +113,8 @@ public ImmutableChecker(ErrorProneFlags flags) {
112
113
private ImmutableChecker (ErrorProneFlags flags , ImmutableSet <String > immutableAnnotations ) {
113
114
this .wellKnownMutability = WellKnownMutability .fromFlags (flags );
114
115
this .immutableAnnotations = immutableAnnotations ;
116
+ this .handleAnonymousClasses =
117
+ flags .getBoolean ("ImmutableChecker:HandleAnonymousClasses" ).orElse (true );
115
118
}
116
119
117
120
@ Override
@@ -128,123 +131,11 @@ public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState
128
131
if (!hasImmutableAnnotation (lambdaType , state )) {
129
132
return NO_MATCH ;
130
133
}
131
- Set <VarSymbol > variablesClosed = new HashSet <>();
132
- SetMultimap <ClassSymbol , MethodSymbol > typesClosed = LinkedHashMultimap .create ();
133
- Set <VarSymbol > variablesOwnedByLambda = new HashSet <>();
134
-
135
- new TreePathScanner <Void , Void >() {
136
- @ Override
137
- public Void visitVariable (VariableTree tree , Void unused ) {
138
- var symbol = getSymbol (tree );
139
- variablesOwnedByLambda .add (symbol );
140
- return super .visitVariable (tree , null );
141
- }
142
-
143
- @ Override
144
- public Void visitMethodInvocation (MethodInvocationTree tree , Void unused ) {
145
- if (getReceiver (tree ) == null ) {
146
- var symbol = getSymbol (tree );
147
- if (!symbol .isStatic ()) {
148
- effectiveTypeOfThis (symbol , getCurrentPath (), state )
149
- .ifPresent (t -> typesClosed .put (t , symbol ));
150
- }
151
- }
152
- return super .visitMethodInvocation (tree , null );
153
- }
154
-
155
- @ Override
156
- public Void visitMemberSelect (MemberSelectTree tree , Void unused ) {
157
- // Note: member selects are not intrinsically problematic; the issue is what might be on the
158
- // LHS of them, which is going to be handled by another visit* method.
159
-
160
- // If we're only seeing a field access, don't complain about the fact we closed around
161
- // `this`. This is special-case as it would otherwise be vexing to complain about accessing
162
- // a field of type ImmutableList.
163
- if (tree .getExpression () instanceof IdentifierTree
164
- && getSymbol (tree ) instanceof VarSymbol
165
- && ((IdentifierTree ) tree .getExpression ()).getName ().contentEquals ("this" )) {
166
- handleIdentifier (getSymbol (tree ));
167
- return null ;
168
- }
169
- return super .visitMemberSelect (tree , null );
170
- }
171
-
172
- @ Override
173
- public Void visitIdentifier (IdentifierTree tree , Void unused ) {
174
- handleIdentifier (getSymbol (tree ));
175
- return super .visitIdentifier (tree , null );
176
- }
177
-
178
- private void handleIdentifier (Symbol symbol ) {
179
- if (symbol instanceof VarSymbol
180
- && !variablesOwnedByLambda .contains (symbol )
181
- && !symbol .isStatic ()) {
182
- variablesClosed .add ((VarSymbol ) symbol );
183
- }
184
- }
185
- }.scan (state .getPath (), null );
186
-
187
- ImmutableSet <String > typarams =
188
- immutableTypeParametersInScope (getSymbol (tree ), state , analysis );
189
- variablesClosed .stream ()
190
- .map (closedVariable -> checkClosedLambdaVariable (closedVariable , tree , typarams , analysis ))
191
- .filter (Violation ::isPresent )
192
- .forEachOrdered (
193
- v -> {
194
- String message = formLambdaReason (lambdaType ) + ", but " + v .message ();
195
- state .reportMatch (buildDescription (tree ).setMessage (message ).build ());
196
- });
197
- for (var entry : typesClosed .asMap ().entrySet ()) {
198
- var classSymbol = entry .getKey ();
199
- var methods = entry .getValue ();
200
- if (!hasImmutableAnnotation (classSymbol .type .tsym , state )) {
201
- String message =
202
- format (
203
- "%s, but accesses instance method(s) '%s' on '%s' which is not @Immutable." ,
204
- formLambdaReason (lambdaType ),
205
- methods .stream ().map (Symbol ::getSimpleName ).collect (joining (", " )),
206
- classSymbol .getSimpleName ());
207
- state .reportMatch (buildDescription (tree ).setMessage (message ).build ());
208
- }
209
- }
134
+ checkClosedTypes (tree , state , lambdaType , analysis );
210
135
211
136
return NO_MATCH ;
212
137
}
213
138
214
- /**
215
- * Gets the effective type of `this`, had the bare invocation of {@code symbol} been qualified
216
- * with it.
217
- */
218
- private static Optional <ClassSymbol > effectiveTypeOfThis (
219
- MethodSymbol symbol , TreePath currentPath , VisitorState state ) {
220
- return stream (currentPath .iterator ())
221
- .filter (ClassTree .class ::isInstance )
222
- .map (t -> ASTHelpers .getSymbol ((ClassTree ) t ))
223
- .filter (c -> isSubtype (c .type , symbol .owner .type , state ))
224
- .findFirst ();
225
- }
226
-
227
- private Violation checkClosedLambdaVariable (
228
- VarSymbol closedVariable ,
229
- LambdaExpressionTree tree ,
230
- ImmutableSet <String > typarams ,
231
- ImmutableAnalysis analysis ) {
232
- if (!closedVariable .getKind ().equals (ElementKind .FIELD )) {
233
- return analysis .isThreadSafeType (false , typarams , closedVariable .type );
234
- }
235
- return analysis .isFieldImmutable (
236
- Optional .empty (),
237
- typarams ,
238
- (ClassSymbol ) closedVariable .owner ,
239
- (ClassType ) closedVariable .owner .type ,
240
- closedVariable ,
241
- (t , v ) -> buildDescription (tree ));
242
- }
243
-
244
- private static String formLambdaReason (TypeSymbol typeSymbol ) {
245
- return "This lambda implements @Immutable interface '" + typeSymbol .getSimpleName () + "'" ;
246
- }
247
-
248
139
private boolean hasImmutableAnnotation (TypeSymbol tsym , VisitorState state ) {
249
140
return immutableAnnotations .stream ()
250
141
.anyMatch (annotation -> hasAnnotation (tsym , annotation , state ));
@@ -483,6 +374,10 @@ private Description handleAnonymousClass(
483
374
if (superType == null ) {
484
375
return NO_MATCH ;
485
376
}
377
+
378
+ if (handleAnonymousClasses ) {
379
+ checkClosedTypes (tree , state , superType .tsym , analysis );
380
+ }
486
381
// We don't need to check that the superclass has an immutable instantiation.
487
382
// The anonymous instance can only be referred to using a superclass type, so
488
383
// the type arguments will be validated at any type use site where we care about
@@ -499,18 +394,142 @@ private Description handleAnonymousClass(
499
394
Optional .of (tree ),
500
395
typarams ,
501
396
ASTHelpers .getType (tree ),
502
- new ViolationReporter () {
503
- @ Override
504
- public Description .Builder describe (Tree tree , Violation info ) {
505
- return describeAnonymous (tree , superType , info );
506
- }
507
- });
397
+ (t , i ) -> describeAnonymous (t , superType , i ));
508
398
if (!info .isPresent ()) {
509
399
return NO_MATCH ;
510
400
}
511
401
return describeAnonymous (tree , superType , info ).build ();
512
402
}
513
403
404
+ private void checkClosedTypes (
405
+ Tree lambdaOrAnonymousClass ,
406
+ VisitorState state ,
407
+ TypeSymbol lambdaType ,
408
+ ImmutableAnalysis analysis ) {
409
+ Set <VarSymbol > variablesClosed = new HashSet <>();
410
+ SetMultimap <ClassSymbol , MethodSymbol > typesClosed = LinkedHashMultimap .create ();
411
+ Set <VarSymbol > variablesOwnedByLambda = new HashSet <>();
412
+
413
+ new TreePathScanner <Void , Void >() {
414
+ @ Override
415
+ public Void visitVariable (VariableTree tree , Void unused ) {
416
+ var symbol = getSymbol (tree );
417
+ variablesOwnedByLambda .add (symbol );
418
+ return super .visitVariable (tree , null );
419
+ }
420
+
421
+ @ Override
422
+ public Void visitMethodInvocation (MethodInvocationTree tree , Void unused ) {
423
+ if (getReceiver (tree ) == null ) {
424
+ var symbol = getSymbol (tree );
425
+ if (!symbol .isStatic () && !symbol .isConstructor ()) {
426
+ effectiveTypeOfThis (symbol , getCurrentPath (), state )
427
+ .filter (t -> !isSameType (t .type , getType (lambdaOrAnonymousClass ), state ))
428
+ .ifPresent (t -> typesClosed .put (t , symbol ));
429
+ }
430
+ }
431
+ return super .visitMethodInvocation (tree , null );
432
+ }
433
+
434
+ @ Override
435
+ public Void visitMemberSelect (MemberSelectTree tree , Void unused ) {
436
+ // Note: member selects are not intrinsically problematic; the issue is what might be on the
437
+ // LHS of them, which is going to be handled by another visit* method.
438
+
439
+ // If we're only seeing a field access, don't complain about the fact we closed around
440
+ // `this`. This is special-case as it would otherwise be vexing to complain about accessing
441
+ // a field of type ImmutableList.
442
+ if (tree .getExpression () instanceof IdentifierTree
443
+ && getSymbol (tree ) instanceof VarSymbol
444
+ && ((IdentifierTree ) tree .getExpression ()).getName ().contentEquals ("this" )) {
445
+ handleIdentifier (getSymbol (tree ));
446
+ return null ;
447
+ }
448
+ return super .visitMemberSelect (tree , null );
449
+ }
450
+
451
+ @ Override
452
+ public Void visitIdentifier (IdentifierTree tree , Void unused ) {
453
+ handleIdentifier (getSymbol (tree ));
454
+ return super .visitIdentifier (tree , null );
455
+ }
456
+
457
+ private void handleIdentifier (Symbol symbol ) {
458
+ if (symbol instanceof VarSymbol
459
+ && !variablesOwnedByLambda .contains (symbol )
460
+ && !symbol .isStatic ()) {
461
+ variablesClosed .add ((VarSymbol ) symbol );
462
+ }
463
+ }
464
+ }.scan (state .getPath (), null );
465
+
466
+ ImmutableSet <String > typarams =
467
+ immutableTypeParametersInScope (getSymbol (lambdaOrAnonymousClass ), state , analysis );
468
+ variablesClosed .stream ()
469
+ .map (
470
+ closedVariable ->
471
+ checkClosedVariable (closedVariable , lambdaOrAnonymousClass , typarams , analysis ))
472
+ .filter (Violation ::isPresent )
473
+ .forEachOrdered (
474
+ v -> {
475
+ String message =
476
+ formAnonymousReason (lambdaOrAnonymousClass , lambdaType ) + ", but " + v .message ();
477
+ state .reportMatch (
478
+ buildDescription (lambdaOrAnonymousClass ).setMessage (message ).build ());
479
+ });
480
+ for (var entry : typesClosed .asMap ().entrySet ()) {
481
+ var classSymbol = entry .getKey ();
482
+ var methods = entry .getValue ();
483
+ if (!hasImmutableAnnotation (classSymbol .type .tsym , state )) {
484
+ String message =
485
+ format (
486
+ "%s, but accesses instance method(s) '%s' on '%s' which is not @Immutable." ,
487
+ formAnonymousReason (lambdaOrAnonymousClass , lambdaType ),
488
+ methods .stream ().map (Symbol ::getSimpleName ).collect (joining (", " )),
489
+ classSymbol .getSimpleName ());
490
+ state .reportMatch (buildDescription (lambdaOrAnonymousClass ).setMessage (message ).build ());
491
+ }
492
+ }
493
+ }
494
+
495
+ /**
496
+ * Gets the effective type of `this`, had the bare invocation of {@code symbol} been qualified
497
+ * with it.
498
+ */
499
+ private static Optional <ClassSymbol > effectiveTypeOfThis (
500
+ MethodSymbol symbol , TreePath currentPath , VisitorState state ) {
501
+ return stream (currentPath .iterator ())
502
+ .filter (ClassTree .class ::isInstance )
503
+ .map (t -> ASTHelpers .getSymbol ((ClassTree ) t ))
504
+ .filter (c -> isSubtype (c .type , symbol .owner .type , state ))
505
+ .findFirst ();
506
+ }
507
+
508
+ private Violation checkClosedVariable (
509
+ VarSymbol closedVariable ,
510
+ Tree tree ,
511
+ ImmutableSet <String > typarams ,
512
+ ImmutableAnalysis analysis ) {
513
+ if (!closedVariable .getKind ().equals (ElementKind .FIELD )) {
514
+ return analysis .isThreadSafeType (false , typarams , closedVariable .type );
515
+ }
516
+ return analysis .isFieldImmutable (
517
+ Optional .empty (),
518
+ typarams ,
519
+ (ClassSymbol ) closedVariable .owner ,
520
+ (ClassType ) closedVariable .owner .type ,
521
+ closedVariable ,
522
+ (t , v ) -> buildDescription (tree ));
523
+ }
524
+
525
+ private static String formAnonymousReason (Tree tree , TypeSymbol typeSymbol ) {
526
+ return "This "
527
+ + (tree instanceof LambdaExpressionTree ? "lambda" : "anonymous class" )
528
+ + " implements @Immutable interface '"
529
+ + typeSymbol .getSimpleName ()
530
+ + "'" ;
531
+ }
532
+
514
533
private Description .Builder describeAnonymous (Tree tree , Type superType , Violation info ) {
515
534
String message =
516
535
format (
0 commit comments