Skip to content
Prev Previous commit
Next Next commit
a fix
  • Loading branch information
msridhar committed Jun 28, 2025
commit 99235a43404648204fb2dd3298c297c6255a7e14
Original file line number Diff line number Diff line change
Expand Up @@ -493,13 +493,33 @@ public void checkTypeParameterNullnessForAssignability(
boolean invokedMethodIsNullUnmarked =
CodeAnnotationInfo.instance(state.context)
.isSymbolUnannotated(methodSymbol, config, analysis.getHandler());
InferGenericMethodSubstitutionViaAssignmentContextVisitor inferVisitor =
new InferGenericMethodSubstitutionViaAssignmentContextVisitor(
state, config, invokedMethodIsNullUnmarked);
Map<TypeVariable, Type> substitution;
Type returnType = methodSymbol.getReturnType();
returnType.accept(inferVisitor, typeFromAssignmentContext);
// check if returnType is a type variable
if (returnType instanceof Type.TypeVar) {
Type.TypeVar typeVar = (Type.TypeVar) returnType;
substitution = new LinkedHashMap<>();
Type upperBound = typeVar.getUpperBound();
boolean typeVarHasNullableUpperBound =
Nullness.hasNullableAnnotation(upperBound.getAnnotationMirrors().stream(), config);
if ((typeVarHasNullableUpperBound || invokedMethodIsNullUnmarked)
&& !Nullness.hasNullableAnnotation(
typeFromAssignmentContext.getAnnotationMirrors().stream(),
config)) { // can just use the lhs type nullability
substitution.put(typeVar, typeFromAssignmentContext);
} else { // rhs can't be nullable. use lhsType but strip @Nullable annotation
// TODO we should just strip out the top-level @Nullable annotation;
// stripMetadata() also removes nested @Nullable annotations
substitution.put(typeVar, typeFromAssignmentContext.stripMetadata());
}

Map<TypeVariable, Type> substitution = inferVisitor.getInferredSubstitution();
} else {
InferGenericMethodSubstitutionViaAssignmentContextVisitor inferVisitor =
new InferGenericMethodSubstitutionViaAssignmentContextVisitor(
state, config, invokedMethodIsNullUnmarked);
returnType.accept(inferVisitor, typeFromAssignmentContext);
substitution = inferVisitor.getInferredSubstitution();
}
inferredSubstitutionsForGenericMethodCalls.put(methodInvocationTree, substitution);
// update rhsType with inferred substitution
exprType =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,29 @@ public void genericInferenceOnParameterPassing() {
.doTest();
}

@Test
public void genericMethodReturningTypeVariable() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" static class Foo<T extends @Nullable Object> {}",
" static <T extends @Nullable Object> T returnTypeVariable(Foo<T> t) {",
" throw new RuntimeException();",
" }",
" static void takesNullable(@Nullable String s) {}",
" static void test() {",
" // legal, but we can't infer the type yet",
" takesNullable(Test.<@Nullable String>returnTypeVariable(new Foo<@Nullable String>()));",
" // also legal",
" takesNullable(returnTypeVariable(new Foo<String>()));",
" }",
"}")
.doTest();
}

private CompilationTestHelper makeHelper() {
return makeTestHelperWithArgs(
Arrays.asList(
Expand Down