Skip to content

Commit 5355c7c

Browse files
authored
JSpecify: initial handling of generic enclosing types for inner classes (#837)
This is a step towards fixing #836. We now properly handle the case of a generic enclosing type for an inner class type, both in checking matching of type argument nullability and in pretty-printing for error messages. We still don't handle `NewClassTree` types correctly which is why #836 is not yet fixed. Also fixes a bug in where generics checks were performed for `return` statements. Previously we would only check generic type arguments if the top-level nullability of the return type was `@NonNull`.
1 parent e7623f7 commit 5355c7c

File tree

3 files changed

+124
-65
lines changed

3 files changed

+124
-65
lines changed

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

Lines changed: 71 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,9 @@ private static void reportInvalidAssignmentInstantiationError(
129129
ErrorMessage.MessageTypes.ASSIGN_GENERIC_NULLABLE,
130130
String.format(
131131
"Cannot assign from type "
132-
+ prettyTypeForError(rhsType)
132+
+ prettyTypeForError(rhsType, state)
133133
+ " to type "
134-
+ prettyTypeForError(lhsType)
134+
+ prettyTypeForError(lhsType, state)
135135
+ " due to mismatched nullability of type parameters"));
136136
state.reportMatch(
137137
errorBuilder.createErrorDescription(
@@ -146,9 +146,9 @@ private static void reportInvalidReturnTypeError(
146146
ErrorMessage.MessageTypes.RETURN_NULLABLE_GENERIC,
147147
String.format(
148148
"Cannot return expression of type "
149-
+ prettyTypeForError(returnType)
149+
+ prettyTypeForError(returnType, state)
150150
+ " from method with return type "
151-
+ prettyTypeForError(methodType)
151+
+ prettyTypeForError(methodType, state)
152152
+ " due to mismatched nullability of type parameters"));
153153
state.reportMatch(
154154
errorBuilder.createErrorDescription(
@@ -163,9 +163,9 @@ private static void reportMismatchedTypeForTernaryOperator(
163163
ErrorMessage.MessageTypes.ASSIGN_GENERIC_NULLABLE,
164164
String.format(
165165
"Conditional expression must have type "
166-
+ prettyTypeForError(expressionType)
166+
+ prettyTypeForError(expressionType, state)
167167
+ " but the sub-expression has type "
168-
+ prettyTypeForError(subPartType)
168+
+ prettyTypeForError(subPartType, state)
169169
+ ", which has mismatched nullability of type parameters"));
170170
state.reportMatch(
171171
errorBuilder.createErrorDescription(
@@ -183,9 +183,9 @@ private static void reportInvalidParametersNullabilityError(
183183
new ErrorMessage(
184184
ErrorMessage.MessageTypes.PASS_NULLABLE_GENERIC,
185185
"Cannot pass parameter of type "
186-
+ prettyTypeForError(actualParameterType)
186+
+ prettyTypeForError(actualParameterType, state)
187187
+ ", as formal parameter has type "
188-
+ prettyTypeForError(formalParameterType)
188+
+ prettyTypeForError(formalParameterType, state)
189189
+ ", which has mismatched type parameter nullability");
190190
state.reportMatch(
191191
errorBuilder.createErrorDescription(
@@ -203,9 +203,9 @@ private static void reportInvalidOverridingMethodReturnTypeError(
203203
new ErrorMessage(
204204
ErrorMessage.MessageTypes.WRONG_OVERRIDE_RETURN_GENERIC,
205205
"Method returns "
206-
+ prettyTypeForError(overridingMethodReturnType)
206+
+ prettyTypeForError(overridingMethodReturnType, state)
207207
+ ", but overridden method returns "
208-
+ prettyTypeForError(overriddenMethodReturnType)
208+
+ prettyTypeForError(overriddenMethodReturnType, state)
209209
+ ", which has mismatched type parameter nullability");
210210
state.reportMatch(
211211
errorBuilder.createErrorDescription(
@@ -223,9 +223,9 @@ private static void reportInvalidOverridingMethodParamTypeError(
223223
new ErrorMessage(
224224
ErrorMessage.MessageTypes.WRONG_OVERRIDE_PARAM_GENERIC,
225225
"Parameter has type "
226-
+ prettyTypeForError(methodParamType)
226+
+ prettyTypeForError(methodParamType, state)
227227
+ ", but overridden method has parameter type "
228-
+ prettyTypeForError(typeParameterType)
228+
+ prettyTypeForError(typeParameterType, state)
229229
+ ", which has mismatched type parameter nullability");
230230
state.reportMatch(
231231
errorBuilder.createErrorDescription(
@@ -546,7 +546,10 @@ public Boolean visitClassType(Type.ClassType lhsType, Type rhsType) {
546546
return false;
547547
}
548548
}
549-
return true;
549+
// If there is an enclosing type (for non-static inner classes), its type argument nullability
550+
// should also match. When there is no enclosing type, getEnclosingType() returns a NoType
551+
// object, which gets handled by the fallback visitType() method
552+
return lhsType.getEnclosingType().accept(this, rhsType.getEnclosingType());
550553
}
551554

552555
@Override
@@ -992,57 +995,66 @@ private static Nullness getTypeNullness(Type type, Config config) {
992995
* Returns a pretty-printed representation of type suitable for error messages. The representation
993996
* uses simple names rather than fully-qualified names, and retains all type-use annotations.
994997
*/
995-
public static String prettyTypeForError(Type type) {
996-
return type.accept(PRETTY_TYPE_VISITOR, null);
998+
public static String prettyTypeForError(Type type, VisitorState state) {
999+
return type.accept(new PrettyTypeVisitor(state), null);
9971000
}
9981001

9991002
/** This code is a modified version of code in {@link com.google.errorprone.util.Signatures} */
1000-
private static final Type.Visitor<String, Void> PRETTY_TYPE_VISITOR =
1001-
new Types.DefaultTypeVisitor<String, Void>() {
1002-
@Override
1003-
public String visitWildcardType(Type.WildcardType t, Void unused) {
1004-
StringBuilder sb = new StringBuilder();
1005-
sb.append(t.kind);
1006-
if (t.kind != BoundKind.UNBOUND) {
1007-
sb.append(t.type.accept(this, null));
1008-
}
1009-
return sb.toString();
1010-
}
1003+
private static final class PrettyTypeVisitor extends Types.DefaultTypeVisitor<String, Void> {
10111004

1012-
@Override
1013-
public String visitClassType(Type.ClassType t, Void s) {
1014-
StringBuilder sb = new StringBuilder();
1015-
for (Attribute.TypeCompound compound : t.getAnnotationMirrors()) {
1016-
sb.append('@');
1017-
sb.append(compound.type.accept(this, null));
1018-
sb.append(' ');
1019-
}
1020-
sb.append(t.tsym.getSimpleName());
1021-
if (t.getTypeArguments().nonEmpty()) {
1022-
sb.append('<');
1023-
sb.append(
1024-
t.getTypeArguments().stream()
1025-
.map(a -> a.accept(this, null))
1026-
.collect(joining(", ")));
1027-
sb.append(">");
1028-
}
1029-
return sb.toString();
1030-
}
1005+
private final VisitorState state;
10311006

1032-
@Override
1033-
public String visitCapturedType(Type.CapturedType t, Void s) {
1034-
return t.wildcard.accept(this, null);
1035-
}
1007+
PrettyTypeVisitor(VisitorState state) {
1008+
this.state = state;
1009+
}
10361010

1037-
@Override
1038-
public String visitArrayType(Type.ArrayType t, Void unused) {
1039-
// TODO properly print cases like int @Nullable[]
1040-
return t.elemtype.accept(this, null) + "[]";
1041-
}
1011+
@Override
1012+
public String visitWildcardType(Type.WildcardType t, Void unused) {
1013+
// NOTE: we have not tested this code yet as we do not yet support wildcard types
1014+
StringBuilder sb = new StringBuilder();
1015+
sb.append(t.kind);
1016+
if (t.kind != BoundKind.UNBOUND) {
1017+
sb.append(t.type.accept(this, null));
1018+
}
1019+
return sb.toString();
1020+
}
10421021

1043-
@Override
1044-
public String visitType(Type t, Void s) {
1045-
return t.toString();
1046-
}
1047-
};
1022+
@Override
1023+
public String visitClassType(Type.ClassType t, Void s) {
1024+
StringBuilder sb = new StringBuilder();
1025+
Type enclosingType = t.getEnclosingType();
1026+
if (!ASTHelpers.isSameType(enclosingType, Type.noType, state)) {
1027+
sb.append(enclosingType.accept(this, null)).append('.');
1028+
}
1029+
for (Attribute.TypeCompound compound : t.getAnnotationMirrors()) {
1030+
sb.append('@');
1031+
sb.append(compound.type.accept(this, null));
1032+
sb.append(' ');
1033+
}
1034+
sb.append(t.tsym.getSimpleName());
1035+
if (t.getTypeArguments().nonEmpty()) {
1036+
sb.append('<');
1037+
sb.append(
1038+
t.getTypeArguments().stream().map(a -> a.accept(this, null)).collect(joining(", ")));
1039+
sb.append(">");
1040+
}
1041+
return sb.toString();
1042+
}
1043+
1044+
@Override
1045+
public String visitCapturedType(Type.CapturedType t, Void s) {
1046+
return t.wildcard.accept(this, null);
1047+
}
1048+
1049+
@Override
1050+
public String visitArrayType(Type.ArrayType t, Void unused) {
1051+
// TODO properly print cases like int @Nullable[]
1052+
return t.elemtype.accept(this, null) + "[]";
1053+
}
1054+
1055+
@Override
1056+
public String visitType(Type t, Void s) {
1057+
return t.toString();
1058+
}
1059+
}
10481060
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,10 @@ private Description checkReturnExpression(
867867
// support)
868868
return Description.NO_MATCH;
869869
}
870+
// Do the generics checks here, since we need to check the type arguments regardless of the
871+
// top-level nullability of the return type
872+
GenericsChecks.checkTypeParameterNullnessForFunctionReturnType(
873+
retExpr, methodSymbol, this, state);
870874
if (getMethodReturnNullness(methodSymbol, state, Nullness.NULLABLE).equals(Nullness.NULLABLE)) {
871875
return Description.NO_MATCH;
872876
}
@@ -880,8 +884,6 @@ private Description checkReturnExpression(
880884
state,
881885
methodSymbol);
882886
}
883-
GenericsChecks.checkTypeParameterNullnessForFunctionReturnType(
884-
retExpr, methodSymbol, this, state);
885887
return Description.NO_MATCH;
886888
}
887889

nullaway/src/test/java/com/uber/nullaway/NullAwayJSpecifyGenericsTests.java

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,51 @@ public void testOKNewClassInstantiationForOtherAnnotations() {
192192
.doTest();
193193
}
194194

195+
@Test
196+
public void nestedGenericTypes() {
197+
makeHelper()
198+
.addSourceLines(
199+
"Test.java",
200+
"package com.uber;",
201+
"import org.jspecify.annotations.Nullable;",
202+
"class Test {",
203+
" class Wrapper<P extends @Nullable Object> {",
204+
" abstract class Fn<R extends @Nullable Object> {",
205+
" abstract R apply(P p);",
206+
" }",
207+
" }",
208+
" static void param(@Nullable Wrapper<String>.Fn<String> p) {}",
209+
" static void positiveParam() {",
210+
" Wrapper<@Nullable String>.Fn<String> x = null;",
211+
" // BUG: Diagnostic contains: Cannot pass parameter of type Test.Wrapper<@Nullable String>.Fn<String>",
212+
" param(x);",
213+
" }",
214+
" static void positiveAssign() {",
215+
" Wrapper<@Nullable String>.Fn<String> p1 = null;",
216+
" // BUG: Diagnostic contains: Cannot assign from type Test.Wrapper<@Nullable String>.Fn<String> to type Test.Wrapper<String>.Fn<String>",
217+
" Wrapper<String>.Fn<String> p2 = p1;",
218+
" }",
219+
" static @Nullable Wrapper<String>.Fn<String> positiveReturn() {",
220+
" Wrapper<@Nullable String>.Fn<String> p1 = null;",
221+
" // BUG: Diagnostic contains: Cannot return expression of type Test.Wrapper<@Nullable String>.Fn<String>",
222+
" return p1;",
223+
" }",
224+
" static void negativeParam() {",
225+
" Wrapper<String>.Fn<String> x = null;",
226+
" param(x);",
227+
" }",
228+
" static void negativeAssign() {",
229+
" Wrapper<@Nullable String>.Fn<String> p1 = null;",
230+
" Wrapper<@Nullable String>.Fn<String> p2 = p1;",
231+
" }",
232+
" static @Nullable Wrapper<@Nullable String>.Fn<String> negativeReturn() {",
233+
" Wrapper<@Nullable String>.Fn<String> p1 = null;",
234+
" return p1;",
235+
" }",
236+
"}")
237+
.doTest();
238+
}
239+
195240
@Test
196241
public void downcastInstantiation() {
197242
makeHelper()
@@ -280,7 +325,7 @@ public void superTypeAssignmentChecksSingleInterface() {
280325
" interface Fn<P extends @Nullable Object, R extends @Nullable Object> {}",
281326
" class FnImpl implements Fn<@Nullable String, @Nullable String> {}",
282327
" void testPositive() {",
283-
" // BUG: Diagnostic contains: Cannot assign from type FnImpl",
328+
" // BUG: Diagnostic contains: Cannot assign from type Test.FnImpl",
284329
" Fn<@Nullable String, String> f = new FnImpl();",
285330
" }",
286331
" void testNegative() {",
@@ -1173,14 +1218,14 @@ public void overrideWithNestedTypeParametersInReturnType() {
11731218
" }",
11741219
" class TestFunc1 implements Fn<P<@Nullable String, String>, @Nullable String> {",
11751220
" @Override",
1176-
" // BUG: Diagnostic contains: Method returns P<@Nullable String, @Nullable String>, but overridden method",
1221+
" // BUG: Diagnostic contains: Method returns Test.P<@Nullable String, @Nullable String>, but overridden method",
11771222
" public P<@Nullable String, @Nullable String> apply() {",
11781223
" return new P<@Nullable String, @Nullable String>();",
11791224
" }",
11801225
" }",
11811226
" class TestFunc2 implements Fn<P<@Nullable String, @Nullable String>, @Nullable String> {",
11821227
" @Override",
1183-
" // BUG: Diagnostic contains: Method returns P<@Nullable String, String>, but overridden method returns",
1228+
" // BUG: Diagnostic contains: Method returns Test.P<@Nullable String, String>, but overridden method returns",
11841229
" public P<@Nullable String, String> apply() {",
11851230
" return new P<@Nullable String, String>();",
11861231
" }",
@@ -1209,7 +1254,7 @@ public void overrideWithNestedTypeParametersInParameterType() {
12091254
" }",
12101255
" class TestFunc implements Fn<P<String, String>, String> {",
12111256
" @Override",
1212-
" // BUG: Diagnostic contains: Parameter has type P<@Nullable String, String>, but overridden method has parameter type P<String, String>",
1257+
" // BUG: Diagnostic contains: Parameter has type Test.P<@Nullable String, String>, but overridden method has parameter type Test.P<String, String>",
12131258
" public String apply(P<@Nullable String, String> p, String s) {",
12141259
" return s;",
12151260
" }",

0 commit comments

Comments
 (0)