Skip to content

Commit d7e9b24

Browse files
authored
Better handle calls to super constructors and superclass methods in JSpecify mode (#1248)
Fixes #1246 We were computing the enclosing type for such calls incorrectly before; now we use the enclosing class of the call itself. We extract the logic for computing enclosing types to its own function and use it consistently everywhere. This change exposed a subtle bug with type substitutions; relevant test case is `nullableWildcardFromCaffeine`. We fix that bug here as well, though there is still another inference bug to be fixed. Opened #1267 and will fix in a follow-up. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - More precise determination of enclosing types for generic method and constructor calls, improving nullability analysis for unqualified calls. - Bug Fixes - Reduced false positives/negatives in generic parameter and return nullability checks at invocation sites. - Tests - Replaced an ignored superclass-constructor test with two targeted super-constructor nullability tests. - Added a new test exercising complex wildcard/substitution scenarios inspired by Caffeine. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent cf02218 commit d7e9b24

3 files changed

Lines changed: 140 additions & 57 deletions

File tree

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

Lines changed: 59 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.sun.source.tree.AnnotatedTypeTree;
1111
import com.sun.source.tree.AnnotationTree;
1212
import com.sun.source.tree.AssignmentTree;
13+
import com.sun.source.tree.ClassTree;
1314
import com.sun.source.tree.ConditionalExpressionTree;
1415
import com.sun.source.tree.ExpressionTree;
1516
import com.sun.source.tree.IdentifierTree;
@@ -366,6 +367,7 @@ private void reportInvalidOverridingMethodParamTypeError(
366367
* @return Type of the tree with preserved annotations.
367368
*/
368369
private @Nullable Type getTreeType(Tree tree) {
370+
tree = ASTHelpers.stripParentheses(tree);
369371
if (tree instanceof NewClassTree
370372
&& ((NewClassTree) tree).getIdentifier() instanceof ParameterizedTypeTree) {
371373
ParameterizedTypeTree paramTypedTree =
@@ -842,36 +844,18 @@ public void compareGenericTypeParameterNullabilityForCall(
842844
return;
843845
}
844846
Type invokedMethodType = methodSymbol.type;
845-
// substitute class-level type arguments for instance methods
846-
if (!methodSymbol.isStatic() && tree instanceof MethodInvocationTree) {
847-
ExpressionTree methodSelect = ((MethodInvocationTree) tree).getMethodSelect();
848-
Type enclosingType;
849-
if (methodSelect instanceof MemberSelectTree) {
850-
enclosingType = getTreeType(((MemberSelectTree) methodSelect).getExpression());
851-
} else {
852-
// implicit this parameter
853-
enclosingType = methodSymbol.owner.type;
854-
}
855-
if (enclosingType != null) {
856-
invokedMethodType =
857-
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config);
858-
}
859-
}
860-
861-
// substitute type arguments for constructor call
862-
if (tree instanceof NewClassTree) {
863-
// get the type arguments from the NewClassTree itself
864-
Type enclosingType = getTreeType(tree);
865-
if (enclosingType != null) {
866-
invokedMethodType =
867-
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config);
868-
}
847+
Type enclosingType = getEnclosingTypeForCallExpression(methodSymbol, tree, state);
848+
if (enclosingType != null) {
849+
invokedMethodType =
850+
TypeSubstitutionUtils.memberType(state.getTypes(), enclosingType, methodSymbol, config);
869851
}
870852

871853
// substitute type arguments for generic methods with explicit type arguments
872-
if (tree instanceof MethodInvocationTree && methodSymbol.type instanceof Type.ForAll) {
873-
invokedMethodType = substituteTypeArgsInGenericMethodType(tree, methodSymbol, state);
854+
if (tree instanceof MethodInvocationTree && invokedMethodType instanceof Type.ForAll) {
855+
invokedMethodType =
856+
substituteTypeArgsInGenericMethodType(tree, (Type.ForAll) invokedMethodType, state);
874857
}
858+
875859
List<Type> formalParamTypes = invokedMethodType.getParameterTypes();
876860
int n = formalParamTypes.size();
877861
if (isVarArgs) {
@@ -1071,8 +1055,9 @@ public Nullness getGenericReturnNullnessAtInvocation(
10711055
// If generic method invocation
10721056
if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
10731057
// Substitute type arguments inside the return type
1058+
Type.ForAll forAllType = (Type.ForAll) invokedMethodSymbol.type;
10741059
Type substitutedReturnType =
1075-
substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state).getReturnType();
1060+
substituteTypeArgsInGenericMethodType(tree, forAllType, state).getReturnType();
10761061
// If this condition evaluates to false, we fall through to the subsequent logic, to handle
10771062
// type variables declared on the enclosing class
10781063
if (substitutedReturnType != null
@@ -1081,15 +1066,11 @@ public Nullness getGenericReturnNullnessAtInvocation(
10811066
}
10821067
}
10831068

1084-
if (!(tree.getMethodSelect() instanceof MemberSelectTree) || invokedMethodSymbol.isStatic()) {
1085-
return Nullness.NONNULL;
1086-
}
1087-
Type methodReceiverType =
1088-
getTreeType(((MemberSelectTree) tree.getMethodSelect()).getExpression());
1089-
if (methodReceiverType == null) {
1069+
Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
1070+
if (enclosingType == null) {
10901071
return Nullness.NONNULL;
10911072
} else {
1092-
return getGenericMethodReturnTypeNullness(invokedMethodSymbol, methodReceiverType, state);
1073+
return getGenericMethodReturnTypeNullness(invokedMethodSymbol, enclosingType, state);
10931074
}
10941075
}
10951076

@@ -1108,33 +1089,31 @@ private static com.sun.tools.javac.util.List<Type> convertTreesToTypes(
11081089
/**
11091090
* Substitutes the type arguments from a generic method invocation into the method's type.
11101091
*
1111-
* @param tree the method invocation or new class tree
1112-
* @param methodSymbol symbol for the invoked generic method
1092+
* @param tree the method invocation tree
1093+
* @param forAllType the generic method type
11131094
* @param state the visitor state
11141095
* @return the substituted method type for the generic method
11151096
*/
11161097
private Type substituteTypeArgsInGenericMethodType(
1117-
Tree tree, Symbol.MethodSymbol methodSymbol, VisitorState state) {
1098+
Tree tree, Type.ForAll forAllType, VisitorState state) {
1099+
Type.MethodType methodType = forAllType.asMethodType();
11181100

11191101
List<? extends Tree> typeArgumentTrees =
11201102
(tree instanceof MethodInvocationTree)
11211103
? ((MethodInvocationTree) tree).getTypeArguments()
11221104
: ((NewClassTree) tree).getTypeArguments();
11231105
com.sun.tools.javac.util.List<Type> explicitTypeArgs = convertTreesToTypes(typeArgumentTrees);
11241106

1125-
Type.ForAll forAllType = (Type.ForAll) methodSymbol.type;
1126-
Type.MethodType underlyingMethodType = (Type.MethodType) forAllType.qtype;
1127-
11281107
// There are no explicit type arguments, so use the inferred types
11291108
if (explicitTypeArgs.isEmpty()) {
11301109
if (inferredTypeVarNullabilityForGenericCalls.containsKey(tree)
11311110
&& tree instanceof MethodInvocationTree) {
11321111
return getTypeWithInferredNullability(
1133-
state, underlyingMethodType, inferredTypeVarNullabilityForGenericCalls.get(tree));
1112+
state, methodType, inferredTypeVarNullabilityForGenericCalls.get(tree));
11341113
}
11351114
}
11361115
return TypeSubstitutionUtils.subst(
1137-
state.getTypes(), underlyingMethodType, forAllType.tvars, explicitTypeArgs, config);
1116+
state.getTypes(), methodType, forAllType.tvars, explicitTypeArgs, config);
11381117
}
11391118

11401119
/**
@@ -1178,8 +1157,9 @@ public Nullness getGenericParameterNullnessAtInvocation(
11781157
// If generic method invocation
11791158
if (!invokedMethodSymbol.getTypeParameters().isEmpty()) {
11801159
// Substitute the argument types within the MethodType
1181-
Type substituted = substituteTypeArgsInGenericMethodType(tree, invokedMethodSymbol, state);
1182-
List<Type> substitutedParamTypes = substituted.getParameterTypes();
1160+
Type.ForAll forAllType = (Type.ForAll) invokedMethodSymbol.type;
1161+
List<Type> substitutedParamTypes =
1162+
substituteTypeArgsInGenericMethodType(tree, forAllType, state).getParameterTypes();
11831163
// If this condition evaluates to false, we fall through to the subsequent logic, to handle
11841164
// type variables declared on the enclosing class
11851165
if (substitutedParamTypes != null
@@ -1190,27 +1170,51 @@ public Nullness getGenericParameterNullnessAtInvocation(
11901170
}
11911171
}
11921172

1193-
if (tree instanceof MethodInvocationTree) {
1194-
if (!(((MethodInvocationTree) tree).getMethodSelect() instanceof MemberSelectTree)
1195-
|| invokedMethodSymbol.isStatic()) {
1196-
return Nullness.NONNULL;
1197-
}
1173+
Type enclosingType = getEnclosingTypeForCallExpression(invokedMethodSymbol, tree, state);
1174+
if (enclosingType == null) {
1175+
return Nullness.NONNULL;
11981176
}
11991177

1178+
return getGenericMethodParameterNullness(paramIndex, invokedMethodSymbol, enclosingType, state);
1179+
}
1180+
1181+
/**
1182+
* Gets the enclosing type for a non-static method call expression, which is either the type of
1183+
* the enclosing class (for implicit this calls) or the type of the receiver expression. For a
1184+
* constructor call, we treat the type being allocated as the enclosing type.
1185+
*
1186+
* @param invokedMethodSymbol symbol for the invoked method
1187+
* @param tree the tree for the invocation
1188+
* @param state the visitor state
1189+
* @return the enclosing type for the method call, or null if it cannot be determined
1190+
*/
1191+
private @Nullable Type getEnclosingTypeForCallExpression(
1192+
Symbol.MethodSymbol invokedMethodSymbol, Tree tree, VisitorState state) {
12001193
Type enclosingType = null;
12011194
if (tree instanceof MethodInvocationTree) {
1202-
enclosingType =
1203-
getTreeType(
1204-
((MemberSelectTree) ((MethodInvocationTree) tree).getMethodSelect()).getExpression());
1205-
1195+
if (invokedMethodSymbol.isStatic()) {
1196+
return null;
1197+
}
1198+
ExpressionTree methodSelect =
1199+
ASTHelpers.stripParentheses(((MethodInvocationTree) tree).getMethodSelect());
1200+
if (methodSelect instanceof IdentifierTree) {
1201+
// implicit this parameter, or a super call. in either case, use the type of the enclosing
1202+
// class.
1203+
ClassTree enclosingClassTree =
1204+
ASTHelpers.findEnclosingNode(state.getPath(), ClassTree.class);
1205+
if (enclosingClassTree != null) {
1206+
enclosingType = castToNonNull(ASTHelpers.getType(enclosingClassTree));
1207+
}
1208+
} else if (methodSelect instanceof MemberSelectTree) {
1209+
enclosingType = getTreeType(((MemberSelectTree) methodSelect).getExpression());
1210+
}
12061211
} else {
12071212
Verify.verify(tree instanceof NewClassTree);
12081213
// for a constructor invocation, the type from the invocation itself is the "enclosing type"
12091214
// for the purposes of determining type arguments
12101215
enclosingType = getTreeType(tree);
12111216
}
1212-
1213-
return getGenericMethodParameterNullness(paramIndex, invokedMethodSymbol, enclosingType, state);
1217+
return enclosingType;
12141218
}
12151219

12161220
/**

nullaway/src/test/java/com/uber/nullaway/jspecify/GenericMethodTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,43 @@ public void issue1238() {
10831083
.doTest();
10841084
}
10851085

1086+
/**
1087+
* Extracted from Caffeine; exposed some subtle bugs in substitutions involving identity of {@code
1088+
* Type} objects
1089+
*/
1090+
@Test
1091+
public void nullableWildcardFromCaffeine() {
1092+
makeHelperWithInferenceFailureWarning()
1093+
.addSourceLines(
1094+
"Test.java",
1095+
"import org.jspecify.annotations.NullMarked;",
1096+
"import org.jspecify.annotations.Nullable;",
1097+
"@NullMarked",
1098+
"public class Test {",
1099+
" public interface CacheLoader<K, V extends @Nullable Object> {}",
1100+
" static class JCacheLoaderAdapter<K, V> implements CacheLoader<K, @Nullable Expirable<V>> {}",
1101+
" static class Expirable<V> {}",
1102+
" static class Caffeine<K, V> {",
1103+
" public <K1 extends K, V1 extends @Nullable V> Object build(",
1104+
" CacheLoader<? super K1, V1> loader) {",
1105+
" throw new RuntimeException();",
1106+
" }",
1107+
" }",
1108+
" class Builder<K, V> {",
1109+
" Caffeine<Object, Object> caffeine = new Caffeine<>();",
1110+
" void test() {",
1111+
" JCacheLoaderAdapter<K, V> adapter = new JCacheLoaderAdapter<>();",
1112+
" caffeine.<K, @Nullable Expirable<V>>build(adapter);",
1113+
" // TODO inference is buggy for this case; doesn't substitute inferred types properly",
1114+
" // See https://github.com/uber/NullAway/issues/1267",
1115+
" @SuppressWarnings(\"NullAway\")",
1116+
" Object o = caffeine.build(adapter);",
1117+
" }",
1118+
" }",
1119+
"}")
1120+
.doTest();
1121+
}
1122+
10861123
private CompilationTestHelper makeHelper() {
10871124
return makeTestHelperWithArgs(
10881125
Arrays.asList(

nullaway/src/test/java/com/uber/nullaway/jspecify/GenericsTests.java

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2342,7 +2342,6 @@ public void callWithConstructorReceiver() {
23422342
.doTest();
23432343
}
23442344

2345-
@Ignore("https://github.com/uber/NullAway/issues/1246")
23462345
@Test
23472346
public void nullableSuperConstructorArg() {
23482347
makeHelper()
@@ -2364,7 +2363,50 @@ public void nullableSuperConstructorArg() {
23642363
.doTest();
23652364
}
23662365

2367-
@Ignore("https://github.com/uber/NullAway/issues/1246")
2366+
@Test
2367+
public void passNonNullToNullableSuperConstructorArg() {
2368+
makeHelper()
2369+
.addSourceLines(
2370+
"Test.java",
2371+
"import org.jspecify.annotations.NullMarked;",
2372+
"import org.jspecify.annotations.Nullable;",
2373+
"@NullMarked",
2374+
"public class Test {",
2375+
" private static class A<T extends @Nullable Object> {",
2376+
" A(T t) {}",
2377+
" }",
2378+
" private static class B extends A<@Nullable Object> {",
2379+
" B(Object o) {",
2380+
" super(o);",
2381+
" }",
2382+
" }",
2383+
"}")
2384+
.doTest();
2385+
}
2386+
2387+
@Test
2388+
public void mismatchedTypeArgNullabilityForSuperConstructor() {
2389+
makeHelper()
2390+
.addSourceLines(
2391+
"Test.java",
2392+
"import org.jspecify.annotations.NullMarked;",
2393+
"import org.jspecify.annotations.Nullable;",
2394+
"import java.util.List;",
2395+
"@NullMarked",
2396+
"public class Test {",
2397+
" private static class A<T extends @Nullable Object> {",
2398+
" A(List<T> l) {}",
2399+
" }",
2400+
" private static class B extends A<@Nullable Object> {",
2401+
" B(List<Object> l) {",
2402+
" // BUG: Diagnostic contains: Cannot pass parameter",
2403+
" super(l);",
2404+
" }",
2405+
" }",
2406+
"}")
2407+
.doTest();
2408+
}
2409+
23682410
@Test
23692411
public void nullableSuperMethodArg() {
23702412
makeHelper()

0 commit comments

Comments
 (0)