Skip to content

Commit 8ab2b64

Browse files
authored
KeyFor Checker: Convert annotations with parse errors to top for method invocations
1 parent b688593 commit 8ab2b64

File tree

6 files changed

+189
-6
lines changed

6 files changed

+189
-6
lines changed

checker/src/main/java/org/checkerframework/checker/nullness/KeyForAnnotatedTypeFactory.java

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.checkerframework.checker.nullness;
22

33
import com.sun.source.tree.ExpressionTree;
4+
import com.sun.source.tree.MethodInvocationTree;
45
import com.sun.source.tree.NewClassTree;
56
import com.sun.source.tree.Tree;
67
import java.lang.annotation.Annotation;
@@ -10,6 +11,7 @@
1011
import java.util.LinkedHashSet;
1112
import java.util.Set;
1213
import javax.lang.model.element.AnnotationMirror;
14+
import javax.lang.model.element.Element;
1315
import javax.lang.model.element.ExecutableElement;
1416
import org.checkerframework.checker.nullness.qual.KeyFor;
1517
import org.checkerframework.checker.nullness.qual.KeyForBottom;
@@ -18,14 +20,21 @@
1820
import org.checkerframework.checker.signature.qual.CanonicalName;
1921
import org.checkerframework.common.basetype.BaseTypeChecker;
2022
import org.checkerframework.dataflow.cfg.node.Node;
23+
import org.checkerframework.dataflow.expression.JavaExpression;
24+
import org.checkerframework.dataflow.expression.Unknown;
2125
import org.checkerframework.dataflow.util.NodeUtils;
2226
import org.checkerframework.framework.flow.CFAbstractAnalysis;
27+
import org.checkerframework.framework.type.AnnotatedTypeFactory;
2328
import org.checkerframework.framework.type.AnnotatedTypeMirror;
29+
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
2430
import org.checkerframework.framework.type.GenericAnnotatedTypeFactory;
2531
import org.checkerframework.framework.type.QualifierHierarchy;
2632
import org.checkerframework.framework.type.SubtypeIsSupersetQualifierHierarchy;
2733
import org.checkerframework.framework.type.treeannotator.ListTreeAnnotator;
2834
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
35+
import org.checkerframework.framework.util.JavaExpressionParseUtil;
36+
import org.checkerframework.framework.util.StringToJavaExpression;
37+
import org.checkerframework.framework.util.dependenttypes.DependentTypesHelper;
2938
import org.checkerframework.javacutil.AnnotationBuilder;
3039
import org.checkerframework.javacutil.AnnotationUtils;
3140
import org.checkerframework.javacutil.TreeUtils;
@@ -221,4 +230,73 @@ boolean isMapPut(Node node) {
221230
public boolean shouldWarnIfStubRedundantWithBytecode() {
222231
return false;
223232
}
233+
234+
@Override
235+
protected DependentTypesHelper createDependentTypesHelper() {
236+
return new KeyForDependentTypesHelper(this);
237+
}
238+
239+
/**
240+
* Converts KeyFor annotations with errors into {@code @UnknownKeyFor} in the type of method
241+
* invocations. This changes all qualifiers on the type of a method invocation expression, even
242+
* qualifiers that are not primary annotations. This is unsound for qualifiers on type arguments
243+
* or array elements on modifiable objects.
244+
*
245+
* <p>For example, if the type of some method called, {@code getList(...)}, is changed from {@code
246+
* List<@KeyFor("a ? b : c") String>} to {@code List<@UnknownKeyFor String>}, then the returned
247+
* list may have @UnknownKeyFor strings added.
248+
*
249+
* <pre>{@code
250+
* List<String> l = getList(...);
251+
* l.add(randoString);
252+
* }</pre>
253+
*
254+
* This is probably ok for the KeyFor Checker because most of the collections of keys are
255+
* unmodifiable.
256+
*/
257+
static class KeyForDependentTypesHelper extends DependentTypesHelper {
258+
259+
/**
260+
* Creates a {@code KeyForDependentTypesHelper}.
261+
*
262+
* @param factory annotated type factory
263+
*/
264+
public KeyForDependentTypesHelper(AnnotatedTypeFactory factory) {
265+
super(factory);
266+
}
267+
268+
@Override
269+
public void atMethodInvocation(
270+
AnnotatedExecutableType methodType, MethodInvocationTree methodInvocationTree) {
271+
if (!hasDependentAnnotations()) {
272+
return;
273+
}
274+
Element methodElt = TreeUtils.elementFromUse(methodInvocationTree);
275+
276+
// The annotations on `declaredMethodType` will be copied to `methodType`.
277+
AnnotatedExecutableType declaredMethodType =
278+
(AnnotatedExecutableType) factory.getAnnotatedType(methodElt);
279+
if (!hasDependentType(declaredMethodType)) {
280+
return;
281+
}
282+
283+
StringToJavaExpression stringToJavaExpr;
284+
stringToJavaExpr =
285+
stringExpr -> {
286+
JavaExpression result =
287+
StringToJavaExpression.atMethodInvocation(
288+
stringExpr, methodInvocationTree, factory.getChecker());
289+
Unknown unknown = result.containedOfClass(Unknown.class);
290+
if (unknown != null) {
291+
throw JavaExpressionParseUtil.constructJavaExpressionParseError(
292+
result.toString(), "Expression " + unknown.toString() + " is unparsable.");
293+
}
294+
return result;
295+
};
296+
297+
convertAnnotatedTypeMirror(stringToJavaExpr, declaredMethodType);
298+
this.viewpointAdaptedCopier.visit(declaredMethodType, methodType);
299+
this.errorAnnoReplacer.visit(methodType.getReturnType());
300+
}
301+
}
224302
}

checker/tests/nullness/Issue2470.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ static void ok() {
4343
static void buggy2() {
4444
new Example()
4545
.setS("test")
46-
// :: error:(contracts.precondition)
46+
// :: error: (contracts.precondition)
4747
.print();
4848
}
4949

checker/tests/nullness/Issue6520.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package open.falsepos;
2+
3+
import java.util.stream.Collectors;
4+
import java.util.stream.Stream;
5+
6+
class Issue6520 {
7+
8+
private record Data(String value) {}
9+
10+
Issue6520(Stream<Data> data) {
11+
data.collect(Collectors.groupingBy(Data::value)).entrySet().stream()
12+
.filter(entry -> entry.getValue().size() > 1);
13+
}
14+
}

checker/tests/nullness/Issue6750.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package open.falsepos;
2+
3+
import java.util.HashMap;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.function.Function;
7+
import java.util.stream.Collectors;
8+
import org.checkerframework.checker.nullness.qual.KeyFor;
9+
10+
public record Issue6750(String type) {
11+
12+
void needKeyFor(@KeyFor("#2") String s, Map<String, String> map) {
13+
throw new RuntimeException();
14+
}
15+
16+
@KeyFor("#1") String returnKeyFor(Map<String, String> map) {
17+
throw new RuntimeException();
18+
}
19+
20+
Map<String, String> getMap(Function<String, String> s) {
21+
throw new RuntimeException();
22+
}
23+
24+
void use() {
25+
// :: error: (argument)
26+
needKeyFor("", getMap(String::toString));
27+
// :: error: (expression.unparsable) :: error: (assignment)
28+
@KeyFor("getMap(String::toString)") String s = returnKeyFor(new HashMap<>(getMap(String::toString)));
29+
}
30+
31+
void method(List<Issue6750> externals) {
32+
externals.stream().collect(Collectors.groupingBy(Issue6750::type)).entrySet().stream()
33+
.forEach(
34+
values -> {
35+
// :: error: (assignment)
36+
@KeyFor({}) String b = values.getKey();
37+
});
38+
}
39+
40+
void test(List<Issue6750> externals) {
41+
externals.stream().collect(Collectors.groupingBy(Issue6750::type)).entrySet().stream()
42+
.forEach(
43+
values -> {
44+
throw new RuntimeException("");
45+
});
46+
}
47+
}

framework/src/main/java/org/checkerframework/framework/util/JavaExpressionParseUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1221,7 +1221,7 @@ public boolean isFlowParseError() {
12211221
* @return a {@link JavaExpressionParseException} for the expression {@code expr} with explanation
12221222
* {@code explanation}.
12231223
*/
1224-
private static JavaExpressionParseException constructJavaExpressionParseError(
1224+
public static JavaExpressionParseException constructJavaExpressionParseError(
12251225
String expr, String explanation) {
12261226
if (expr == null) {
12271227
throw new BugInCF("Must have an expression.");

framework/src/main/java/org/checkerframework/framework/util/dependenttypes/DependentTypesHelper.java

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
4848
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
4949
import org.checkerframework.framework.type.AnnotatedTypeParameterBounds;
50+
import org.checkerframework.framework.type.QualifierHierarchy;
5051
import org.checkerframework.framework.type.treeannotator.TreeAnnotator;
5152
import org.checkerframework.framework.type.visitor.AnnotatedTypeScanner;
5253
import org.checkerframework.framework.type.visitor.DoubleAnnotatedTypeScanner;
@@ -119,6 +120,12 @@ public class DependentTypesHelper {
119120
/** This scans an annotated type and returns a list of {@link DependentTypesError}. */
120121
private final ExpressionErrorCollector expressionErrorCollector = new ExpressionErrorCollector();
121122

123+
/**
124+
* This scans the annotated type and replaces any dependent type annotation that has a parse error
125+
* with the top annotation in the hierarchy.
126+
*/
127+
protected final ErrorAnnoReplacer errorAnnoReplacer;
128+
122129
/**
123130
* A scanner that applies a function to each {@link AnnotationMirror} and replaces it in the given
124131
* {@code AnnotatedTypeMirror}. (This side-effects the {@code AnnotatedTypeMirror}.)
@@ -129,7 +136,7 @@ public class DependentTypesHelper {
129136
* Copies annotations that might have been viewpoint adapted from the visited type (the first
130137
* formal parameter of {@code ViewpointAdaptedCopier#visit}) to the second formal parameter.
131138
*/
132-
private final ViewpointAdaptedCopier viewpointAdaptedCopier = new ViewpointAdaptedCopier();
139+
protected final ViewpointAdaptedCopier viewpointAdaptedCopier = new ViewpointAdaptedCopier();
133140

134141
/** The type mirror for java.lang.Object. */
135142
protected final TypeMirror objectTM;
@@ -141,7 +148,7 @@ public class DependentTypesHelper {
141148
*/
142149
public DependentTypesHelper(AnnotatedTypeFactory factory) {
143150
this.factory = factory;
144-
151+
this.errorAnnoReplacer = new ErrorAnnoReplacer(factory.getQualifierHierarchy());
145152
this.annoToElements = new HashMap<>();
146153
for (Class<? extends Annotation> expressionAnno : factory.getSupportedTypeQualifiers()) {
147154
List<ExecutableElement> elementList =
@@ -1198,6 +1205,38 @@ private ExpressionErrorCollector() {
11981205
}
11991206
}
12001207

1208+
/**
1209+
* Replaces a dependent type annotation with a parser error with the top qualifier in the
1210+
* hierarchy.
1211+
*/
1212+
protected class ErrorAnnoReplacer extends SimpleAnnotatedTypeScanner<Void, Void> {
1213+
1214+
/**
1215+
* Create an ErrorAnnoReplacer.
1216+
*
1217+
* @param qh the qualifier hierarchy
1218+
*/
1219+
private ErrorAnnoReplacer(QualifierHierarchy qh) {
1220+
super(
1221+
(AnnotatedTypeMirror type, Void aVoid) -> {
1222+
AnnotationMirrorSet replacementAnnos = null;
1223+
for (AnnotationMirror am : type.getPrimaryAnnotations()) {
1224+
if (isExpressionAnno(am) && !errorElements(am).isEmpty()) {
1225+
if (replacementAnnos == null) {
1226+
replacementAnnos = new AnnotationMirrorSet();
1227+
}
1228+
replacementAnnos.add(qh.getTopAnnotation(am));
1229+
}
1230+
}
1231+
1232+
if (replacementAnnos != null) {
1233+
type.replaceAnnotations(replacementAnnos);
1234+
}
1235+
return null;
1236+
});
1237+
}
1238+
}
1239+
12011240
/**
12021241
* Appends list2 to list1 in a new list. If either list is empty, returns the other. Thus, the
12031242
* result may be aliased to one of the arguments and the client should only read, not write into,
@@ -1226,7 +1265,11 @@ private static List<DependentTypesError> concatenate(
12261265
* visited type to the second formal parameter except for annotations on types that have been
12271266
* substituted.
12281267
*/
1229-
private class ViewpointAdaptedCopier extends DoubleAnnotatedTypeScanner<Void> {
1268+
protected class ViewpointAdaptedCopier extends DoubleAnnotatedTypeScanner<Void> {
1269+
1270+
/** Create a ViewpointAdaptedCopier. */
1271+
private ViewpointAdaptedCopier() {}
1272+
12301273
@Override
12311274
protected Void scan(AnnotatedTypeMirror from, AnnotatedTypeMirror to) {
12321275
if (from == null || to == null) {
@@ -1241,6 +1284,7 @@ protected Void scan(AnnotatedTypeMirror from, AnnotatedTypeMirror to) {
12411284
}
12421285
}
12431286
to.replaceAnnotations(replacements);
1287+
12441288
if (from.getKind() != to.getKind()
12451289
|| (from.getKind() == TypeKind.TYPEVAR
12461290
&& TypesUtils.isCapturedTypeVariable(to.getUnderlyingType()))) {
@@ -1277,7 +1321,7 @@ protected Void defaultAction(AnnotatedTypeMirror type1, AnnotatedTypeMirror type
12771321
* @param atm a type
12781322
* @return true if {@code atm} has any dependent type annotations
12791323
*/
1280-
private boolean hasDependentType(AnnotatedTypeMirror atm) {
1324+
protected boolean hasDependentType(AnnotatedTypeMirror atm) {
12811325
if (atm == null) {
12821326
return false;
12831327
}

0 commit comments

Comments
 (0)