Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #740: Adding visitors for handling different types along with ClassType in Generic Type invariance check #806

Merged
merged 31 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a0b76ea
adding failing test case for issue 740
akulk022 Jun 24, 2023
b2c533a
adding visitors for handling ArrayTypes
akulk022 Aug 7, 2023
6ae742b
clean-up of redundant code in visitors
akulk022 Aug 14, 2023
15e1601
fixing indentation
akulk022 Aug 14, 2023
9fae10c
Merge branch 'master' into generic_type_issue_#740_ak
msridhar Aug 14, 2023
47dd926
updating visitor methods returning null
akulk022 Aug 16, 2023
f5b6c1d
making default Type method Nullable in visitor
akulk022 Aug 16, 2023
e9d969b
formatting
msridhar Aug 16, 2023
20d747a
changing TreeVisitor second argument type to Void since it is unused
akulk022 Aug 16, 2023
0d3c2f0
Update default return type for Compare Nullability Visitor
akulk022 Aug 16, 2023
4273601
removing cast from TYPE_ARG_VISITOR
akulk022 Aug 16, 2023
9e96103
removing unnecessary @Nullable annotation for method that always retu…
akulk022 Aug 17, 2023
d1e61bf
Merge branch 'generic_type_issue_#740_ak' of https://github.com/akulk…
akulk022 Aug 17, 2023
d2db3cc
Merge remote-tracking branch 'origin/master' into generic_type_issue_…
akulk022 Aug 17, 2023
d7c9812
After merging with changes made by PR#805 making GenericChecks method…
akulk022 Aug 17, 2023
aed09fd
Adding test cases
akulk022 Aug 23, 2023
1a12cef
Merge branch 'master' into generic_type_issue_#740_ak
msridhar Aug 25, 2023
a4bd9bc
Merge branch 'master' into generic_type_issue_#740_ak
msridhar Aug 25, 2023
680a17c
Removing redundant code from TypeWithPreservedAnnotations
akulk022 Aug 27, 2023
d314952
Adding more descriptive comments for visitors and fixing formatting o…
akulk022 Aug 27, 2023
b9eb8fc
fix NPE issue
msridhar Aug 27, 2023
989a755
fix other test failure
msridhar Aug 27, 2023
4e5adf0
copy type symbol for array type from original
msridhar Aug 27, 2023
657f38e
removing TYPE_ARG_VISITOR and the check to see if we have more nested…
akulk022 Aug 28, 2023
6556379
Update nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java
akulk022 Aug 28, 2023
ce51560
using the existing visitor instead of allocating a new one
akulk022 Aug 28, 2023
2e14f16
fix position of annotations
msridhar Aug 28, 2023
6f5c722
add todo
msridhar Aug 28, 2023
b419103
restore
msridhar Aug 28, 2023
394ff52
more tests
msridhar Aug 29, 2023
1438bbd
Merge branch 'master' into generic_type_issue_#740_ak
msridhar Aug 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 158 additions & 101 deletions nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ExpressionTree;
Expand All @@ -20,6 +21,7 @@
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.BoundKind;
import com.sun.tools.javac.code.Symbol;
Expand Down Expand Up @@ -354,57 +356,10 @@ public static void checkTypeParameterNullnessForFunctionReturnType(
* @param state the visitor state
*/
private static boolean compareNullabilityAnnotations(
Type.ClassType lhsType, Type.ClassType rhsType, VisitorState state) {
Types types = state.getTypes();
// The base type of rhsType may be a subtype of lhsType's base type. In such cases, we must
// compare lhsType against the supertype of rhsType with a matching base type.
rhsType = (Type.ClassType) types.asSuper(rhsType, lhsType.tsym);
// This is impossible, considering the fact that standard Java subtyping succeeds before running
// NullAway
if (rhsType == null) {
throw new RuntimeException("Did not find supertype of " + rhsType + " matching " + lhsType);
}
List<Type> lhsTypeArguments = lhsType.getTypeArguments();
List<Type> rhsTypeArguments = rhsType.getTypeArguments();
// This is impossible, considering the fact that standard Java subtyping succeeds before running
// NullAway
if (lhsTypeArguments.size() != rhsTypeArguments.size()) {
throw new RuntimeException(
"Number of types arguments in " + rhsType + " does not match " + lhsType);
}
for (int i = 0; i < lhsTypeArguments.size(); i++) {
Type lhsTypeArgument = lhsTypeArguments.get(i);
Type rhsTypeArgument = rhsTypeArguments.get(i);
boolean isLHSNullableAnnotated = false;
List<Attribute.TypeCompound> lhsAnnotations = lhsTypeArgument.getAnnotationMirrors();
// To ensure that we are checking only jspecify nullable annotations
for (Attribute.TypeCompound annotation : lhsAnnotations) {
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
isLHSNullableAnnotated = true;
break;
}
}
boolean isRHSNullableAnnotated = false;
List<Attribute.TypeCompound> rhsAnnotations = rhsTypeArgument.getAnnotationMirrors();
// To ensure that we are checking only jspecify nullable annotations
for (Attribute.TypeCompound annotation : rhsAnnotations) {
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
isRHSNullableAnnotated = true;
break;
}
}
if (isLHSNullableAnnotated != isRHSNullableAnnotated) {
return false;
}
// nested generics
if (lhsTypeArgument.getTypeArguments().length() > 0) {
if (!compareNullabilityAnnotations(
(Type.ClassType) lhsTypeArgument, (Type.ClassType) rhsTypeArgument, state)) {
return false;
}
}
}
return true;
Type lhsType, Type rhsType, VisitorState state) {
// it is fair to assume rhyType should be the same as lhsType as the Java compiler has passed
// before NullAway.
return lhsType.accept(new CompareNullabilityVisitor(state), rhsType);
}

/**
Expand All @@ -418,56 +373,7 @@ private static boolean compareNullabilityAnnotations(
*/
private static Type.ClassType typeWithPreservedAnnotations(
ParameterizedTypeTree tree, VisitorState state) {
Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree);
Preconditions.checkNotNull(type);
Type nullableType = NULLABLE_TYPE_SUPPLIER.get(state);
List<? extends Tree> typeArguments = tree.getTypeArguments();
List<Type> newTypeArgs = new ArrayList<>();
for (int i = 0; i < typeArguments.size(); i++) {
AnnotatedTypeTree annotatedType = null;
Tree curTypeArg = typeArguments.get(i);
// If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a
// ParameterizedTypeTree in the case of a nested generic type
if (curTypeArg instanceof AnnotatedTypeTree) {
annotatedType = (AnnotatedTypeTree) curTypeArg;
} else if (curTypeArg instanceof ParameterizedTypeTree
&& ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) {
annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType();
}
List<? extends AnnotationTree> annotations =
annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList();
boolean hasNullableAnnotation = false;
for (AnnotationTree annotation : annotations) {
if (ASTHelpers.isSameType(
nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) {
hasNullableAnnotation = true;
break;
}
}
// construct a TypeMetadata object containing a nullability annotation if needed
com.sun.tools.javac.util.List<Attribute.TypeCompound> nullableAnnotationCompound =
hasNullableAnnotation
? com.sun.tools.javac.util.List.from(
Collections.singletonList(
new Attribute.TypeCompound(
nullableType, com.sun.tools.javac.util.List.nil(), null)))
: com.sun.tools.javac.util.List.nil();
TypeMetadata typeMetadata =
new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound));
Type currentTypeArgType = castToNonNull(ASTHelpers.getType(curTypeArg));
if (currentTypeArgType.getTypeArguments().size() > 0) {
// nested generic type; recursively preserve its nullability type argument annotations
currentTypeArgType =
typeWithPreservedAnnotations((ParameterizedTypeTree) curTypeArg, state);
}
Type.ClassType newTypeArgType =
(Type.ClassType) currentTypeArgType.cloneWithMetadata(typeMetadata);
newTypeArgs.add(newTypeArgType);
}
Type.ClassType finalType =
new Type.ClassType(
type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym);
return finalType;
return (Type.ClassType) tree.accept(new PreservedAnnotationTreeVisitor(state), null);
}

/**
Expand Down Expand Up @@ -577,6 +483,156 @@ public static void compareGenericTypeParameterNullabilityForCall(
}
}

/**
* Visitor that is called from compareNullabilityAnnotations which recursively compares the
* Nullability annotations for the nested generic type arguments. Compares the Type it is called
* upon, i.e. the LHS type and the Type passed as an argument, i.e. The RHS type.
*/
public static class CompareNullabilityVisitor extends Types.DefaultTypeVisitor<Boolean, Type> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine with these being inner classes for now, but a good follow up PR would be a simple refactor of this into a com.uber.nullaway.generics package with three top level classes (preferably in its own PR without any other changes, so it's easier to review).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #817, agreed this is good for a follow up

private final VisitorState state;

CompareNullabilityVisitor(VisitorState state) {
this.state = state;
}

@Override
public Boolean visitClassType(Type.ClassType lhsType, Type rhsType) {
Types types = state.getTypes();
// The base type of rhsType may be a subtype of lhsType's base type. In such cases, we must
// compare lhsType against the supertype of rhsType with a matching base type.
rhsType = (Type.ClassType) types.asSuper(rhsType, lhsType.tsym);
// This is impossible, considering the fact that standard Java subtyping succeeds before
// running NullAway
if (rhsType == null) {
throw new RuntimeException("Did not find supertype of " + rhsType + " matching " + lhsType);
}
List<Type> lhsTypeArguments = lhsType.getTypeArguments();
List<Type> rhsTypeArguments = rhsType.getTypeArguments();
// This is impossible, considering the fact that standard Java subtyping succeeds before
// running NullAway
if (lhsTypeArguments.size() != rhsTypeArguments.size()) {
throw new RuntimeException(
"Number of types arguments in " + rhsType + " does not match " + lhsType);
}
for (int i = 0; i < lhsTypeArguments.size(); i++) {
Type lhsTypeArgument = lhsTypeArguments.get(i);
Type rhsTypeArgument = rhsTypeArguments.get(i);
boolean isLHSNullableAnnotated = false;
List<Attribute.TypeCompound> lhsAnnotations = lhsTypeArgument.getAnnotationMirrors();
// To ensure that we are checking only jspecify nullable annotations
for (Attribute.TypeCompound annotation : lhsAnnotations) {
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
isLHSNullableAnnotated = true;
break;
}
}
boolean isRHSNullableAnnotated = false;
List<Attribute.TypeCompound> rhsAnnotations = rhsTypeArgument.getAnnotationMirrors();
// To ensure that we are checking only jspecify nullable annotations
for (Attribute.TypeCompound annotation : rhsAnnotations) {
if (annotation.getAnnotationType().toString().equals(NULLABLE_NAME)) {
isRHSNullableAnnotated = true;
break;
}
}
if (isLHSNullableAnnotated != isRHSNullableAnnotated) {
return false;
}
// nested generics
if (!lhsTypeArgument.accept(this, rhsTypeArgument)) {
return false;
}
}
return true;
}

@Override
public Boolean visitArrayType(Type.ArrayType lhsType, Type rhsType) {
Type.ArrayType arrRhsType = (Type.ArrayType) rhsType;
return lhsType.getComponentType().accept(this, arrRhsType.getComponentType());
}

@Override
public Boolean visitType(Type t, Type type) {
return true;
}
}

/**
* Visitor For getting the preserved Annotation Type for the nested generic type arguments within
* the ParameterizedTypeTree originally passed to TypeWithPreservedAnnotations method, since these
* nested arguments may not always be ParameterizedTypeTrees and may be of different types for
* e.g. ArrayTypeTree.
*/
public static class PreservedAnnotationTreeVisitor extends SimpleTreeVisitor<Type, Void> {

private final VisitorState state;

PreservedAnnotationTreeVisitor(VisitorState state) {
this.state = state;
}

@Override
public Type visitArrayType(ArrayTypeTree tree, Void p) {
Type elemType = tree.getType().accept(this, null);
return new Type.ArrayType(elemType, castToNonNull(ASTHelpers.getType(tree)).tsym);
}

@Override
public Type visitParameterizedType(ParameterizedTypeTree tree, Void p) {
Type.ClassType type = (Type.ClassType) ASTHelpers.getType(tree);
msridhar marked this conversation as resolved.
Show resolved Hide resolved
Preconditions.checkNotNull(type);
Type nullableType = NULLABLE_TYPE_SUPPLIER.get(state);
List<? extends Tree> typeArguments = tree.getTypeArguments();
List<Type> newTypeArgs = new ArrayList<>();
for (int i = 0; i < typeArguments.size(); i++) {
AnnotatedTypeTree annotatedType = null;
Tree curTypeArg = typeArguments.get(i);
// If the type argument has an annotation, it will either be an AnnotatedTypeTree, or a
// ParameterizedTypeTree in the case of a nested generic type
if (curTypeArg instanceof AnnotatedTypeTree) {
annotatedType = (AnnotatedTypeTree) curTypeArg;
} else if (curTypeArg instanceof ParameterizedTypeTree
&& ((ParameterizedTypeTree) curTypeArg).getType() instanceof AnnotatedTypeTree) {
annotatedType = (AnnotatedTypeTree) ((ParameterizedTypeTree) curTypeArg).getType();
}
List<? extends AnnotationTree> annotations =
annotatedType != null ? annotatedType.getAnnotations() : Collections.emptyList();
boolean hasNullableAnnotation = false;
for (AnnotationTree annotation : annotations) {
if (ASTHelpers.isSameType(
nullableType, ASTHelpers.getType(annotation.getAnnotationType()), state)) {
hasNullableAnnotation = true;
break;
}
}
// construct a TypeMetadata object containing a nullability annotation if needed
com.sun.tools.javac.util.List<Attribute.TypeCompound> nullableAnnotationCompound =
hasNullableAnnotation
? com.sun.tools.javac.util.List.from(
Collections.singletonList(
new Attribute.TypeCompound(
nullableType, com.sun.tools.javac.util.List.nil(), null)))
: com.sun.tools.javac.util.List.nil();
TypeMetadata typeMetadata =
new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound));
Type currentTypeArgType = curTypeArg.accept(this, null);
Type newTypeArgType = currentTypeArgType.cloneWithMetadata(typeMetadata);
newTypeArgs.add(newTypeArgType);
}
Type.ClassType finalType =
new Type.ClassType(
type.getEnclosingType(), com.sun.tools.javac.util.List.from(newTypeArgs), type.tsym);
return finalType;
}

/** By default, just use the type computed by javac */
@Override
protected Type defaultAction(Tree node, Void unused) {
return castToNonNull(ASTHelpers.getType(node));
}
}

/**
* Checks that type parameter nullability is consistent between an overriding method and the
* corresponding overridden method.
Expand Down Expand Up @@ -908,6 +964,7 @@ public String visitCapturedType(Type.CapturedType t, Void s) {

@Override
public String visitArrayType(Type.ArrayType t, Void unused) {
// TODO properly print cases like int @Nullable[]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also fix printing of these types in a follow-up

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #818

return t.elemtype.accept(this, null) + "[]";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -760,6 +760,82 @@ public void rawTypes() {
.doTest();
}

@Test
public void nestedGenericTypeAssignment() {
msridhar marked this conversation as resolved.
Show resolved Hide resolved
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" static class A<T extends @Nullable Object> { }",
" static void testPositive() {",
" // BUG: Diagnostic contains: Cannot assign from type",
" A<A<@Nullable String>[]> var = new A<A<String>[]>();",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding the:

A<A<String>[]> var = new A<A<@Nullable String>[]>();

and

A<A<String>[]> var = new A<A<String>[]>();

cases?

Copy link
Collaborator

@msridhar msridhar Aug 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

" }",
" static void testNegative() {",
" A<A<@Nullable String>[]> var = new A<A<@Nullable String>[]>();",
" }",
"}")
.doTest();
}

@Test
public void genericPrimitiveArrayTypeAssignment() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" static class A<T extends @Nullable Object> { }",
" static void testPositive() {",
" // BUG: Diagnostic contains: Cannot assign from type A<int[]>",
" A<int @Nullable[]> x = new A<int[]>();",
" }",
" static void testNegative() {",
" A<int @Nullable[]> x = new A<int @Nullable[]>();",
" }",
"}")
.doTest();
}

@Test
public void nestedGenericTypeVariables() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" static class A<T extends @Nullable Object> { }",
" static class B<T> {",
" void foo() {",
" A<A<T>[]> x = new A<A<T>[]>();",
" }",
" }",
"}")
.doTest();
}

@Test
public void nestedGenericWildcardTypeVariables() {
makeHelper()
.addSourceLines(
"Test.java",
"package com.uber;",
"import org.jspecify.annotations.Nullable;",
"class Test {",
" static class A<T extends @Nullable Object> { }",
" static class B<T> {",
" void foo() {",
" A<A<? extends String>[]> x = new A<A<? extends String>[]>();",
" }",
" }",
"}")
.doTest();
}

@Test
public void overrideReturnTypes() {
makeHelper()
Expand Down