1919import static com .google .common .collect .ImmutableSet .toImmutableSet ;
2020import static com .google .errorprone .BugPattern .SeverityLevel .ERROR ;
2121import static com .google .errorprone .matchers .Description .NO_MATCH ;
22+ import static com .google .errorprone .util .ASTHelpers .getReceiver ;
2223import static com .google .errorprone .util .ASTHelpers .getSymbol ;
24+ import static com .google .errorprone .util .ASTHelpers .getType ;
25+ import static com .google .errorprone .util .ASTHelpers .hasAnnotation ;
26+ import static java .lang .String .format ;
27+ import static java .util .stream .Collectors .joining ;
2328
2429import com .google .common .base .Joiner ;
2530import com .google .common .collect .ImmutableSet ;
31+ import com .google .common .collect .LinkedHashMultimap ;
32+ import com .google .common .collect .SetMultimap ;
2633import com .google .common .collect .Sets ;
2734import com .google .common .collect .Sets .SetView ;
2835import com .google .errorprone .BugPattern ;
3138import com .google .errorprone .annotations .Immutable ;
3239import com .google .errorprone .bugpatterns .BugChecker ;
3340import com .google .errorprone .bugpatterns .BugChecker .ClassTreeMatcher ;
41+ import com .google .errorprone .bugpatterns .BugChecker .LambdaExpressionTreeMatcher ;
3442import com .google .errorprone .bugpatterns .BugChecker .MemberReferenceTreeMatcher ;
3543import com .google .errorprone .bugpatterns .BugChecker .MethodInvocationTreeMatcher ;
3644import com .google .errorprone .bugpatterns .BugChecker .MethodTreeMatcher ;
4250import com .google .errorprone .matchers .Description ;
4351import com .google .errorprone .util .ASTHelpers ;
4452import com .sun .source .tree .ClassTree ;
53+ import com .sun .source .tree .IdentifierTree ;
54+ import com .sun .source .tree .LambdaExpressionTree ;
4555import com .sun .source .tree .MemberReferenceTree ;
56+ import com .sun .source .tree .MemberSelectTree ;
4657import com .sun .source .tree .MethodInvocationTree ;
4758import com .sun .source .tree .MethodTree ;
4859import com .sun .source .tree .NewClassTree ;
4960import com .sun .source .tree .Tree ;
5061import com .sun .source .tree .TypeParameterTree ;
62+ import com .sun .source .tree .VariableTree ;
63+ import com .sun .source .util .TreePathScanner ;
5164import com .sun .tools .javac .code .Symbol ;
5265import com .sun .tools .javac .code .Symbol .ClassSymbol ;
5366import com .sun .tools .javac .code .Symbol .MethodSymbol ;
67+ import com .sun .tools .javac .code .Symbol .TypeSymbol ;
5468import com .sun .tools .javac .code .Symbol .TypeVariableSymbol ;
69+ import com .sun .tools .javac .code .Symbol .VarSymbol ;
5570import com .sun .tools .javac .code .Type ;
71+ import com .sun .tools .javac .code .Type .ClassType ;
5672import com .sun .tools .javac .tree .JCTree .JCMemberReference ;
5773import com .sun .tools .javac .tree .JCTree .JCNewClass ;
5874import java .util .Collection ;
5975import java .util .HashMap ;
76+ import java .util .HashSet ;
6077import java .util .Map ;
6178import java .util .Optional ;
79+ import java .util .Set ;
80+ import javax .lang .model .element .ElementKind ;
6281
6382/** A {@link BugChecker}; see the associated {@link BugPattern} annotation for details. */
6483@ BugPattern (
6887 documentSuppression = false )
6988public class ImmutableChecker extends BugChecker
7089 implements ClassTreeMatcher ,
90+ LambdaExpressionTreeMatcher ,
7191 NewClassTreeMatcher ,
7292 MethodInvocationTreeMatcher ,
7393 MethodTreeMatcher ,
7494 MemberReferenceTreeMatcher {
7595
7696 private final WellKnownMutability wellKnownMutability ;
7797 private final ImmutableSet <String > immutableAnnotations ;
98+ private final boolean matchLambdas ;
7899
79100 ImmutableChecker (ImmutableSet <String > immutableAnnotations ) {
80101 this (ErrorProneFlags .empty (), immutableAnnotations );
@@ -87,6 +108,125 @@ public ImmutableChecker(ErrorProneFlags flags) {
87108 private ImmutableChecker (ErrorProneFlags flags , ImmutableSet <String > immutableAnnotations ) {
88109 this .wellKnownMutability = WellKnownMutability .fromFlags (flags );
89110 this .immutableAnnotations = immutableAnnotations ;
111+ this .matchLambdas = flags .getBoolean ("ImmutableChecker:MatchLambdas" ).orElse (true );
112+ }
113+
114+ @ Override
115+ public Description matchLambdaExpression (LambdaExpressionTree tree , VisitorState state ) {
116+ if (!matchLambdas ) {
117+ return NO_MATCH ;
118+ }
119+ TypeSymbol lambdaType = getType (tree ).tsym ;
120+ if (!hasImmutableAnnotation (lambdaType , state )) {
121+ return NO_MATCH ;
122+ }
123+ Set <VarSymbol > variablesClosed = new HashSet <>();
124+ SetMultimap <ClassSymbol , MethodSymbol > typesClosed = LinkedHashMultimap .create ();
125+ Set <VarSymbol > variablesOwnedByLambda = new HashSet <>();
126+
127+ new TreePathScanner <Void , Void >() {
128+ @ Override
129+ public Void visitVariable (VariableTree tree , Void unused ) {
130+ var symbol = getSymbol (tree );
131+ variablesOwnedByLambda .add (symbol );
132+ return super .visitVariable (tree , null );
133+ }
134+
135+ @ Override
136+ public Void visitMethodInvocation (MethodInvocationTree tree , Void unused ) {
137+ if (getReceiver (tree ) == null ) {
138+ var symbol = getSymbol (tree );
139+ if (!symbol .isStatic ()) {
140+ // TODO(b/77333859): This isn't precise. What we really want is the type of `this`, if
141+ // the method call were qualified with it.
142+ typesClosed .put ((ClassSymbol ) symbol .owner , symbol );
143+ }
144+ }
145+ return super .visitMethodInvocation (tree , null );
146+ }
147+
148+ @ Override
149+ public Void visitMemberSelect (MemberSelectTree tree , Void unused ) {
150+ // Special case the access of fields to allow accessing fields which would pass an immutable
151+ // check.
152+ if (tree .getExpression () instanceof IdentifierTree
153+ && getSymbol (tree ) instanceof VarSymbol ) {
154+ handleIdentifier (getSymbol (tree ));
155+ // If we're only seeing a field access, don't complain about the fact we closed around
156+ // `this`.
157+ if (tree .getExpression () instanceof IdentifierTree
158+ && ((IdentifierTree ) tree .getExpression ()).getName ().contentEquals ("this" )) {
159+ return null ;
160+ }
161+ }
162+ return super .visitMemberSelect (tree , null );
163+ }
164+
165+ @ Override
166+ public Void visitIdentifier (IdentifierTree tree , Void unused ) {
167+ handleIdentifier (getSymbol (tree ));
168+ return super .visitIdentifier (tree , null );
169+ }
170+
171+ private void handleIdentifier (Symbol symbol ) {
172+ if (symbol instanceof VarSymbol && !variablesOwnedByLambda .contains (symbol )) {
173+ variablesClosed .add ((VarSymbol ) symbol );
174+ }
175+ }
176+ }.scan (state .getPath (), null );
177+
178+ ImmutableAnalysis analysis = createImmutableAnalysis (state );
179+ ImmutableSet <String > typarams =
180+ immutableTypeParametersInScope (getSymbol (tree ), state , analysis );
181+ variablesClosed .stream ()
182+ .map (closedVariable -> checkClosedLambdaVariable (closedVariable , tree , typarams , analysis ))
183+ .filter (Violation ::isPresent )
184+ .forEachOrdered (
185+ v -> {
186+ String message = formLambdaReason (lambdaType ) + ", but " + v .message ();
187+ state .reportMatch (buildDescription (tree ).setMessage (message ).build ());
188+ });
189+ for (var entry : typesClosed .asMap ().entrySet ()) {
190+ var classSymbol = entry .getKey ();
191+ var methods = entry .getValue ();
192+ if (!hasImmutableAnnotation (classSymbol .type .tsym , state )) {
193+ String message =
194+ format (
195+ "%s, but accesses instance method(s) '%s' on '%s' which is not @Immutable." ,
196+ formLambdaReason (lambdaType ),
197+ methods .stream ().map (Symbol ::getSimpleName ).collect (joining (", " )),
198+ classSymbol .getSimpleName ());
199+ state .reportMatch (buildDescription (tree ).setMessage (message ).build ());
200+ }
201+ }
202+
203+ return NO_MATCH ;
204+ }
205+
206+ private Violation checkClosedLambdaVariable (
207+ VarSymbol closedVariable ,
208+ LambdaExpressionTree tree ,
209+ ImmutableSet <String > typarams ,
210+ ImmutableAnalysis analysis ) {
211+ if (!closedVariable .getKind ().equals (ElementKind .FIELD )) {
212+ return analysis .isThreadSafeType (false , typarams , closedVariable .type );
213+ }
214+ return analysis .isFieldImmutable (
215+ Optional .empty (),
216+ typarams ,
217+ (ClassSymbol ) closedVariable .owner ,
218+ (ClassType ) closedVariable .owner .type ,
219+ closedVariable ,
220+ (t , v ) -> buildDescription (tree ));
221+ }
222+
223+ private static String formLambdaReason (TypeSymbol typeSymbol ) {
224+ return "This lambda implements @Immutable interface '" + typeSymbol .getSimpleName () + "'" ;
225+ }
226+
227+ private boolean hasImmutableAnnotation (TypeSymbol tsym , VisitorState state ) {
228+ return immutableAnnotations .stream ()
229+ .anyMatch (annotation -> hasAnnotation (tsym , annotation , state ));
90230 }
91231
92232 // check instantiations of `@ImmutableTypeParameter`s in method references
@@ -203,7 +343,7 @@ public Description matchClass(ClassTree tree, VisitorState state) {
203343 if (!difference .isEmpty ()) {
204344 return buildDescription (tree )
205345 .setMessage (
206- String . format (
346+ format (
207347 "could not find type(s) referenced by containerOf: %s" ,
208348 Joiner .on ("', '" ).join (difference )))
209349 .build ();
@@ -219,7 +359,7 @@ public Description matchClass(ClassTree tree, VisitorState state) {
219359 if (!immutableAndContainer .isEmpty ()) {
220360 return buildDescription (tree )
221361 .setMessage (
222- String . format (
362+ format (
223363 "using both @ImmutableTypeParameter and containerOf is redundant: %s" ,
224364 Joiner .on ("', '" ).join (immutableAndContainer )))
225365 .build ();
@@ -276,7 +416,7 @@ private Description.Builder describeClass(
276416 message = "type annotated with @Immutable could not be proven immutable: " + info .message ();
277417 } else {
278418 message =
279- String . format (
419+ format (
280420 "Class extends @Immutable type %s, but is not immutable: %s" ,
281421 annotation .typeName (), info .message ());
282422 }
@@ -323,7 +463,7 @@ public Description.Builder describe(Tree tree, Violation info) {
323463
324464 private Description .Builder describeAnonymous (Tree tree , Type superType , Violation info ) {
325465 String message =
326- String . format (
466+ format (
327467 "Class extends @Immutable type %s, but is not immutable: %s" ,
328468 superType , info .message ());
329469 return buildDescription (tree ).setMessage (message );
@@ -339,8 +479,7 @@ private Description checkSubtype(ClassTree tree, VisitorState state) {
339479 return NO_MATCH ;
340480 }
341481 String message =
342- String .format (
343- "Class extends @Immutable type %s, but is not annotated as immutable" , superType );
482+ format ("Class extends @Immutable type %s, but is not annotated as immutable" , superType );
344483 Fix fix =
345484 SuggestedFix .builder ()
346485 .prefixWith (tree , "@Immutable " )
@@ -360,8 +499,7 @@ private Type immutableSupertype(Symbol sym, VisitorState state) {
360499 }
361500 // Don't use getImmutableAnnotation here: subtypes of trusted types are
362501 // also trusted, only check for explicitly annotated supertypes.
363- if (immutableAnnotations .stream ()
364- .anyMatch (annotation -> ASTHelpers .hasAnnotation (superType .tsym , annotation , state ))) {
502+ if (hasImmutableAnnotation (superType .tsym , state )) {
365503 return superType ;
366504 }
367505 // We currently trust that @interface annotations are immutable, but don't enforce that
0 commit comments