Skip to content

Commit 4fc1ff5

Browse files
graememorganError Prone Team
authored andcommitted
ImmutableChecker: start of work to match lambdas.
PiperOrigin-RevId: 441716890
1 parent 006aa1d commit 4fc1ff5

3 files changed

Lines changed: 302 additions & 9 deletions

File tree

core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableAnalysis.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ Violation areFieldsImmutable(
257257
}
258258

259259
/** Check a single field for immutability. */
260-
private Violation isFieldImmutable(
260+
Violation isFieldImmutable(
261261
Optional<Tree> tree,
262262
ImmutableSet<String> immutableTyParams,
263263
ClassSymbol classSym,

core/src/main/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableChecker.java

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@
1919
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2020
import static com.google.errorprone.BugPattern.SeverityLevel.ERROR;
2121
import static com.google.errorprone.matchers.Description.NO_MATCH;
22+
import static com.google.errorprone.util.ASTHelpers.getReceiver;
2223
import 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

2429
import com.google.common.base.Joiner;
2530
import com.google.common.collect.ImmutableSet;
31+
import com.google.common.collect.LinkedHashMultimap;
32+
import com.google.common.collect.SetMultimap;
2633
import com.google.common.collect.Sets;
2734
import com.google.common.collect.Sets.SetView;
2835
import com.google.errorprone.BugPattern;
@@ -31,6 +38,7 @@
3138
import com.google.errorprone.annotations.Immutable;
3239
import com.google.errorprone.bugpatterns.BugChecker;
3340
import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher;
41+
import com.google.errorprone.bugpatterns.BugChecker.LambdaExpressionTreeMatcher;
3442
import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher;
3543
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
3644
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
@@ -42,23 +50,34 @@
4250
import com.google.errorprone.matchers.Description;
4351
import com.google.errorprone.util.ASTHelpers;
4452
import com.sun.source.tree.ClassTree;
53+
import com.sun.source.tree.IdentifierTree;
54+
import com.sun.source.tree.LambdaExpressionTree;
4555
import com.sun.source.tree.MemberReferenceTree;
56+
import com.sun.source.tree.MemberSelectTree;
4657
import com.sun.source.tree.MethodInvocationTree;
4758
import com.sun.source.tree.MethodTree;
4859
import com.sun.source.tree.NewClassTree;
4960
import com.sun.source.tree.Tree;
5061
import com.sun.source.tree.TypeParameterTree;
62+
import com.sun.source.tree.VariableTree;
63+
import com.sun.source.util.TreePathScanner;
5164
import com.sun.tools.javac.code.Symbol;
5265
import com.sun.tools.javac.code.Symbol.ClassSymbol;
5366
import com.sun.tools.javac.code.Symbol.MethodSymbol;
67+
import com.sun.tools.javac.code.Symbol.TypeSymbol;
5468
import com.sun.tools.javac.code.Symbol.TypeVariableSymbol;
69+
import com.sun.tools.javac.code.Symbol.VarSymbol;
5570
import com.sun.tools.javac.code.Type;
71+
import com.sun.tools.javac.code.Type.ClassType;
5672
import com.sun.tools.javac.tree.JCTree.JCMemberReference;
5773
import com.sun.tools.javac.tree.JCTree.JCNewClass;
5874
import java.util.Collection;
5975
import java.util.HashMap;
76+
import java.util.HashSet;
6077
import java.util.Map;
6178
import 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(
@@ -68,13 +87,15 @@
6887
documentSuppression = false)
6988
public 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

Comments
 (0)