Skip to content

Commit 8786e88

Browse files
haewifulmsridhar
andauthored
JSpecify: infer generic method type arguments for assignments (#1131)
This PR infers nullability of generic method type arguments for the case where the result of the call is assigned to a variable, e.g.: ```java class Foo<T extends @nullable Object> { Foo(T t) {} static <U extends @nullable Object> Foo<U> make(U u) { return new Foo<>(u); } void test() { Foo<@nullable Object> f1 = Foo.make(null); // legal Foo<Object> f2 = Foo.make(null); // error reported; arg cannot be @nullable } } ``` See the tests for (many) more examples. --------- Co-authored-by: Manu Sridharan <[email protected]>
1 parent 7a89102 commit 8786e88

File tree

6 files changed

+443
-36
lines changed

6 files changed

+443
-36
lines changed

nullaway/src/main/java/com/uber/nullaway/NullAway.java

+21-7
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ public Config getConfig() {
248248
*/
249249
private final Handler handler;
250250

251+
/** Returns the handler being used for this analysis */
252+
public Handler getHandler() {
253+
return handler;
254+
}
255+
251256
/**
252257
* entities relevant to field initialization per class. cached for performance. nulled out in
253258
* {@link #matchClass(ClassTree, VisitorState)}
@@ -277,6 +282,14 @@ public Config getConfig() {
277282
*/
278283
private final Map<ExpressionTree, Nullness> computedNullnessMap = new LinkedHashMap<>();
279284

285+
/** Logic and state for generics checking */
286+
private final GenericsChecks genericsChecks = new GenericsChecks();
287+
288+
/** Returns the GenericsChecks object for this analysis, used for generics-related checking */
289+
public GenericsChecks getGenericsChecks() {
290+
return genericsChecks;
291+
}
292+
280293
/**
281294
* Error Prone requires us to have an empty constructor for each Plugin, in addition to the
282295
* constructor taking an ErrorProneFlags object. This constructor should not be used anywhere
@@ -500,7 +513,7 @@ public Description matchAssignment(AssignmentTree tree, VisitorState state) {
500513
}
501514
// generics check
502515
if (lhsType != null && config.isJSpecifyMode()) {
503-
GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
516+
genericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
504517
}
505518

506519
if (config.isJSpecifyMode() && tree.getVariable() instanceof ArrayAccessTree) {
@@ -1494,7 +1507,7 @@ public Description matchVariable(VariableTree tree, VisitorState state) {
14941507
}
14951508
VarSymbol symbol = ASTHelpers.getSymbol(tree);
14961509
if (tree.getInitializer() != null && config.isJSpecifyMode()) {
1497-
GenericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
1510+
genericsChecks.checkTypeParameterNullnessForAssignability(tree, this, state);
14981511
}
14991512
if (!config.isLegacyAnnotationLocation()) {
15001513
checkNullableAnnotationPositionInType(
@@ -1662,6 +1675,7 @@ public Description matchClass(ClassTree tree, VisitorState state) {
16621675
class2Entities.clear();
16631676
class2ConstructorUninit.clear();
16641677
computedNullnessMap.clear();
1678+
genericsChecks.clearCache();
16651679
EnclosingEnvironmentNullness.instance(state.context).clear();
16661680
} else if (classAnnotationIntroducesPartialMarking(classSymbol)) {
16671681
// Handle the case where the top-class is unannotated, but there is a @NullMarked annotation
@@ -1885,13 +1899,13 @@ private Description handleInvocation(
18851899
Nullness.paramHasNullableAnnotation(methodSymbol, i, config)
18861900
? Nullness.NULLABLE
18871901
: ((config.isJSpecifyMode() && tree instanceof MethodInvocationTree)
1888-
? GenericsChecks.getGenericParameterNullnessAtInvocation(
1902+
? genericsChecks.getGenericParameterNullnessAtInvocation(
18891903
i, methodSymbol, (MethodInvocationTree) tree, state, config)
18901904
: Nullness.NONNULL);
18911905
}
18921906
}
18931907
if (config.isJSpecifyMode()) {
1894-
GenericsChecks.compareGenericTypeParameterNullabilityForCall(
1908+
genericsChecks.compareGenericTypeParameterNullabilityForCall(
18951909
methodSymbol, tree, actualParams, varArgsMethod, this, state);
18961910
if (!methodSymbol.getTypeParameters().isEmpty()) {
18971911
GenericsChecks.checkGenericMethodCallTypeArguments(tree, state, this, config, handler);
@@ -2637,8 +2651,8 @@ private boolean mayBeNullMethodCall(
26372651
return true;
26382652
}
26392653
if (config.isJSpecifyMode()
2640-
&& GenericsChecks.getGenericReturnNullnessAtInvocation(
2641-
exprSymbol, invocationTree, state, config)
2654+
&& genericsChecks
2655+
.getGenericReturnNullnessAtInvocation(exprSymbol, invocationTree, state, config)
26422656
.equals(Nullness.NULLABLE)) {
26432657
return true;
26442658
}
@@ -2657,7 +2671,7 @@ public boolean nullnessFromDataflow(VisitorState state, ExpressionTree expr) {
26572671
}
26582672

26592673
public AccessPathNullnessAnalysis getNullnessAnalysis(VisitorState state) {
2660-
return AccessPathNullnessAnalysis.instance(state, nonAnnotatedMethod, config, this.handler);
2674+
return AccessPathNullnessAnalysis.instance(state, nonAnnotatedMethod, this);
26612675
}
26622676

26632677
private Description matchDereference(

nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessAnalysis.java

+9-14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.sun.source.util.TreePath;
2929
import com.sun.tools.javac.util.Context;
3030
import com.uber.nullaway.Config;
31+
import com.uber.nullaway.NullAway;
3132
import com.uber.nullaway.Nullness;
3233
import com.uber.nullaway.handlers.Handler;
3334
import com.uber.nullaway.handlers.contract.ContractNullnessStoreInitializer;
@@ -65,10 +66,9 @@ public final class AccessPathNullnessAnalysis {
6566

6667
// Use #instance to instantiate
6768
private AccessPathNullnessAnalysis(
68-
Predicate<MethodInvocationNode> methodReturnsNonNull,
69-
VisitorState state,
70-
Config config,
71-
Handler handler) {
69+
Predicate<MethodInvocationNode> methodReturnsNonNull, VisitorState state, NullAway analysis) {
70+
Config config = analysis.getConfig();
71+
Handler handler = analysis.getHandler();
7272
apContext =
7373
AccessPath.AccessPathContext.builder()
7474
.setImmutableTypes(handler.onRegisterImmutableTypes())
@@ -79,8 +79,7 @@ private AccessPathNullnessAnalysis(
7979
methodReturnsNonNull,
8080
state,
8181
apContext,
82-
config,
83-
handler,
82+
analysis,
8483
new CoreNullnessStoreInitializer());
8584
this.dataFlow = new DataFlow(config.assertsEnabled(), handler);
8685

@@ -91,8 +90,7 @@ private AccessPathNullnessAnalysis(
9190
methodReturnsNonNull,
9291
state,
9392
apContext,
94-
config,
95-
handler,
93+
analysis,
9694
new ContractNullnessStoreInitializer());
9795
}
9896
}
@@ -103,18 +101,15 @@ private AccessPathNullnessAnalysis(
103101
* @param state visitor state for the compilation
104102
* @param methodReturnsNonNull predicate determining whether a method is assumed to return NonNull
105103
* value
106-
* @param config analysis config
104+
* @param analysis instance of NullAway analysis
107105
* @return instance of the analysis
108106
*/
109107
public static AccessPathNullnessAnalysis instance(
110-
VisitorState state,
111-
Predicate<MethodInvocationNode> methodReturnsNonNull,
112-
Config config,
113-
Handler handler) {
108+
VisitorState state, Predicate<MethodInvocationNode> methodReturnsNonNull, NullAway analysis) {
114109
Context context = state.context;
115110
AccessPathNullnessAnalysis instance = context.get(FIELD_NULLNESS_ANALYSIS_KEY);
116111
if (instance == null) {
117-
instance = new AccessPathNullnessAnalysis(methodReturnsNonNull, state, config, handler);
112+
instance = new AccessPathNullnessAnalysis(methodReturnsNonNull, state, analysis);
118113
context.put(FIELD_NULLNESS_ANALYSIS_KEY, instance);
119114
}
120115
return instance;

nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.sun.tools.javac.code.TypeTag;
3838
import com.uber.nullaway.CodeAnnotationInfo;
3939
import com.uber.nullaway.Config;
40+
import com.uber.nullaway.NullAway;
4041
import com.uber.nullaway.NullabilityUtil;
4142
import com.uber.nullaway.Nullness;
4243
import com.uber.nullaway.generics.GenericsChecks;
@@ -160,22 +161,24 @@ public class AccessPathNullnessPropagation
160161

161162
private final Handler handler;
162163

164+
private final GenericsChecks genericsChecks;
165+
163166
private final NullnessStoreInitializer nullnessStoreInitializer;
164167

165168
public AccessPathNullnessPropagation(
166169
Nullness defaultAssumption,
167170
Predicate<MethodInvocationNode> methodReturnsNonNull,
168171
VisitorState state,
169172
AccessPath.AccessPathContext apContext,
170-
Config config,
171-
Handler handler,
173+
NullAway analysis,
172174
NullnessStoreInitializer nullnessStoreInitializer) {
173175
this.defaultAssumption = defaultAssumption;
174176
this.methodReturnsNonNull = methodReturnsNonNull;
175177
this.state = state;
176178
this.apContext = apContext;
177-
this.config = config;
178-
this.handler = handler;
179+
this.config = analysis.getConfig();
180+
this.handler = analysis.getHandler();
181+
this.genericsChecks = analysis.getGenericsChecks();
179182
this.nullnessStoreInitializer = nullnessStoreInitializer;
180183
}
181184

@@ -1086,7 +1089,7 @@ private boolean genericReturnIsNullable(MethodInvocationNode node) {
10861089
MethodInvocationTree tree = node.getTree();
10871090
if (tree != null) {
10881091
Nullness nullness =
1089-
GenericsChecks.getGenericReturnNullnessAtInvocation(
1092+
genericsChecks.getGenericReturnNullnessAtInvocation(
10901093
ASTHelpers.getSymbol(tree), tree, state, config);
10911094
return nullness.equals(NULLABLE);
10921095
}

nullaway/src/main/java/com/uber/nullaway/generics/GenericsChecks.java

+83-10
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.sun.tools.javac.code.TargetType;
2727
import com.sun.tools.javac.code.Type;
2828
import com.sun.tools.javac.tree.JCTree;
29+
import com.sun.tools.javac.util.ListBuffer;
2930
import com.uber.nullaway.CodeAnnotationInfo;
3031
import com.uber.nullaway.Config;
3132
import com.uber.nullaway.ErrorBuilder;
@@ -35,6 +36,7 @@
3536
import com.uber.nullaway.handlers.Handler;
3637
import java.util.ArrayList;
3738
import java.util.HashMap;
39+
import java.util.LinkedHashMap;
3840
import java.util.List;
3941
import java.util.Map;
4042
import java.util.Objects;
@@ -46,8 +48,14 @@
4648
/** Methods for performing checks related to generic types and nullability. */
4749
public final class GenericsChecks {
4850

49-
/** Do not instantiate; all methods should be static */
50-
private GenericsChecks() {}
51+
/**
52+
* Maps a MethodInvocationTree representing a call to a generic method to a substitution for its
53+
* type arguments. The call must not have any explicit type arguments. The substitution is a map
54+
* from type variables for the method to their inferred type arguments (most importantly with
55+
* inferred nullability information).
56+
*/
57+
private final Map<MethodInvocationTree, Map<TypeVariable, Type>>
58+
inferredSubstitutionsForGenericMethodCalls = new LinkedHashMap<>();
5159

5260
/**
5361
* Checks that for an instantiated generic type, {@code @Nullable} types are only used for type
@@ -413,13 +421,16 @@ private static void reportInvalidOverridingMethodParamTypeError(
413421
* @param analysis the analysis object
414422
* @param state the visitor state
415423
*/
416-
public static void checkTypeParameterNullnessForAssignability(
424+
public void checkTypeParameterNullnessForAssignability(
417425
Tree tree, NullAway analysis, VisitorState state) {
418426
Config config = analysis.getConfig();
419427
if (!config.isJSpecifyMode()) {
420428
return;
421429
}
422430
Type lhsType = getTreeType(tree, config);
431+
if (lhsType == null) {
432+
return;
433+
}
423434
Tree rhsTree;
424435
if (tree instanceof VariableTree) {
425436
VariableTree varTree = (VariableTree) tree;
@@ -435,14 +446,58 @@ public static void checkTypeParameterNullnessForAssignability(
435446
}
436447
Type rhsType = getTreeType(rhsTree, config);
437448

438-
if (lhsType != null && rhsType != null) {
449+
if (rhsTree instanceof MethodInvocationTree) {
450+
MethodInvocationTree methodInvocationTree = (MethodInvocationTree) rhsTree;
451+
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocationTree);
452+
if (methodSymbol.type instanceof Type.ForAll
453+
&& methodInvocationTree.getTypeArguments().isEmpty()) {
454+
// generic method call with no explicit generic arguments
455+
// update inferred type arguments based on the assignment context
456+
InferSubstitutionViaAssignmentContextVisitor inferVisitor =
457+
new InferSubstitutionViaAssignmentContextVisitor(config);
458+
Type returnType = methodSymbol.getReturnType();
459+
returnType.accept(inferVisitor, lhsType);
460+
461+
Map<TypeVariable, Type> substitution = inferVisitor.getInferredSubstitution();
462+
inferredSubstitutionsForGenericMethodCalls.put(methodInvocationTree, substitution);
463+
if (rhsType != null) {
464+
// update rhsType with inferred substitution
465+
rhsType =
466+
substituteInferredTypesForTypeVariables(
467+
state, methodSymbol.getReturnType(), substitution, config);
468+
}
469+
}
470+
}
471+
472+
if (rhsType != null) {
439473
boolean isAssignmentValid = subtypeParameterNullability(lhsType, rhsType, state, config);
440474
if (!isAssignmentValid) {
441475
reportInvalidAssignmentInstantiationError(tree, lhsType, rhsType, state, analysis);
442476
}
443477
}
444478
}
445479

480+
/**
481+
* Substitutes inferred types for type variables within a type.
482+
*
483+
* @param state The visitor state
484+
* @param targetType The type with type variables on which substitutions will be applied
485+
* @param substitution The cache that maps type variables to its inferred types
486+
* @param config Configuration for the analysis
487+
* @return {@code targetType} with the substitutions applied
488+
*/
489+
private Type substituteInferredTypesForTypeVariables(
490+
VisitorState state, Type targetType, Map<TypeVariable, Type> substitution, Config config) {
491+
ListBuffer<Type> typeVars = new ListBuffer<>();
492+
ListBuffer<Type> inferredTypes = new ListBuffer<>();
493+
for (Map.Entry<TypeVariable, Type> entry : substitution.entrySet()) {
494+
typeVars.append((Type) entry.getKey());
495+
inferredTypes.append(entry.getValue());
496+
}
497+
return TypeSubstitutionUtils.subst(
498+
state.getTypes(), targetType, typeVars.toList(), inferredTypes.toList(), config);
499+
}
500+
446501
/**
447502
* Checks that the nullability of type parameters for a returned expression matches that of the
448503
* type parameters of the enclosing method's return type.
@@ -613,7 +668,7 @@ public static void checkTypeParameterNullnessForConditionalExpression(
613668
* @param analysis the analysis object
614669
* @param state the visitor state
615670
*/
616-
public static void compareGenericTypeParameterNullabilityForCall(
671+
public void compareGenericTypeParameterNullabilityForCall(
617672
Symbol.MethodSymbol methodSymbol,
618673
Tree tree,
619674
List<? extends ExpressionTree> actualParams,
@@ -640,7 +695,7 @@ public static void compareGenericTypeParameterNullabilityForCall(
640695
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config);
641696
}
642697
}
643-
// substitute type arguments for generic methods
698+
// substitute type arguments for generic methods with explicit type arguments
644699
if (tree instanceof MethodInvocationTree && methodSymbol.type instanceof Type.ForAll) {
645700
invokedMethodType =
646701
substituteTypeArgsInGenericMethodType(
@@ -830,7 +885,7 @@ public static Nullness getGenericMethodReturnTypeNullness(
830885
* @return Nullness of invocation's return type, or {@code NONNULL} if the call does not invoke an
831886
* instance method
832887
*/
833-
public static Nullness getGenericReturnNullnessAtInvocation(
888+
public Nullness getGenericReturnNullnessAtInvocation(
834889
Symbol.MethodSymbol invokedMethodSymbol,
835890
MethodInvocationTree tree,
836891
VisitorState state,
@@ -883,7 +938,7 @@ private static com.sun.tools.javac.util.List<Type> convertTreesToTypes(
883938
* @param config the NullAway config
884939
* @return the substituted method type for the generic method
885940
*/
886-
private static Type substituteTypeArgsInGenericMethodType(
941+
private Type substituteTypeArgsInGenericMethodType(
887942
MethodInvocationTree methodInvocationTree,
888943
Symbol.MethodSymbol methodSymbol,
889944
VisitorState state,
@@ -894,6 +949,17 @@ private static Type substituteTypeArgsInGenericMethodType(
894949

895950
Type.ForAll forAllType = (Type.ForAll) methodSymbol.type;
896951
Type.MethodType underlyingMethodType = (Type.MethodType) forAllType.qtype;
952+
953+
// There are no explicit type arguments, so use the inferred types
954+
if (explicitTypeArgs.isEmpty()) {
955+
if (inferredSubstitutionsForGenericMethodCalls.containsKey(methodInvocationTree)) {
956+
return substituteInferredTypesForTypeVariables(
957+
state,
958+
underlyingMethodType,
959+
inferredSubstitutionsForGenericMethodCalls.get(methodInvocationTree),
960+
config);
961+
}
962+
}
897963
return TypeSubstitutionUtils.subst(
898964
state.getTypes(), underlyingMethodType, forAllType.tvars, explicitTypeArgs, config);
899965
}
@@ -932,7 +998,7 @@ private static Type substituteTypeArgsInGenericMethodType(
932998
* @return Nullness of parameter at {@code paramIndex}, or {@code NONNULL} if the call does not
933999
* invoke an instance method
9341000
*/
935-
public static Nullness getGenericParameterNullnessAtInvocation(
1001+
public Nullness getGenericParameterNullnessAtInvocation(
9361002
int paramIndex,
9371003
Symbol.MethodSymbol invokedMethodSymbol,
9381004
MethodInvocationTree tree,
@@ -941,7 +1007,6 @@ public static Nullness getGenericParameterNullnessAtInvocation(
9411007
// If generic method invocation
9421008
if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
9431009
// Substitute the argument types within the MethodType
944-
// NOTE: if explicitTypeArgs is empty, this is a noop
9451010
List<Type> substitutedParamTypes =
9461011
substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state, config)
9471012
.getParameterTypes();
@@ -1160,6 +1225,14 @@ public static boolean passingLambdaOrMethodRefWithGenericReturnToUnmarkedCode(
11601225
return callingUnannotated;
11611226
}
11621227

1228+
/**
1229+
* Clears the cache of inferred substitutions for generic method calls. This should be invoked
1230+
* after each CompilationUnit to avoid memory leaks.
1231+
*/
1232+
public void clearCache() {
1233+
inferredSubstitutionsForGenericMethodCalls.clear();
1234+
}
1235+
11631236
public static boolean isNullableAnnotated(Type type, Config config) {
11641237
return Nullness.hasNullableAnnotation(type.getAnnotationMirrors().stream(), config);
11651238
}

0 commit comments

Comments
 (0)