Skip to content

Commit feec9d9

Browse files
authored
Support marking method type variable upper bounds as @Nullable in library models (#1345)
We need this support to address #1157 (which I'll do in a follow-up PR). Now `LibraryModels` can mark the upper bound of some method type variable as `@Nullable`. Rename method in `LibraryModels` for getting upper bounds of class type variables for clarity. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Enhanced null-checking capabilities to support nullable upper bounds in generic type variables for library methods. * **Tests** * Added test coverage for validating nullable upper bounds in generic method type parameters. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent fdec09a commit feec9d9

File tree

11 files changed

+151
-10
lines changed

11 files changed

+151
-10
lines changed

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,26 @@ public interface LibraryModels {
115115

116116
/**
117117
* Get the (className, type argument index) pairs for library classes where the generic type
118-
* argument has a {@code @Nullable} upper bound. Only used in JSpecify mode.
118+
* variable has a {@code @Nullable} upper bound. Only used in JSpecify mode.
119119
*
120-
* @return map from the className to the positions of the generic type arguments that have a
120+
* @return map from the className to the positions of the generic type variables that have a
121121
* {@code Nullable} upper bound.
122122
*/
123123
default ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBounds() {
124124
return ImmutableSetMultimap.of();
125125
}
126126

127+
/**
128+
* Get the (method, type argument index) pairs for library methods where the generic type variable
129+
* has a {@code @Nullable} upper bound. Only used in JSpecify mode
130+
*
131+
* @return map from the method to the positions of the generic type variables that have a {@code
132+
* Nullable} upper bound.
133+
*/
134+
default ImmutableSetMultimap<MethodRef, Integer> methodTypeVariablesWithNullableUpperBounds() {
135+
return ImmutableSetMultimap.of();
136+
}
137+
127138
/**
128139
* Get the set of library classes that are NullMarked. Only used in JSpecify mode.
129140
*

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,29 @@ private boolean upperBoundIsNullable(Element typeVarElement) {
299299
if (fromUnannotatedMethod(typeVarElement)) {
300300
return true;
301301
}
302+
// first, check if library model overrides the upper bound nullability
303+
Element enclosingElement = typeVarElement.getEnclosingElement();
304+
if (enclosingElement instanceof Symbol.MethodSymbol) {
305+
Symbol.MethodSymbol methodSymbol = (Symbol.MethodSymbol) enclosingElement;
306+
int typeVarIndex =
307+
methodSymbol.getTypeParameters().indexOf((Symbol.TypeVariableSymbol) typeVarElement);
308+
// TODO typeVarIndex is -1 in some cases; see test
309+
// com.uber.nullaway.jspecify.GenericMethodTests.instanceGenericMethodWithMethodRefArgument.
310+
// Investigate further.
311+
if (typeVarIndex >= 0
312+
&& handler.onOverrideMethodTypeVariableUpperBound(methodSymbol, typeVarIndex)) {
313+
return true;
314+
}
315+
} else if (enclosingElement instanceof Symbol.ClassSymbol) {
316+
Symbol.ClassSymbol classSymbol = (Symbol.ClassSymbol) enclosingElement;
317+
int typeVarIndex =
318+
classSymbol.getTypeParameters().indexOf((Symbol.TypeVariableSymbol) typeVarElement);
319+
if (typeVarIndex >= 0
320+
&& handler.onOverrideClassTypeVariableUpperBound(classSymbol.toString(), typeVarIndex)) {
321+
return true;
322+
}
323+
}
324+
// otherwise, check the actual upper bound annotations
302325
Type upperBound = (Type) ((TypeVariable) typeVarElement.asType()).getUpperBound();
303326
com.sun.tools.javac.util.List<Attribute.TypeCompound> annotationMirrors =
304327
upperBound.getAnnotationMirrors();

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ private boolean[] getTypeParamsWithNullableUpperBound(Type type) {
179179
com.sun.tools.javac.util.List<Attribute.TypeCompound> annotationMirrors =
180180
upperBound.getAnnotationMirrors();
181181
if (Nullness.hasNullableAnnotation(annotationMirrors.stream(), config)
182-
|| handler.onOverrideTypeParameterUpperBound(type.tsym.toString(), i)) {
182+
|| handler.onOverrideClassTypeVariableUpperBound(type.tsym.toString(), i)) {
183183
result[i] = true;
184184
}
185185
}
@@ -254,10 +254,11 @@ public void checkGenericMethodCallTypeArguments(Tree tree, VisitorState state) {
254254
upperBound.getAnnotationMirrors();
255255
boolean hasNullableAnnotation =
256256
Nullness.hasNullableAnnotation(annotationMirrors.stream(), config)
257-
|| handler.onOverrideTypeParameterUpperBound(baseType.tsym.toString(), i);
257+
|| handler.onOverrideClassTypeVariableUpperBound(baseType.tsym.toString(), i);
258258
// if type variable's upper bound does not have @Nullable annotation then the instantiation
259259
// is invalid
260-
if (!hasNullableAnnotation) {
260+
if (!hasNullableAnnotation
261+
&& !handler.onOverrideMethodTypeVariableUpperBound(methodSymbol, i)) {
261262
reportInvalidTypeArgumentError(
262263
nullableTypeArguments.get(i), methodSymbol, typeVariable, state);
263264
}

nullaway/src/main/java/com/uber/nullaway/handlers/BaseNoOpHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public void onNonNullFieldAssignment(
208208
}
209209

210210
@Override
211-
public boolean onOverrideTypeParameterUpperBound(String className, int index) {
211+
public boolean onOverrideClassTypeVariableUpperBound(String className, int index) {
212212
return false;
213213
}
214214

nullaway/src/main/java/com/uber/nullaway/handlers/CompositeHandler.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,24 @@ public MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
312312

313313
/** Returns true if any handler returns true. */
314314
@Override
315-
public boolean onOverrideTypeParameterUpperBound(String className, int index) {
315+
public boolean onOverrideClassTypeVariableUpperBound(String className, int index) {
316316
boolean result = false;
317317
for (Handler h : handlers) {
318-
result = h.onOverrideTypeParameterUpperBound(className, index);
318+
result = h.onOverrideClassTypeVariableUpperBound(className, index);
319+
if (result) {
320+
break;
321+
}
322+
}
323+
return result;
324+
}
325+
326+
/** Returns true if any handler returns true. */
327+
@Override
328+
public boolean onOverrideMethodTypeVariableUpperBound(
329+
Symbol.MethodSymbol methodSymbol, int index) {
330+
boolean result = false;
331+
for (Handler h : handlers) {
332+
result = h.onOverrideMethodTypeVariableUpperBound(methodSymbol, index);
319333
if (result) {
320334
break;
321335
}

nullaway/src/main/java/com/uber/nullaway/handlers/Handler.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,20 @@ MethodInvocationNode onCFGBuildPhase1AfterVisitMethodInvocation(
392392
* @return boolean true if the variable should be treated as having a {@code @Nullable} upper
393393
* bound
394394
*/
395-
boolean onOverrideTypeParameterUpperBound(String className, int index);
395+
boolean onOverrideClassTypeVariableUpperBound(String className, int index);
396+
397+
/**
398+
* Method to override the nullability of the upper bound for a generic type variable on a method.
399+
*
400+
* @param methodSymbol The method symbol
401+
* @param index index of the generic type variable (starting at 0)
402+
* @return boolean true if the variable should be treated as having a {@code @Nullable} upper
403+
* bound
404+
*/
405+
default boolean onOverrideMethodTypeVariableUpperBound(
406+
Symbol.MethodSymbol methodSymbol, int index) {
407+
return false;
408+
}
396409

397410
/**
398411
* Method to override the null-markedness of a class.

nullaway/src/main/java/com/uber/nullaway/handlers/LibraryModelsHandler.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,11 +353,21 @@ private void setUnconditionalArgumentNullness(
353353
}
354354

355355
@Override
356-
public boolean onOverrideTypeParameterUpperBound(String className, int index) {
356+
public boolean onOverrideClassTypeVariableUpperBound(String className, int index) {
357357
ImmutableSet<Integer> res = libraryModels.typeVariablesWithNullableUpperBounds().get(className);
358358
return res.contains(index);
359359
}
360360

361+
@Override
362+
public boolean onOverrideMethodTypeVariableUpperBound(
363+
Symbol.MethodSymbol methodSymbol, int index) {
364+
ImmutableSet<Integer> res =
365+
libraryModels
366+
.methodTypeVariablesWithNullableUpperBounds()
367+
.get(MethodRef.fromSymbol(methodSymbol));
368+
return res.contains(index);
369+
}
370+
361371
@Override
362372
public boolean onOverrideNullMarkedClasses(String className) {
363373
return libraryModels.nullMarkedClasses().contains(className);
@@ -1004,6 +1014,9 @@ private static class CombinedLibraryModels implements LibraryModels {
10041014

10051015
private final ImmutableSetMultimap<String, Integer> nullableVariableTypeUpperBounds;
10061016

1017+
private final ImmutableSetMultimap<MethodRef, Integer>
1018+
methodTypeVariablesWithNullableUpperBounds;
1019+
10071020
private final ImmutableSet<String> nullMarkedClasses;
10081021

10091022
private final ImmutableSet<FieldRef> nullableFields;
@@ -1022,6 +1035,8 @@ private static class CombinedLibraryModels implements LibraryModels {
10221035
new ImmutableSetMultimap.Builder<>();
10231036
ImmutableSetMultimap.Builder<String, Integer> nullableVariableTypeUpperBoundsBuilder =
10241037
new ImmutableSetMultimap.Builder<>();
1038+
ImmutableSetMultimap.Builder<MethodRef, Integer>
1039+
methodTypeVariableNullableUpperBoundsBuilder = new ImmutableSetMultimap.Builder<>();
10251040
ImmutableSet.Builder<String> nullMarkedClassesBuilder = new ImmutableSet.Builder<>();
10261041
ImmutableSetMultimap.Builder<MethodRef, Integer> nullImpliesTrueParametersBuilder =
10271042
new ImmutableSetMultimap.Builder<>();
@@ -1097,6 +1112,8 @@ private static class CombinedLibraryModels implements LibraryModels {
10971112
}
10981113
nullableVariableTypeUpperBoundsBuilder.putAll(
10991114
libraryModels.typeVariablesWithNullableUpperBounds());
1115+
methodTypeVariableNullableUpperBoundsBuilder.putAll(
1116+
libraryModels.methodTypeVariablesWithNullableUpperBounds());
11001117
for (String name : libraryModels.nullMarkedClasses()) {
11011118
nullMarkedClassesBuilder.add(name);
11021119
}
@@ -1119,6 +1136,8 @@ private static class CombinedLibraryModels implements LibraryModels {
11191136
customStreamNullabilitySpecs = customStreamNullabilitySpecsBuilder.build();
11201137
nullableFields = nullableFieldsBuilder.build();
11211138
nullableVariableTypeUpperBounds = nullableVariableTypeUpperBoundsBuilder.build();
1139+
methodTypeVariablesWithNullableUpperBounds =
1140+
methodTypeVariableNullableUpperBoundsBuilder.build();
11221141
nullMarkedClasses = nullMarkedClassesBuilder.build();
11231142
}
11241143

@@ -1171,6 +1190,11 @@ public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBound
11711190
return nullableVariableTypeUpperBounds;
11721191
}
11731192

1193+
@Override
1194+
public ImmutableSetMultimap<MethodRef, Integer> methodTypeVariablesWithNullableUpperBounds() {
1195+
return methodTypeVariablesWithNullableUpperBounds;
1196+
}
1197+
11741198
@Override
11751199
public ImmutableSet<String> nullMarkedClasses() {
11761200
return nullMarkedClasses;

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,24 @@ public void issue1294_lambdaArguments() {
14481448
.doTest();
14491449
}
14501450

1451+
@Test
1452+
public void instanceGenericMethodWithMethodRefArgument() {
1453+
makeHelper()
1454+
.addSourceLines(
1455+
"Test.java",
1456+
"import org.jspecify.annotations.NullMarked;",
1457+
"import java.util.List;",
1458+
"import java.util.function.Consumer;",
1459+
"@NullMarked",
1460+
"class Test {",
1461+
" public <E extends Enum<E>> void visitEnum(String descriptor, String value, Consumer<E> consumer) {}",
1462+
" void test(String s1, String s2, List<Object> l) {",
1463+
" visitEnum(s1, s2, l::add);",
1464+
" }",
1465+
"}")
1466+
.doTest();
1467+
}
1468+
14511469
private CompilationTestHelper makeHelper() {
14521470
return makeTestHelperWithArgs(
14531471
JSpecifyJavacConfig.withJSpecifyModeArgs(

test-java-lib/src/main/java/com/uber/lib/unannotated/ProviderNullMarkedViaModel.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22

33
public interface ProviderNullMarkedViaModel<T> {
44
T get();
5+
6+
static <U> ProviderNullMarkedViaModel<U> of(U value) {
7+
return () -> value;
8+
}
59
}

test-library-models/src/main/java/com/uber/nullaway/testlibrarymodels/TestLibraryModels.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,4 +143,10 @@ public ImmutableSetMultimap<String, Integer> typeVariablesWithNullableUpperBound
143143
public ImmutableSet<String> nullMarkedClasses() {
144144
return ImmutableSet.of("com.uber.lib.unannotated.ProviderNullMarkedViaModel");
145145
}
146+
147+
@Override
148+
public ImmutableSetMultimap<MethodRef, Integer> methodTypeVariablesWithNullableUpperBounds() {
149+
return ImmutableSetMultimap.of(
150+
methodRef("com.uber.lib.unannotated.ProviderNullMarkedViaModel", "<U>of(U)"), 0);
151+
}
146152
}

0 commit comments

Comments
 (0)