Skip to content

Commit 0431a90

Browse files
akulk022msridhar
andauthored
Issue #740: Adding visitors for handling different types along with ClassType in Generic Type invariance check (#806)
- Description: Only ClassTypes are supported for checking the invariance of the nullability annotations for Generic types and hence TypeVisitors and a TreeVisitor are added to handle different scenarios like for example: ` Foo<Foo<@nullable String>[]> x = new Foo<Foo<String>[]>(); ` - Issue Number: 740 - All the tests in NullAwayJSpecifyGenericTests.java have passed for these changes. Fixes #740 --------- Co-authored-by: Manu Sridharan <msridhar@gmail.com>
1 parent ade9ed1 commit 0431a90

2 files changed

Lines changed: 237 additions & 101 deletions

File tree

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

Lines changed: 158 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.google.errorprone.util.ASTHelpers;
1111
import com.sun.source.tree.AnnotatedTypeTree;
1212
import com.sun.source.tree.AnnotationTree;
13+
import com.sun.source.tree.ArrayTypeTree;
1314
import com.sun.source.tree.AssignmentTree;
1415
import com.sun.source.tree.ConditionalExpressionTree;
1516
import com.sun.source.tree.ExpressionTree;
@@ -20,6 +21,7 @@
2021
import com.sun.source.tree.ParameterizedTypeTree;
2122
import com.sun.source.tree.Tree;
2223
import com.sun.source.tree.VariableTree;
24+
import com.sun.source.util.SimpleTreeVisitor;
2325
import com.sun.tools.javac.code.Attribute;
2426
import com.sun.tools.javac.code.BoundKind;
2527
import com.sun.tools.javac.code.Symbol;
@@ -354,57 +356,10 @@ public static void checkTypeParameterNullnessForFunctionReturnType(
354356
* @param state the visitor state
355357
*/
356358
private static boolean compareNullabilityAnnotations(
357-
Type.ClassType lhsType, Type.ClassType rhsType, VisitorState state) {
358-
Types types = state.getTypes();
359-
// The base type of rhsType may be a subtype of lhsType's base type. In such cases, we must
360-
// compare lhsType against the supertype of rhsType with a matching base type.
361-
rhsType = (Type.ClassType) types.asSuper(rhsType, lhsType.tsym);
362-
// This is impossible, considering the fact that standard Java subtyping succeeds before running
363-
// NullAway
364-
if (rhsType == null) {
365-
throw new RuntimeException("Did not find supertype of " + rhsType + " matching " + lhsType);
366-
}
367-
List<Type> lhsTypeArguments = lhsType.getTypeArguments();
368-
List<Type> rhsTypeArguments = rhsType.getTypeArguments();
369-
// This is impossible, considering the fact that standard Java subtyping succeeds before running
370-
// NullAway
371-
if (lhsTypeArguments.size() != rhsTypeArguments.size()) {
372-
throw new RuntimeException(
373-
"Number of types arguments in " + rhsType + " does not match " + lhsType);
374-
}
375-
for (int i = 0; i < lhsTypeArguments.size(); i++) {
376-
Type lhsTypeArgument = lhsTypeArguments.get(i);
377-
Type rhsTypeArgument = rhsTypeArguments.get(i);
378-
boolean isLHSNullableAnnotated = false;
379-
List<Attribute.TypeCompound> lhsAnnotations = lhsTypeArgument.getAnnotationMirrors();
380-
// To ensure that we are checking only jspecify nullable annotations
381-
for (Attribute.TypeCompound annotation : lhsAnnotations) {
382-
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
383-
isLHSNullableAnnotated = true;
384-
break;
385-
}
386-
}
387-
boolean isRHSNullableAnnotated = false;
388-
List<Attribute.TypeCompound> rhsAnnotations = rhsTypeArgument.getAnnotationMirrors();
389-
// To ensure that we are checking only jspecify nullable annotations
390-
for (Attribute.TypeCompound annotation : rhsAnnotations) {
391-
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
392-
isRHSNullableAnnotated = true;
393-
break;
394-
}
395-
}
396-
if (isLHSNullableAnnotated != isRHSNullableAnnotated) {
397-
return false;
398-
}
399-
// nested generics
400-
if (lhsTypeArgument.getTypeArguments().length() > 0) {
401-
if (!compareNullabilityAnnotations(
402-
(Type.ClassType) lhsTypeArgument, (Type.ClassType) rhsTypeArgument, state)) {
403-
return false;
404-
}
405-
}
406-
}
407-
return true;
359+
Type lhsType, Type rhsType, VisitorState state) {
360+
// it is fair to assume rhyType should be the same as lhsType as the Java compiler has passed
361+
// before NullAway.
362+
return lhsType.accept(new CompareNullabilityVisitor(state), rhsType);
408363
}
409364

410365
/**
@@ -418,56 +373,7 @@ private static boolean compareNullabilityAnnotations(
418373
*/
419374
private static Type.ClassType typeWithPreservedAnnotations(
420375
ParameterizedTypeTree tree, VisitorState state) {
421-
Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree);
422-
Preconditions.checkNotNull(type);
423-
Type nullableType = NULLABLE_TYPE_SUPPLIER.get(state);
424-
List<? extends Tree> typeArguments = tree.getTypeArguments();
425-
List<Type> newTypeArgs = new ArrayList<>();
426-
for (int i = 0; i < typeArguments.size(); i++) {
427-
AnnotatedTypeTree annotatedType = null;
428-
Tree curTypeArg = typeArguments.get(i);
429-
// If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a
430-
// ParameterizedTypeTree in the case of a nested generic type
431-
if (curTypeArg instanceof AnnotatedTypeTree) {
432-
annotatedType = (AnnotatedTypeTree) curTypeArg;
433-
} else if (curTypeArg instanceof ParameterizedTypeTree
434-
&& ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) {
435-
annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType();
436-
}
437-
List<? extends AnnotationTree> annotations =
438-
annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList();
439-
boolean hasNullableAnnotation = false;
440-
for (AnnotationTree annotation : annotations) {
441-
if (ASTHelpers.isSameType(
442-
nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) {
443-
hasNullableAnnotation = true;
444-
break;
445-
}
446-
}
447-
// construct a TypeMetadata object containing a nullability annotation if needed
448-
com.sun.tools.javac.util.List<Attribute.TypeCompound> nullableAnnotationCompound =
449-
hasNullableAnnotation
450-
? com.sun.tools.javac.util.List.from(
451-
Collections.singletonList(
452-
new Attribute.TypeCompound(
453-
nullableType, com.sun.tools.javac.util.List.nil(), null)))
454-
: com.sun.tools.javac.util.List.nil();
455-
TypeMetadata typeMetadata =
456-
new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound));
457-
Type currentTypeArgType = castToNonNull(ASTHelpers.getType(curTypeArg));
458-
if (currentTypeArgType.getTypeArguments().size() > 0) {
459-
// nested generic type; recursively preserve its nullability type argument annotations
460-
currentTypeArgType =
461-
typeWithPreservedAnnotations((ParameterizedTypeTree) curTypeArg, state);
462-
}
463-
Type.ClassType newTypeArgType =
464-
(Type.ClassType) currentTypeArgType.cloneWithMetadata(typeMetadata);
465-
newTypeArgs.add(newTypeArgType);
466-
}
467-
Type.ClassType finalType =
468-
new Type.ClassType(
469-
type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym);
470-
return finalType;
376+
return (Type.ClassType) tree.accept(new PreservedAnnotationTreeVisitor(state), null);
471377
}
472378

473379
/**
@@ -577,6 +483,156 @@ public static void compareGenericTypeParameterNullabilityForCall(
577483
}
578484
}
579485

486+
/**
487+
* Visitor that is called from compareNullabilityAnnotations which recursively compares the
488+
* Nullability annotations for the nested generic type arguments. Compares the Type it is called
489+
* upon, i.e. the LHS type and the Type passed as an argument, i.e. The RHS type.
490+
*/
491+
public static class CompareNullabilityVisitor extends Types.DefaultTypeVisitor<Boolean, Type> {
492+
private final VisitorState state;
493+
494+
CompareNullabilityVisitor(VisitorState state) {
495+
this.state = state;
496+
}
497+
498+
@Override
499+
public Boolean visitClassType(Type.ClassType lhsType, Type rhsType) {
500+
Types types = state.getTypes();
501+
// The base type of rhsType may be a subtype of lhsType's base type. In such cases, we must
502+
// compare lhsType against the supertype of rhsType with a matching base type.
503+
rhsType = (Type.ClassType) types.asSuper(rhsType, lhsType.tsym);
504+
// This is impossible, considering the fact that standard Java subtyping succeeds before
505+
// running NullAway
506+
if (rhsType == null) {
507+
throw new RuntimeException("Did not find supertype of " + rhsType + " matching " + lhsType);
508+
}
509+
List<Type> lhsTypeArguments = lhsType.getTypeArguments();
510+
List<Type> rhsTypeArguments = rhsType.getTypeArguments();
511+
// This is impossible, considering the fact that standard Java subtyping succeeds before
512+
// running NullAway
513+
if (lhsTypeArguments.size() != rhsTypeArguments.size()) {
514+
throw new RuntimeException(
515+
"Number of types arguments in " + rhsType + " does not match " + lhsType);
516+
}
517+
for (int i = 0; i < lhsTypeArguments.size(); i++) {
518+
Type lhsTypeArgument = lhsTypeArguments.get(i);
519+
Type rhsTypeArgument = rhsTypeArguments.get(i);
520+
boolean isLHSNullableAnnotated = false;
521+
List<Attribute.TypeCompound> lhsAnnotations = lhsTypeArgument.getAnnotationMirrors();
522+
// To ensure that we are checking only jspecify nullable annotations
523+
for (Attribute.TypeCompound annotation : lhsAnnotations) {
524+
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
525+
isLHSNullableAnnotated = true;
526+
break;
527+
}
528+
}
529+
boolean isRHSNullableAnnotated = false;
530+
List<Attribute.TypeCompound> rhsAnnotations = rhsTypeArgument.getAnnotationMirrors();
531+
// To ensure that we are checking only jspecify nullable annotations
532+
for (Attribute.TypeCompound annotation : rhsAnnotations) {
533+
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
534+
isRHSNullableAnnotated = true;
535+
break;
536+
}
537+
}
538+
if (isLHSNullableAnnotated != isRHSNullableAnnotated) {
539+
return false;
540+
}
541+
// nested generics
542+
if (!lhsTypeArgument.accept(this, rhsTypeArgument)) {
543+
return false;
544+
}
545+
}
546+
return true;
547+
}
548+
549+
@Override
550+
public Boolean visitArrayType(Type.ArrayType lhsType, Type rhsType) {
551+
Type.ArrayType arrRhsType = (Type.ArrayType) rhsType;
552+
return lhsType.getComponentType().accept(this, arrRhsType.getComponentType());
553+
}
554+
555+
@Override
556+
public Boolean visitType(Type t, Type type) {
557+
return true;
558+
}
559+
}
560+
561+
/**
562+
* Visitor For getting the preserved Annotation Type for the nested generic type arguments within
563+
* the ParameterizedTypeTree originally passed to TypeWithPreservedAnnotations method, since these
564+
* nested arguments may not always be ParameterizedTypeTrees and may be of different types for
565+
* e.g. ArrayTypeTree.
566+
*/
567+
public static class PreservedAnnotationTreeVisitor extends SimpleTreeVisitor<Type, Void> {
568+
569+
private final VisitorState state;
570+
571+
PreservedAnnotationTreeVisitor(VisitorState state) {
572+
this.state = state;
573+
}
574+
575+
@Override
576+
public Type visitArrayType(ArrayTypeTree tree, Void p) {
577+
Type elemType = tree.getType().accept(this, null);
578+
return new Type.ArrayType(elemType, castToNonNull(ASTHelpers.getType(tree)).tsym);
579+
}
580+
581+
@Override
582+
public Type visitParameterizedType(ParameterizedTypeTree tree, Void p) {
583+
Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree);
584+
Preconditions.checkNotNull(type);
585+
Type nullableType = NULLABLE_TYPE_SUPPLIER.get(state);
586+
List<? extends Tree> typeArguments = tree.getTypeArguments();
587+
List<Type> newTypeArgs = new ArrayList<>();
588+
for (int i = 0; i < typeArguments.size(); i++) {
589+
AnnotatedTypeTree annotatedType = null;
590+
Tree curTypeArg = typeArguments.get(i);
591+
// If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a
592+
// ParameterizedTypeTree in the case of a nested generic type
593+
if (curTypeArg instanceof AnnotatedTypeTree) {
594+
annotatedType = (AnnotatedTypeTree) curTypeArg;
595+
} else if (curTypeArg instanceof ParameterizedTypeTree
596+
&& ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) {
597+
annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType();
598+
}
599+
List<? extends AnnotationTree> annotations =
600+
annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList();
601+
boolean hasNullableAnnotation = false;
602+
for (AnnotationTree annotation : annotations) {
603+
if (ASTHelpers.isSameType(
604+
nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) {
605+
hasNullableAnnotation = true;
606+
break;
607+
}
608+
}
609+
// construct a TypeMetadata object containing a nullability annotation if needed
610+
com.sun.tools.javac.util.List<Attribute.TypeCompound> nullableAnnotationCompound =
611+
hasNullableAnnotation
612+
? com.sun.tools.javac.util.List.from(
613+
Collections.singletonList(
614+
new Attribute.TypeCompound(
615+
nullableType, com.sun.tools.javac.util.List.nil(), null)))
616+
: com.sun.tools.javac.util.List.nil();
617+
TypeMetadata typeMetadata =
618+
new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound));
619+
Type currentTypeArgType = curTypeArg.accept(this, null);
620+
Type newTypeArgType = currentTypeArgType.cloneWithMetadata(typeMetadata);
621+
newTypeArgs.add(newTypeArgType);
622+
}
623+
Type.ClassType finalType =
624+
new Type.ClassType(
625+
type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym);
626+
return finalType;
627+
}
628+
629+
/** By default, just use the type computed by javac */
630+
@Override
631+
protected Type defaultAction(Tree node, Void unused) {
632+
return castToNonNull(ASTHelpers.getType(node));
633+
}
634+
}
635+
580636
/**
581637
* Checks that type parameter nullability is consistent between an overriding method and the
582638
* corresponding overridden method.
@@ -908,6 +964,7 @@ public String visitCapturedType(Type.CapturedType t, Void s) {
908964

909965
@Override
910966
public String visitArrayType(Type.ArrayType t, Void unused) {
967+
// TODO properly print cases like int @Nullable[]
911968
return t.elemtype.accept(this, null) + "[]";
912969
}
913970

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,85 @@ public void rawTypes() {
760760
.doTest();
761761
}
762762

763+
@Test
764+
public void nestedGenericTypeAssignment() {
765+
makeHelper()
766+
.addSourceLines(
767+
"Test.java",
768+
"package com.uber;",
769+
"import org.jspecify.annotations.Nullable;",
770+
"class Test {",
771+
" static class A<T extends @Nullable Object> { }",
772+
" static void testPositive() {",
773+
" // BUG: Diagnostic contains: Cannot assign from type",
774+
" A<A<@Nullable String>[]> var1 = new A<A<String>[]>();",
775+
" // BUG: Diagnostic contains: Cannot assign from type",
776+
" A<A<String>[]> var2 = new A<A<@Nullable String>[]>();",
777+
" }",
778+
" static void testNegative() {",
779+
" A<A<@Nullable String>[]> var1 = new A<A<@Nullable String>[]>();",
780+
" A<A<String>[]> var2 = new A<A<String>[]>();",
781+
" }",
782+
"}")
783+
.doTest();
784+
}
785+
786+
@Test
787+
public void genericPrimitiveArrayTypeAssignment() {
788+
makeHelper()
789+
.addSourceLines(
790+
"Test.java",
791+
"package com.uber;",
792+
"import org.jspecify.annotations.Nullable;",
793+
"class Test {",
794+
" static class A<T extends @Nullable Object> { }",
795+
" static void testPositive() {",
796+
" // BUG: Diagnostic contains: Cannot assign from type A<int[]>",
797+
" A<int @Nullable[]> x = new A<int[]>();",
798+
" }",
799+
" static void testNegative() {",
800+
" A<int @Nullable[]> x = new A<int @Nullable[]>();",
801+
" }",
802+
"}")
803+
.doTest();
804+
}
805+
806+
@Test
807+
public void nestedGenericTypeVariables() {
808+
makeHelper()
809+
.addSourceLines(
810+
"Test.java",
811+
"package com.uber;",
812+
"import org.jspecify.annotations.Nullable;",
813+
"class Test {",
814+
" static class A<T extends @Nullable Object> { }",
815+
" static class B<T> {",
816+
" void foo() {",
817+
" A<A<T>[]> x = new A<A<T>[]>();",
818+
" }",
819+
" }",
820+
"}")
821+
.doTest();
822+
}
823+
824+
@Test
825+
public void nestedGenericWildcardTypeVariables() {
826+
makeHelper()
827+
.addSourceLines(
828+
"Test.java",
829+
"package com.uber;",
830+
"import org.jspecify.annotations.Nullable;",
831+
"class Test {",
832+
" static class A<T extends @Nullable Object> { }",
833+
" static class B<T> {",
834+
" void foo() {",
835+
" A<A<? extends String>[]> x = new A<A<? extends String>[]>();",
836+
" }",
837+
" }",
838+
"}")
839+
.doTest();
840+
}
841+
763842
@Test
764843
public void overrideReturnTypes() {
765844
makeHelper()

0 commit comments

Comments
 (0)