Skip to content

Commit be243a1

Browse files
graememorganError Prone Team
authored and
Error Prone Team
committed
ImmutableChecker: check any variables that anonymous classes close around.
PiperOrigin-RevId: 450004419
1 parent 2cb3b54 commit be243a1

File tree

2 files changed

+209
-120
lines changed

2 files changed

+209
-120
lines changed

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

+139-120
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import static com.google.errorprone.util.ASTHelpers.getSymbol;
2626
import static com.google.errorprone.util.ASTHelpers.getType;
2727
import static com.google.errorprone.util.ASTHelpers.hasAnnotation;
28+
import static com.google.errorprone.util.ASTHelpers.isSameType;
2829
import static com.google.errorprone.util.ASTHelpers.isSubtype;
2930
import static com.google.errorprone.util.ASTHelpers.targetType;
3031
import static java.lang.String.format;
@@ -47,7 +48,6 @@
4748
import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher;
4849
import com.google.errorprone.bugpatterns.BugChecker.MethodTreeMatcher;
4950
import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher;
50-
import com.google.errorprone.bugpatterns.threadsafety.ImmutableAnalysis.ViolationReporter;
5151
import com.google.errorprone.bugpatterns.threadsafety.ThreadSafety.Violation;
5252
import com.google.errorprone.fixes.Fix;
5353
import com.google.errorprone.fixes.SuggestedFix;
@@ -100,6 +100,7 @@ public class ImmutableChecker extends BugChecker
100100

101101
private final WellKnownMutability wellKnownMutability;
102102
private final ImmutableSet<String> immutableAnnotations;
103+
private final boolean handleAnonymousClasses;
103104

104105
ImmutableChecker(ImmutableSet<String> immutableAnnotations) {
105106
this(ErrorProneFlags.empty(), immutableAnnotations);
@@ -112,6 +113,8 @@ public ImmutableChecker(ErrorProneFlags flags) {
112113
private ImmutableChecker(ErrorProneFlags flags, ImmutableSet<String> immutableAnnotations) {
113114
this.wellKnownMutability = WellKnownMutability.fromFlags(flags);
114115
this.immutableAnnotations = immutableAnnotations;
116+
this.handleAnonymousClasses =
117+
flags.getBoolean("ImmutableChecker:HandleAnonymousClasses").orElse(true);
115118
}
116119

117120
@Override
@@ -128,123 +131,11 @@ public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState
128131
if (!hasImmutableAnnotation(lambdaType, state)) {
129132
return NO_MATCH;
130133
}
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);
210135

211136
return NO_MATCH;
212137
}
213138

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-
248139
private boolean hasImmutableAnnotation(TypeSymbol tsym, VisitorState state) {
249140
return immutableAnnotations.stream()
250141
.anyMatch(annotation -> hasAnnotation(tsym, annotation, state));
@@ -483,6 +374,10 @@ private Description handleAnonymousClass(
483374
if (superType == null) {
484375
return NO_MATCH;
485376
}
377+
378+
if (handleAnonymousClasses) {
379+
checkClosedTypes(tree, state, superType.tsym, analysis);
380+
}
486381
// We don't need to check that the superclass has an immutable instantiation.
487382
// The anonymous instance can only be referred to using a superclass type, so
488383
// the type arguments will be validated at any type use site where we care about
@@ -499,18 +394,142 @@ private Description handleAnonymousClass(
499394
Optional.of(tree),
500395
typarams,
501396
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));
508398
if (!info.isPresent()) {
509399
return NO_MATCH;
510400
}
511401
return describeAnonymous(tree, superType, info).build();
512402
}
513403

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+
514533
private Description.Builder describeAnonymous(Tree tree, Type superType, Violation info) {
515534
String message =
516535
format(

core/src/test/java/com/google/errorprone/bugpatterns/threadsafety/ImmutableCheckerTest.java

+70
Original file line numberDiff line numberDiff line change
@@ -2853,4 +2853,74 @@ public void chainedGettersAreAcceptable() {
28532853
"}")
28542854
.doTest();
28552855
}
2856+
2857+
@Test
2858+
public void anonymousClass_cannotCloseAroundMutableLocal() {
2859+
compilationHelper
2860+
.addSourceLines(
2861+
"Test.java",
2862+
"import com.google.errorprone.annotations.Immutable;",
2863+
"import java.util.List;",
2864+
"import java.util.ArrayList;",
2865+
"class Test {",
2866+
" @Immutable interface ImmutableFunction<A, B> { A apply(B b); }",
2867+
" void test(ImmutableFunction<Integer, Integer> f) {",
2868+
" List<Integer> xs = new ArrayList<>();",
2869+
" // BUG: Diagnostic contains:",
2870+
" test(new ImmutableFunction<>() {",
2871+
" @Override public Integer apply(Integer x) {",
2872+
" return xs.get(x);",
2873+
" }",
2874+
" });",
2875+
" }",
2876+
"}")
2877+
.doTest();
2878+
}
2879+
2880+
@Test
2881+
public void anonymousClass_hasMutableFieldSuppressed_noWarningAtUsageSite() {
2882+
compilationHelper
2883+
.addSourceLines(
2884+
"Test.java",
2885+
"import com.google.errorprone.annotations.Immutable;",
2886+
"import java.util.List;",
2887+
"import java.util.ArrayList;",
2888+
"class Test {",
2889+
" @Immutable interface ImmutableFunction<A, B> { A apply(B b); }",
2890+
" void test(ImmutableFunction<Integer, Integer> f) {",
2891+
" test(new ImmutableFunction<>() {",
2892+
" @Override public Integer apply(Integer x) {",
2893+
" return xs.get(x);",
2894+
" }",
2895+
" @SuppressWarnings(\"Immutable\")",
2896+
" List<Integer> xs = new ArrayList<>();",
2897+
" });",
2898+
" }",
2899+
"}")
2900+
.doTest();
2901+
}
2902+
2903+
@Test
2904+
public void anonymousClass_canCallSuperMethodOnNonImmutableSuperClass() {
2905+
compilationHelper
2906+
.addSourceLines(
2907+
"Test.java",
2908+
"import com.google.errorprone.annotations.Immutable;",
2909+
"import java.util.List;",
2910+
"import java.util.ArrayList;",
2911+
"class Test {",
2912+
" interface Function<A, B> { default void foo() {} }",
2913+
" @Immutable interface ImmutableFunction<A, B> extends Function<A, B> { A apply(B b);"
2914+
+ " }",
2915+
" void test(ImmutableFunction<Integer, Integer> f) {",
2916+
" test(new ImmutableFunction<>() {",
2917+
" @Override public Integer apply(Integer x) {",
2918+
" foo();",
2919+
" return 0;",
2920+
" }",
2921+
" });",
2922+
" }",
2923+
"}")
2924+
.doTest();
2925+
}
28562926
}

0 commit comments

Comments
 (0)