Skip to content

Commit 20dd152

Browse files
authored
Inference of generic method type arguments based on returns and parameter passing (#1226)
1 parent a6273b0 commit 20dd152

4 files changed

Lines changed: 283 additions & 62 deletions

File tree

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

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -985,7 +985,7 @@ private Description checkReturnExpression(
985985

986986
// Check generic type arguments for returned expression here, since we need to check the type
987987
// arguments regardless of the top-level nullability of the return type
988-
GenericsChecks.checkTypeParameterNullnessForFunctionReturnType(
988+
genericsChecks.checkTypeParameterNullnessForFunctionReturnType(
989989
retExpr, methodSymbol, this, state);
990990

991991
// Now, perform the check for returning @Nullable from @NonNull. First, we check if the return
@@ -1962,7 +1962,7 @@ private Description handleInvocation(
19621962
}
19631963
actual = actualParams.get(argPos);
19641964
VarSymbol formalParamSymbol = formalParams.get(formalParams.size() - 1);
1965-
boolean isVarArgsCall = isVarArgsCall(tree);
1965+
boolean isVarArgsCall = NullabilityUtil.isVarArgsCall(tree);
19661966
if (isVarArgsCall) {
19671967
// This is the case were varargs are being passed individually, as 1 or more actual
19681968
// arguments starting at the position of the var args formal.
@@ -2016,23 +2016,6 @@ private Description handleInvocation(
20162016
return checkCastToNonNullTakesNullable(tree, state, methodSymbol, actualParams);
20172017
}
20182018

2019-
/**
2020-
* Checks if the method invocation is a varargs call, i.e., if individual arguments are being
2021-
* passed in the varargs position. If false, it means that an array is being passed in the varargs
2022-
* position.
2023-
*
2024-
* @param tree the method invocation tree (MethodInvocationTree or NewClassTree)
2025-
* @return true if the method invocation is a varargs call, false otherwise
2026-
*/
2027-
private boolean isVarArgsCall(Tree tree) {
2028-
// javac sets the varargsElement field to a non-null value if the invocation is a varargs call
2029-
Type varargsElement =
2030-
tree instanceof JCTree.JCMethodInvocation
2031-
? ((JCTree.JCMethodInvocation) tree).varargsElement
2032-
: ((JCTree.JCNewClass) tree).varargsElement;
2033-
return varargsElement != null;
2034-
}
2035-
20362019
private Description checkCastToNonNullTakesNullable(
20372020
Tree tree,
20382021
VisitorState state,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,4 +645,21 @@ public static boolean hasJetBrainsNotNullDeclarationAnnotation(Symbol.VarSymbol
645645
.map(a -> a.getAnnotationType().toString())
646646
.anyMatch(annotName -> annotName.equals(JETBRAINS_NOT_NULL));
647647
}
648+
649+
/**
650+
* Checks if the method invocation is a varargs call, i.e., if individual arguments are being
651+
* passed in the varargs position. If false, it means that an array is being passed in the varargs
652+
* position.
653+
*
654+
* @param tree the method invocation tree (MethodInvocationTree or NewClassTree)
655+
* @return true if the method invocation is a varargs call, false otherwise
656+
*/
657+
public static boolean isVarArgsCall(Tree tree) {
658+
// javac sets the varargsElement field to a non-null value if the invocation is a varargs call
659+
Type varargsElement =
660+
tree instanceof JCTree.JCMethodInvocation
661+
? ((JCTree.JCMethodInvocation) tree).varargsElement
662+
: ((JCTree.JCNewClass) tree).varargsElement;
663+
return varargsElement != null;
664+
}
648665
}

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

Lines changed: 124 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.uber.nullaway.ErrorBuilder;
3434
import com.uber.nullaway.ErrorMessage;
3535
import com.uber.nullaway.NullAway;
36+
import com.uber.nullaway.NullabilityUtil;
3637
import com.uber.nullaway.Nullness;
3738
import com.uber.nullaway.handlers.Handler;
3839
import java.util.ArrayList;
@@ -446,42 +447,92 @@ public void checkTypeParameterNullnessForAssignability(
446447
return;
447448
}
448449
Type rhsType = getTreeType(rhsTree, config);
449-
450-
if (rhsTree instanceof MethodInvocationTree) {
451-
MethodInvocationTree methodInvocationTree = (MethodInvocationTree) rhsTree;
452-
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocationTree);
453-
if (methodSymbol.type instanceof Type.ForAll
454-
&& methodInvocationTree.getTypeArguments().isEmpty()) {
455-
// generic method call with no explicit generic arguments
456-
// update inferred type arguments based on the assignment context
457-
boolean invokedMethodIsNullUnmarked =
458-
CodeAnnotationInfo.instance(state.context)
459-
.isSymbolUnannotated(methodSymbol, config, analysis.getHandler());
460-
InferGenericMethodSubstitutionViaAssignmentContextVisitor inferVisitor =
461-
new InferGenericMethodSubstitutionViaAssignmentContextVisitor(
462-
state, config, invokedMethodIsNullUnmarked);
463-
Type returnType = methodSymbol.getReturnType();
464-
returnType.accept(inferVisitor, lhsType);
465-
466-
Map<TypeVariable, Type> substitution = inferVisitor.getInferredSubstitution();
467-
inferredSubstitutionsForGenericMethodCalls.put(methodInvocationTree, substitution);
468-
if (rhsType != null) {
469-
// update rhsType with inferred substitution
470-
rhsType =
471-
substituteInferredTypesForTypeVariables(
472-
state, methodSymbol.getReturnType(), substitution, config);
473-
}
474-
}
475-
}
476-
477450
if (rhsType != null) {
451+
if (rhsTree instanceof MethodInvocationTree) {
452+
rhsType =
453+
inferGenericMethodCallType(
454+
analysis, state, (MethodInvocationTree) rhsTree, config, lhsType, rhsType);
455+
}
478456
boolean isAssignmentValid = subtypeParameterNullability(lhsType, rhsType, state, config);
479457
if (!isAssignmentValid) {
480458
reportInvalidAssignmentInstantiationError(tree, lhsType, rhsType, state, analysis);
481459
}
482460
}
483461
}
484462

463+
/**
464+
* Infers the type of a generic method call based on the assignment context. Side-effects the
465+
* #inferredSubstitutionsForGenericMethodCalls map with the inferred type.
466+
*
467+
* @param analysis the analysis
468+
* @param state the visitor state
469+
* @param invocationTree the method invocation tree representing the call to a generic method
470+
* @param config the analysis config
471+
* @param typeFromAssignmentContext the type being "assigned to" in the assignment context
472+
* @param exprType the type of the right-hand side of the pseudo-assignment, which may be null
473+
* @return the type of the method call after inference
474+
*/
475+
private Type inferGenericMethodCallType(
476+
NullAway analysis,
477+
VisitorState state,
478+
MethodInvocationTree invocationTree,
479+
Config config,
480+
Type typeFromAssignmentContext,
481+
Type exprType) {
482+
Type result = exprType;
483+
MethodInvocationTree methodInvocationTree = invocationTree;
484+
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvocationTree);
485+
if (methodSymbol.type instanceof Type.ForAll
486+
&& methodInvocationTree.getTypeArguments().isEmpty()) {
487+
// generic method call with no explicit generic arguments
488+
// update inferred type arguments based on the assignment context
489+
boolean invokedMethodIsNullUnmarked =
490+
CodeAnnotationInfo.instance(state.context)
491+
.isSymbolUnannotated(methodSymbol, config, analysis.getHandler());
492+
Map<TypeVariable, Type> substitution;
493+
Type returnType = methodSymbol.getReturnType();
494+
if (returnType instanceof Type.TypeVar) {
495+
// we need different logic if the return type is a type variable
496+
// if the assignment context type is @Nullable, we shouldn't infer anything, since that
497+
// accommodates the type argument being either @Nullable or @NonNull
498+
Type.TypeVar typeVar = (Type.TypeVar) returnType;
499+
substitution = new LinkedHashMap<>();
500+
boolean nonNullAssignmentContextType =
501+
!Nullness.hasNullableAnnotation(
502+
typeFromAssignmentContext.getAnnotationMirrors().stream(), config);
503+
if (nonNullAssignmentContextType) {
504+
// if the assignment context type is @NonNull, we can just use it
505+
substitution.put(typeVar, typeFromAssignmentContext);
506+
} else {
507+
Type upperBound = typeVar.getUpperBound();
508+
boolean typeVarHasNullableUpperBound =
509+
Nullness.hasNullableAnnotation(upperBound.getAnnotationMirrors().stream(), config);
510+
// if the type variable cannot be @Nullable, we can use the lhsType with any @Nullable
511+
// annotation stripped
512+
if (!typeVarHasNullableUpperBound && !invokedMethodIsNullUnmarked) {
513+
// we can use the lhsType with any @Nullable annotation stripped
514+
// TODO we should just strip out the top-level @Nullable annotation;
515+
// stripMetadata() also removes nested @Nullable annotations
516+
substitution.put(typeVar, typeFromAssignmentContext.stripMetadata());
517+
}
518+
}
519+
520+
} else {
521+
InferGenericMethodSubstitutionViaAssignmentContextVisitor inferVisitor =
522+
new InferGenericMethodSubstitutionViaAssignmentContextVisitor(
523+
state, config, invokedMethodIsNullUnmarked);
524+
returnType.accept(inferVisitor, typeFromAssignmentContext);
525+
substitution = inferVisitor.getInferredSubstitution();
526+
}
527+
inferredSubstitutionsForGenericMethodCalls.put(methodInvocationTree, substitution);
528+
// update with inferred substitution
529+
result =
530+
substituteInferredTypesForTypeVariables(
531+
state, methodSymbol.getReturnType(), substitution, config);
532+
}
533+
return result;
534+
}
535+
485536
/**
486537
* Substitutes inferred types for type variables within a type.
487538
*
@@ -512,7 +563,7 @@ private Type substituteInferredTypesForTypeVariables(
512563
* @param analysis the analysis object
513564
* @param state the visitor state
514565
*/
515-
public static void checkTypeParameterNullnessForFunctionReturnType(
566+
public void checkTypeParameterNullnessForFunctionReturnType(
516567
ExpressionTree retExpr,
517568
Symbol.MethodSymbol methodSymbol,
518569
NullAway analysis,
@@ -529,6 +580,16 @@ public static void checkTypeParameterNullnessForFunctionReturnType(
529580
}
530581
Type returnExpressionType = getTreeType(retExpr, config);
531582
if (formalReturnType != null && returnExpressionType != null) {
583+
if (retExpr instanceof MethodInvocationTree) {
584+
returnExpressionType =
585+
inferGenericMethodCallType(
586+
analysis,
587+
state,
588+
(MethodInvocationTree) retExpr,
589+
config,
590+
formalReturnType,
591+
returnExpressionType);
592+
}
532593
boolean isReturnTypeValid =
533594
subtypeParameterNullability(formalReturnType, returnExpressionType, state, config);
534595
if (!isReturnTypeValid) {
@@ -730,28 +791,48 @@ public void compareGenericTypeParameterNullabilityForCall(
730791
// bail out of any checking involving raw types for now
731792
return;
732793
}
733-
Type actualParameter = getTreeType(actualParams.get(i), config);
734-
if (actualParameter != null) {
735-
if (!subtypeParameterNullability(formalParameter, actualParameter, state, config)) {
794+
ExpressionTree currentActualParam = actualParams.get(i);
795+
Type actualParameterType = getTreeType(currentActualParam, config);
796+
if (actualParameterType != null) {
797+
if (currentActualParam instanceof MethodInvocationTree) {
798+
// infer the type of the method call based on the assignment context
799+
// and the formal parameter type
800+
actualParameterType =
801+
inferGenericMethodCallType(
802+
analysis,
803+
state,
804+
(MethodInvocationTree) currentActualParam,
805+
config,
806+
formalParameter,
807+
actualParameterType);
808+
}
809+
if (!subtypeParameterNullability(formalParameter, actualParameterType, state, config)) {
736810
reportInvalidParametersNullabilityError(
737-
formalParameter, actualParameter, actualParams.get(i), state, analysis);
811+
formalParameter, actualParameterType, currentActualParam, state, analysis);
738812
}
739813
}
740814
}
741-
if (isVarArgs && !formalParamTypes.isEmpty()) {
742-
Type.ArrayType varargsArrayType =
743-
(Type.ArrayType) formalParamTypes.get(formalParamTypes.size() - 1);
744-
Type varargsElementType = varargsArrayType.elemtype;
815+
if (isVarArgs && !formalParamTypes.isEmpty() && NullabilityUtil.isVarArgsCall(tree)) {
816+
Type varargsElementType =
817+
((Type.ArrayType) formalParamTypes.get(formalParamTypes.size() - 1)).elemtype;
745818
for (int i = formalParamTypes.size() - 1; i < actualParams.size(); i++) {
746-
Type actualParameterType = getTreeType(actualParams.get(i), config);
747-
// If the actual parameter type is assignable to the varargs array type, then the call site
748-
// is passing the varargs directly in an array, and we should skip our check.
749-
if (actualParameterType != null
750-
&& !state.getTypes().isAssignable(actualParameterType, varargsArrayType)) {
819+
ExpressionTree actualParamExpr = actualParams.get(i);
820+
Type actualParameterType = getTreeType(actualParamExpr, config);
821+
if (actualParameterType != null) {
822+
if (actualParamExpr instanceof MethodInvocationTree) {
823+
actualParameterType =
824+
inferGenericMethodCallType(
825+
analysis,
826+
state,
827+
(MethodInvocationTree) actualParamExpr,
828+
config,
829+
varargsElementType,
830+
actualParameterType);
831+
}
751832
if (!subtypeParameterNullability(
752833
varargsElementType, actualParameterType, state, config)) {
753834
reportInvalidParametersNullabilityError(
754-
varargsElementType, actualParameterType, actualParams.get(i), state, analysis);
835+
varargsElementType, actualParameterType, actualParamExpr, state, analysis);
755836
}
756837
}
757838
}

0 commit comments

Comments
 (0)