Skip to content

Commit c27f3f4

Browse files
authored
Substitute inferred @NonNull types for generic method inference (#1445)
Fixes #1444 In certain cases, like the test from #1444 added here, `javac` adds a `@Nullable` annotation to a type incorrectly as part of its inference. Here, for a call from the test: ```java @NullMarked public class Test { static class DefaultConfiguration { final @nullable Stream<? extends String> children; } final Stream<? extends String> children; Test(DefaultConfiguration configuration) { // relevant call this.children = notNull(configuration.children, "children must not be null"); } public static <T> T notNull(@nullable T object, String message) { throw new RuntimeException("todo"); } } ``` `javac`s inferred type for the return of the `notNull` call matches the inferred parameter type exactly, `@Nullable Stream<...>`. But, when applying JSpecify rules, the return cannot be `@Nullable`. To fix this, add inferred `@NonNull` qualifiers along with `@Nullable` when doing a substitution inside a type. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Simplified printing of error/unknown generic types to show only the simple type name. * Improved type-variable substitution so inferred nullability is applied for both nullable and non-null cases. * **Tests** * Added/updated tests covering generic method type inference and JSpecify/Nullability behaviors (includes a new scenario for issue1444). <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent fc6d956 commit c27f3f4

3 files changed

Lines changed: 52 additions & 13 deletions

File tree

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ public String visitArrayType(Type.ArrayType t, @Nullable Void unused) {
8989
return sb.append(" []").toString();
9090
}
9191

92+
@Override
93+
public String visitErrorType(Type.ErrorType t, @Nullable Void unused) {
94+
// this arises for our synthetic @Nullable and @NonNull annotations; we just return the simple
95+
// name
96+
return t.tsym.getSimpleName().toString();
97+
}
98+
9299
@Override
93100
public String visitType(Type t, @Nullable Void s) {
94101
return t.toString();

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -224,18 +224,20 @@ private static Type substituteInferredNullabilityForTypeVariables(
224224
ListBuffer<Type> inferredTypes = new ListBuffer<>();
225225
for (Map.Entry<Element, ConstraintSolver.InferredNullability> entry :
226226
typeVarNullability.entrySet()) {
227-
if (entry.getValue() == NULLABLE) {
228-
// find all TypeVars occurring in targetType with the same symbol and substitute for those.
229-
// we can have multiple such TypeVars due to previous substitutions that modified the type
230-
// in some way, e.g., by changing its bounds
231-
Element symbol = entry.getKey();
232-
TypeVarWithSymbolCollector tvc = new TypeVarWithSymbolCollector(symbol);
233-
targetType.accept(tvc, null);
234-
for (Type.TypeVar tv : tvc.getMatches()) {
235-
typeVars.append(tv);
236-
inferredTypes.append(
237-
typeWithAnnot(tv, GenericsChecks.getSyntheticNullableAnnotType(state)));
238-
}
227+
// find all TypeVars occurring in targetType with the same symbol and substitute for those.
228+
// we can have multiple such TypeVars due to previous substitutions that modified the type
229+
// in some way, e.g., by changing its bounds
230+
Element symbol = entry.getKey();
231+
TypeVarWithSymbolCollector tvc = new TypeVarWithSymbolCollector(symbol);
232+
targetType.accept(tvc, null);
233+
for (Type.TypeVar tv : tvc.getMatches()) {
234+
typeVars.append(tv);
235+
inferredTypes.append(
236+
typeWithAnnot(
237+
tv,
238+
entry.getValue() == NULLABLE
239+
? GenericsChecks.getSyntheticNullableAnnotType(state)
240+
: GenericsChecks.getSyntheticNonNullAnnotType(state)));
239241
}
240242
}
241243
List<Type> typeVarsToReplace = typeVars.toList();

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,7 @@ abstract class TestCase {
425425
static class Bar<T extends @Nullable Object> {}
426426
abstract <U> Bar<U> make(Bar<U> other);
427427
void test(Bar<Bar<String>> other) {
428-
// BUG: Diagnostic contains: incompatible types: Bar<Bar<String>> cannot be converted to Bar<Bar<@Nullable String>>
428+
// BUG: Diagnostic contains: incompatible types: Bar<@NonNull Bar<String>> cannot be converted to Bar<Bar<@Nullable String>>
429429
Bar<Bar<@Nullable String>> unused = make(other);
430430
}
431431
}
@@ -1498,6 +1498,36 @@ void test(String s1, String s2, List<Object> l) {
14981498
.doTest();
14991499
}
15001500

1501+
@Test
1502+
public void issue1444() {
1503+
makeHelper()
1504+
.addSourceLines(
1505+
"Test.java",
1506+
"""
1507+
import org.jspecify.annotations.NullMarked;
1508+
import org.jspecify.annotations.Nullable;
1509+
import java.util.stream.Stream;
1510+
@NullMarked
1511+
public class Test {
1512+
static class DefaultConfiguration {
1513+
final @Nullable Stream<? extends String> children;
1514+
1515+
DefaultConfiguration(Stream<? extends String> children) {
1516+
this.children = children;
1517+
}
1518+
}
1519+
final Stream<? extends String> children;
1520+
Test(DefaultConfiguration configuration) {
1521+
this.children = notNull(configuration.children, "children must not be null");
1522+
}
1523+
public static <T> T notNull(@Nullable T object, String message) {
1524+
throw new RuntimeException("todo");
1525+
}
1526+
}
1527+
""")
1528+
.doTest();
1529+
}
1530+
15011531
private CompilationTestHelper makeHelper() {
15021532
return makeTestHelperWithArgs(
15031533
JSpecifyJavacConfig.withJSpecifyModeArgs(

0 commit comments

Comments
 (0)