Skip to content

Commit aecdb0e

Browse files
authored
RLC: allow wrapper types to have a differently-named @MustCall method from wrapped type (#6272)
1 parent a76764f commit aecdb0e

File tree

8 files changed

+306
-30
lines changed

8 files changed

+306
-30
lines changed

checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/MustCallAlias.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
* always be used in pairs. On a method, it is written on some formal parameter type and on the
1212
* method return type. On a constructor, it is written on some formal parameter type and on the
1313
* result type. Fulfilling the must-call obligation of one is equivalent to fulfilling the must-call
14-
* obligation of the other.
14+
* obligation of the other. Beyond its impact as a polymorphic annotation on {@code MustCall} types,
15+
* the Resource Leak Checker uses {@link MustCallAlias} annotations to more precisely determine when
16+
* a must-call obligation has been satisfied.
1517
*
1618
* <p>This annotation is useful for wrapper objects. For example, consider the declaration of {@code
1719
* java.net.Socket#getOutputStream}:
@@ -25,6 +27,25 @@
2527
* Calling {@code close()} on the returned {@code OutputStream} will close the underlying socket,
2628
* but the Socket may also be closed directly, which has the same effect.
2729
*
30+
* <h2>Type system semantics</h2>
31+
*
32+
* Within the Must Call Checker's type system, {@code @MustCallAlias} annotations have a semantics
33+
* different from a standard polymorphic annotation, in that the relevant actual parameter type and
34+
* return type at a call site are not equated in all cases. Given an actual parameter {@code p}
35+
* passed in a {@code @MustCallAlias} position at a call site, the return type of the call is
36+
* defined as follows:
37+
*
38+
* <ul>
39+
* <li>If the base return type has a non-empty {@code @InheritableMustCall("m")} annotation on its
40+
* declaration, and {@code p} has a non-empty {@code @MustCall} type, then the return type is
41+
* {@code @MustCall("m")}.
42+
* <li>In all other cases, the return type has the same {@code @MustCall} type as {@code p}.
43+
* </ul>
44+
*
45+
* {@link PolyMustCall} has an identical type system semantics. This special treatment is required
46+
* to allow for a wrapper object to have a must-call method with a different name than the must-call
47+
* method name for the wrapped object.
48+
*
2849
* <h2>Verifying {@code @MustCallAlias} annotations</h2>
2950
*
3051
* Suppose that {@code @MustCallAlias} is written on the type of formal parameter {@code p}.
@@ -47,7 +68,10 @@
4768
* </ul>
4869
*
4970
* When the -AnoResourceAliases command-line argument is passed to the checker, this annotation is
50-
* treated identically to {@link PolyMustCall}.
71+
* treated identically to {@link PolyMustCall}. That is, the annotation still impacts {@link
72+
* MustCall} types as a polymorphic annotation (see "Type system semantics" above), but it is not
73+
* used by the Resource Leak Checker to more precisely reason about when obligations have been
74+
* satisfied.
5175
*
5276
* @checker_framework.manual #resource-leak-checker Resource Leak Checker
5377
* @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism

checker-qual/src/main/java/org/checkerframework/checker/mustcall/qual/PolyMustCall.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
import org.checkerframework.framework.qual.PolymorphicQualifier;
99

1010
/**
11-
* The polymorphic qualifier for the Must Call type system.
11+
* The polymorphic qualifier for the Must Call type system. The semantics of this qualifier differ
12+
* from that of a standard polymorphic qualifier; see {@link MustCallAlias} for documentation of its
13+
* semantics.
1214
*
1315
* @checker_framework.manual #must-call-checker Must Call Checker
1416
* @checker_framework.manual #qualifier-polymorphism Qualifier polymorphism

checker/src/main/java/org/checkerframework/checker/mustcall/MustCallAnnotatedTypeFactory.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import javax.lang.model.element.Element;
2323
import javax.lang.model.element.ElementKind;
2424
import javax.lang.model.element.ExecutableElement;
25+
import javax.lang.model.element.TypeElement;
2526
import javax.lang.model.type.TypeMirror;
2627
import org.checkerframework.checker.mustcall.qual.CreatesMustCallFor;
2728
import org.checkerframework.checker.mustcall.qual.InheritableMustCall;
@@ -38,19 +39,23 @@
3839
import org.checkerframework.dataflow.cfg.node.LocalVariableNode;
3940
import org.checkerframework.dataflow.cfg.node.Node;
4041
import org.checkerframework.framework.flow.CFStore;
42+
import org.checkerframework.framework.type.AnnotatedTypeFactory;
4143
import org.checkerframework.framework.type.AnnotatedTypeMirror;
4244
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
4345
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
4446
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
4547
import org.checkerframework.framework.type.QualifierHierarchy;
4648
import org.checkerframework.framework.type.QualifierUpperBounds;
4749
import org.checkerframework.framework.type.SubtypeIsSubsetQualifierHierarchy;
50+
import org.checkerframework.framework.type.poly.DefaultQualifierPolymorphism;
51+
import org.checkerframework.framework.type.poly.QualifierPolymorphism;
4852
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
4953
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
5054
import org.checkerframework.framework.type.typeannotator.DefaultQualifierForUseTypeAnnotator;
5155
import org.checkerframework.framework.type.typeannotator.ListTypeAnnotator;
5256
import org.checkerframework.framework.type.typeannotator.TypeAnnotator;
5357
import org.checkerframework.javacutil.AnnotationBuilder;
58+
import org.checkerframework.javacutil.AnnotationMirrorMap;
5459
import org.checkerframework.javacutil.AnnotationMirrorSet;
5560
import org.checkerframework.javacutil.AnnotationUtils;
5661
import org.checkerframework.javacutil.ElementUtils;
@@ -220,6 +225,71 @@ protected void constructorFromUsePreSubstitution(
220225
super.constructorFromUsePreSubstitution(tree, type);
221226
}
222227

228+
/**
229+
* Class to implement the customized semantics of {@link MustCallAlias} (and {@link PolyMustCall})
230+
* annotations; see the {@link MustCallAlias} documentation for details.
231+
*/
232+
private class MustCallQualifierPolymorphism extends DefaultQualifierPolymorphism {
233+
/**
234+
* Creates a {@link MustCallQualifierPolymorphism}.
235+
*
236+
* @param env the processing environment
237+
* @param factory the factory for the current checker
238+
*/
239+
public MustCallQualifierPolymorphism(ProcessingEnvironment env, AnnotatedTypeFactory factory) {
240+
super(env, factory);
241+
}
242+
243+
@Override
244+
protected void replace(
245+
AnnotatedTypeMirror type, AnnotationMirrorMap<AnnotationMirror> replacements) {
246+
AnnotationMirrorMap<AnnotationMirror> realReplacements = replacements;
247+
AnnotationMirror extantPolyAnnoReplacement = null;
248+
TypeElement typeElement = TypesUtils.getTypeElement(type.getUnderlyingType());
249+
// only customize replacement for type elements
250+
if (typeElement != null) {
251+
assert replacements.size() == 1 && replacements.containsKey(POLY);
252+
extantPolyAnnoReplacement = replacements.get(POLY);
253+
if (AnnotationUtils.areSameByName(
254+
extantPolyAnnoReplacement, MustCall.class.getCanonicalName())) {
255+
List<String> extentReplacementVals =
256+
AnnotationUtils.getElementValueArray(
257+
extantPolyAnnoReplacement,
258+
getMustCallValueElement(),
259+
String.class,
260+
Collections.emptyList());
261+
// replacement only customized when parameter type has a non-empty must-call obligation
262+
if (!extentReplacementVals.isEmpty()) {
263+
AnnotationMirror inheritableMustCall =
264+
getDeclAnnotation(typeElement, InheritableMustCall.class);
265+
if (inheritableMustCall != null) {
266+
List<String> inheritableMustCallVals =
267+
AnnotationUtils.getElementValueArray(
268+
inheritableMustCall,
269+
inheritableMustCallValueElement,
270+
String.class,
271+
Collections.emptyList());
272+
if (!inheritableMustCallVals.equals(extentReplacementVals)) {
273+
// Use the must call values from the @InheritableMustCall annotation instead.
274+
// This allows for wrapper types to have a must-call method with a different
275+
// name than the must-call method for the wrapped type
276+
AnnotationMirror mustCall = createMustCall(inheritableMustCallVals);
277+
realReplacements = new AnnotationMirrorMap<>();
278+
realReplacements.put(POLY, mustCall);
279+
}
280+
}
281+
}
282+
}
283+
}
284+
super.replace(type, realReplacements);
285+
}
286+
}
287+
288+
@Override
289+
protected QualifierPolymorphism createQualifierPolymorphism() {
290+
return new MustCallQualifierPolymorphism(processingEnv, this);
291+
}
292+
223293
/**
224294
* Changes the type of each parameter not annotated as @Owning to @MustCallUnknown (top). Also
225295
* replaces the component type of the varargs array, if applicable.

checker/src/main/java/org/checkerframework/checker/resourceleak/MustCallConsistencyAnalyzer.java

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import java.util.Collections;
1717
import java.util.Deque;
1818
import java.util.EnumSet;
19+
import java.util.HashMap;
1920
import java.util.HashSet;
2021
import java.util.IdentityHashMap;
2122
import java.util.Iterator;
@@ -74,7 +75,6 @@
7475
import org.checkerframework.framework.flow.CFStore;
7576
import org.checkerframework.framework.flow.CFValue;
7677
import org.checkerframework.framework.type.AnnotatedTypeMirror;
77-
import org.checkerframework.framework.type.QualifierHierarchy;
7878
import org.checkerframework.framework.util.JavaExpressionParseUtil.JavaExpressionParseException;
7979
import org.checkerframework.framework.util.StringToJavaExpression;
8080
import org.checkerframework.javacutil.AnnotationUtils;
@@ -319,35 +319,34 @@ public boolean derivedFromMustCallAlias() {
319319

320320
/**
321321
* Gets the must-call methods (i.e. the list of methods that must be called to satisfy the
322-
* must-call obligation) of the resource represented by this Obligation.
322+
* must-call obligation) of each resource alias represented by this Obligation.
323323
*
324324
* @param rlAtf a Resource Leak Annotated Type Factory
325325
* @param mcStore a CFStore produced by the MustCall checker's dataflow analysis. If this is
326326
* null, then the default MustCall type of each variable's class will be used.
327-
* @return the list of must-call method names, or null if the resource's must-call obligations
328-
* are unsatisfiable (i.e. its value in the Must Call store is MustCallUnknown)
327+
* @return a map from each resource alias of this to a list of its must-call method names, or
328+
* null if the must-call obligations are unsatisfiable (i.e. the value of some tracked
329+
* resource alias of this in the Must Call store is MustCallUnknown)
329330
*/
330-
public @Nullable List<String> getMustCallMethods(
331+
public @Nullable Map<ResourceAlias, List<String>> getMustCallMethods(
331332
ResourceLeakAnnotatedTypeFactory rlAtf, @Nullable CFStore mcStore) {
333+
Map<ResourceAlias, List<String>> result = new HashMap<>(this.resourceAliases.size());
332334
MustCallAnnotatedTypeFactory mustCallAnnotatedTypeFactory =
333335
rlAtf.getTypeFactoryOfSubchecker(MustCallChecker.class);
334336

335-
// Need to get the LUB (ie, union) of the MC values, because if a CreatesMustCallFor
336-
// method was called on just one of the aliases then they all need to be treated as if
337-
// they need to call the relevant methods.
338-
QualifierHierarchy qualHierarchy = mustCallAnnotatedTypeFactory.getQualifierHierarchy();
339-
AnnotationMirror mcLub = mustCallAnnotatedTypeFactory.BOTTOM;
340337
for (ResourceAlias alias : this.resourceAliases) {
341338
AnnotationMirror mcAnno = getMustCallValue(alias, mcStore, mustCallAnnotatedTypeFactory);
342-
TypeMirror aliasTm = alias.reference.getType();
343-
mcLub = qualHierarchy.leastUpperBoundShallow(mcLub, aliasTm, mcAnno, aliasTm);
344-
}
345-
if (AnnotationUtils.areSameByName(
346-
mcLub, "org.checkerframework.checker.mustcall.qual.MustCall")) {
347-
return rlAtf.getMustCallValues(mcLub);
348-
} else {
349-
return null;
339+
if (!AnnotationUtils.areSameByName(mcAnno, MustCall.class.getCanonicalName())) {
340+
// MustCallUnknown; cannot be satisfied
341+
return null;
342+
}
343+
List<String> annoVals = rlAtf.getMustCallValues(mcAnno);
344+
// Really, annoVals should never be empty here; we should not have created the obligation in
345+
// the first place
346+
// TODO: add an assertion that annoVals is non-empty and address any failures
347+
result.put(alias, annoVals);
350348
}
349+
return result;
351350
}
352351

353352
/**
@@ -2324,11 +2323,12 @@ private static boolean varTrackedInObligations(
23242323
private void checkMustCall(
23252324
Obligation obligation, AccumulationStore cmStore, CFStore mcStore, String outOfScopeReason) {
23262325

2327-
List<String> mustCallValue = obligation.getMustCallMethods(typeFactory, mcStore);
2326+
Map<ResourceAlias, List<String>> mustCallValues =
2327+
obligation.getMustCallMethods(typeFactory, mcStore);
23282328

2329-
// optimization: if mustCallValue is null, always issue a warning (there is no way to satisfy
2329+
// optimization: if mustCallValues is null, always issue a warning (there is no way to satisfy
23302330
// the check). A null mustCallValue occurs when the type is top (@MustCallUnknown).
2331-
if (mustCallValue == null) {
2331+
if (mustCallValues == null) {
23322332
// Report the error at the first alias' definition. This choice is arbitrary but
23332333
// consistent.
23342334
ResourceAlias firstAlias = obligation.resourceAliases.iterator().next();
@@ -2345,14 +2345,20 @@ private void checkMustCall(
23452345
}
23462346
return;
23472347
}
2348-
// optimization: if there are no must-call methods, do not need to perform the check
2349-
if (mustCallValue.isEmpty()) {
2350-
return;
2348+
if (mustCallValues.isEmpty()) {
2349+
throw new TypeSystemError("unexpected empty must-call values for obligation " + obligation);
23512350
}
23522351

23532352
boolean mustCallSatisfied = false;
23542353
for (ResourceAlias alias : obligation.resourceAliases) {
23552354

2355+
List<String> mustCallValuesForAlias = mustCallValues.get(alias);
2356+
// optimization when there are no methods to call
2357+
if (mustCallValuesForAlias.isEmpty()) {
2358+
mustCallSatisfied = true;
2359+
break;
2360+
}
2361+
23562362
// sometimes the store is null! this looks like a bug in checker dataflow.
23572363
// TODO track down and report the root-cause bug
23582364
AccumulationValue cmValue = cmStore != null ? cmStore.getValue(alias.reference) : null;
@@ -2378,7 +2384,7 @@ private void checkMustCall(
23782384
.getEffectiveAnnotationInHierarchy(typeFactory.top);
23792385
}
23802386

2381-
if (calledMethodsSatisfyMustCall(mustCallValue, cmAnno)) {
2387+
if (calledMethodsSatisfyMustCall(mustCallValuesForAlias, cmAnno)) {
23822388
mustCallSatisfied = true;
23832389
break;
23842390
}
@@ -2394,7 +2400,7 @@ private void checkMustCall(
23942400
checker.reportError(
23952401
firstAlias.tree,
23962402
"required.method.not.called",
2397-
formatMissingMustCallMethods(mustCallValue),
2403+
formatMissingMustCallMethods(mustCallValues.get(firstAlias)),
23982404
firstAlias.stringForErrorMessage(),
23992405
firstAlias.reference.getType().toString(),
24002406
outOfScopeReason);

checker/src/main/java/org/checkerframework/checker/resourceleak/ResourceLeakAnnotatedTypeFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ protected ResourceLeakAnalysis createFlowAnalysis() {
206206
* Returns the {@link MustCall#value} element/argument of the @MustCall annotation on the class
207207
* type of {@code element}. If there is no such annotation, returns the empty list.
208208
*
209-
* <p>Do not use this method to get the MustCall value of an {@link
209+
* <p>Do not use this method to get the MustCall values of an {@link
210210
* org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation}. Instead, use
211211
* {@link
212212
* org.checkerframework.checker.resourceleak.MustCallConsistencyAnalyzer.Obligation#getMustCallMethods(ResourceLeakAnnotatedTypeFactory,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Tests for @PolyMustCall and @MustCallAlias where the must-call method of the return type has
2+
// a different name than the must-call method of the parameter type.
3+
4+
import org.checkerframework.checker.calledmethods.qual.EnsuresCalledMethods;
5+
import org.checkerframework.checker.mustcall.qual.*;
6+
7+
class PolyMustCallDifferentNames {
8+
9+
@InheritableMustCall("a")
10+
static class Wrapped {
11+
void a() {}
12+
}
13+
14+
@InheritableMustCall("b")
15+
static class Wrapper1 {
16+
private final @Owning Wrapped field;
17+
18+
public @PolyMustCall Wrapper1(@PolyMustCall Wrapped w) {
19+
// we get this error since we only have a field-assignment special case for @MustCallAlias,
20+
// not @PolyMustCall
21+
// :: error: (assignment)
22+
this.field = w;
23+
}
24+
25+
@EnsuresCalledMethods(
26+
value = {"this.field"},
27+
methods = {"a"})
28+
void b() {
29+
this.field.a();
30+
}
31+
}
32+
33+
static @PolyMustCall Wrapper1 getWrapper1(@PolyMustCall Wrapped w) {
34+
return new Wrapper1(w);
35+
}
36+
37+
@InheritableMustCall("c")
38+
static class Wrapper2 {
39+
private final @Owning Wrapped field;
40+
41+
public @MustCallAlias Wrapper2(@MustCallAlias Wrapped w) {
42+
this.field = w;
43+
}
44+
45+
@EnsuresCalledMethods(
46+
value = {"this.field"},
47+
methods = {"a"})
48+
void c() {
49+
this.field.a();
50+
}
51+
}
52+
53+
static @MustCallAlias Wrapper2 getWrapper2(@MustCallAlias Wrapped w) {
54+
return new Wrapper2(w);
55+
}
56+
57+
static void test1() {
58+
@MustCall("a") Wrapped x = new Wrapped();
59+
@MustCall("b") Wrapper1 w1 = new Wrapper1(x);
60+
@MustCall("b") Wrapper1 w2 = getWrapper1(x);
61+
// :: error: (assignment)
62+
@MustCall("a") Wrapper1 w3 = new Wrapper1(x);
63+
// :: error: (assignment)
64+
@MustCall("a") Wrapper1 w4 = getWrapper1(x);
65+
}
66+
67+
static void test2() {
68+
@MustCall("a") Wrapped x = new Wrapped();
69+
@MustCall("c") Wrapper2 w1 = new Wrapper2(x);
70+
@MustCall("c") Wrapper2 w2 = getWrapper2(x);
71+
// :: error: (assignment)
72+
@MustCall("a") Wrapper2 w3 = new Wrapper2(x);
73+
// :: error: (assignment)
74+
@MustCall("a") Wrapper2 w4 = getWrapper2(x);
75+
}
76+
}

0 commit comments

Comments
 (0)