-
Notifications
You must be signed in to change notification settings - Fork 299
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
Changes from 7 commits
a0b76ea
b2c533a
6ae742b
15e1601
9fae10c
47dd926
f5b6c1d
e9d969b
20d747a
0d3c2f0
4273601
9e96103
d1e61bf
d2db3cc
d7c9812
aed09fd
1a12cef
a4bd9bc
680a17c
d314952
b9eb8fc
989a755
4e5adf0
657f38e
6556379
ce51560
2e14f16
6f5c722
b419103
394ff52
1438bbd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
|
@@ -334,57 +336,10 @@ public void checkTypeParameterNullnessForFunctionReturnType( | |
* @param lhsType type for the lhs of the assignment | ||
* @param rhsType type for the rhs of the assignment | ||
*/ | ||
private boolean compareNullabilityAnnotations(Type.ClassType lhsType, Type.ClassType 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.getTypeArguments().length() > 0) { | ||
if (!compareNullabilityAnnotations( | ||
(Type.ClassType) lhsTypeArgument, (Type.ClassType) rhsTypeArgument)) { | ||
return false; | ||
} | ||
} | ||
} | ||
return true; | ||
private boolean compareNullabilityAnnotations(Type lhsType, Type rhsType) { | ||
// it is fair to assume rhyType should be the same as lhsType as the Java compiler has passed | ||
// before NullAway. | ||
return lhsType.accept(COMPARE_NULLABILITY_VISITOR, rhsType); | ||
} | ||
|
||
/** | ||
|
@@ -433,12 +388,16 @@ private Type.ClassType typeWithPreservedAnnotations(ParameterizedTypeTree tree) | |
TypeMetadata typeMetadata = | ||
new TypeMetadata(new TypeMetadata.Annotations(nullableAnnotationCompound)); | ||
Type currentTypeArgType = castToNonNull(ASTHelpers.getType(curTypeArg)); | ||
if (currentTypeArgType.getTypeArguments().size() > 0) { | ||
List<Type> genericArgType = currentTypeArgType.accept(TYPE_ARG_VISITOR, null); | ||
if (genericArgType.size() > 0) { | ||
// nested generic type; recursively preserve its nullability type argument annotations | ||
currentTypeArgType = typeWithPreservedAnnotations((ParameterizedTypeTree) curTypeArg); | ||
// currentTypeArgType = typeWithPreservedAnnotations((ParameterizedTypeTree) curTypeArg); | ||
// visitor based approach | ||
currentTypeArgType = curTypeArg.accept(new PreservedAnnotationTreeVisitor(), null); | ||
} | ||
Type.ClassType newTypeArgType = | ||
(Type.ClassType) currentTypeArgType.cloneWithMetadata(typeMetadata); | ||
// Type.ClassType newTypeArgType = (Type.ClassType) | ||
// currentTypeArgType.cloneWithMetadata(typeMetadata); | ||
Type newTypeArgType = currentTypeArgType.cloneWithMetadata(typeMetadata); | ||
newTypeArgs.add(newTypeArgType); | ||
} | ||
Type.ClassType finalType = | ||
|
@@ -547,6 +506,173 @@ public void compareGenericTypeParameterNullabilityForCall( | |
} | ||
} | ||
|
||
/** To dispatch the logic to obtain the generic type arguments for different Types */ | ||
private final Type.Visitor<List<Type>, Void> TYPE_ARG_VISITOR = | ||
new Types.DefaultTypeVisitor<List<Type>, Void>() { | ||
@Override | ||
public List<Type> visitClassType(Type.ClassType t, Void s) { | ||
return t.getTypeArguments(); | ||
} | ||
|
||
@Override | ||
public List<Type> visitArrayType(Type.ArrayType t, Void unused) { | ||
Type.ClassType classArg = (Type.ClassType) t.getComponentType(); | ||
return classArg.getTypeArguments(); | ||
} | ||
|
||
@Override | ||
public List<Type> visitCapturedType(Type.CapturedType t, Void s) { | ||
return t.wildcard.accept(this, null); | ||
} | ||
|
||
@Override | ||
public List<Type> visitType(Type t, Void unused) { | ||
return Collections.emptyList(); | ||
} | ||
}; | ||
|
||
/** To dispatch the logic to0 obtain the generic type arguments for different Types */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment seems wrong There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it is wrong, it should be fixed in the newer commits, Thanks! |
||
private final Type.Visitor<Boolean, Type> COMPARE_NULLABILITY_VISITOR = | ||
new Types.DefaultTypeVisitor<Boolean, Type>() { | ||
@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 | ||
List<Type> genericArgs = lhsTypeArgument.accept(TYPE_ARG_VISITOR, null); | ||
if (genericArgs.size() > 0) { | ||
if (!lhsTypeArgument.accept(COMPARE_NULLABILITY_VISITOR, rhsTypeArgument)) { | ||
return false; | ||
} | ||
} | ||
} | ||
return true; | ||
} | ||
|
||
@Override | ||
public Boolean visitArrayType(Type.ArrayType lhsType, Type rhsType) { | ||
Type.ArrayType arrRhsType = (Type.ArrayType) rhsType; | ||
return lhsType | ||
.getComponentType() | ||
.accept(COMPARE_NULLABILITY_VISITOR, arrRhsType.getComponentType()); | ||
} | ||
|
||
@Override | ||
@Nullable | ||
public Boolean visitType(Type t, Type type) { | ||
return null; | ||
akulk022 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
}; | ||
|
||
/** Visitor For Handling Different Tree Types */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need a more descriptive comment There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, I have added a more descriptive comment |
||
public class PreservedAnnotationTreeVisitor extends SimpleTreeVisitor<Type, Boolean> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be Void, I do not recall why it was Boolean but it certainly doesn't make sense now, Thanks for catching that. |
||
@Override | ||
public Type visitArrayType(ArrayTypeTree tree, Boolean p) { | ||
ParameterizedTypeTree paramTree = (ParameterizedTypeTree) tree.getType(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure, I tried running a test with a regular ArrayType inside the generic like A<String[]> and that ran fine. Maybe its better to just do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you try a type like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It gives a ClassCastException in the TYPE_ARG_VISITOR since it cannot cast PrimitiveType to ClassType There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok then we need a fix somewhere. Please add that as another test case as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was a simple fix to not cast it into ClassType on those lines, as far as this piece of code is concerned, I just realised that we do not accept this Visitor unless we have generic type arguments so we never even reach this method in those situations. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what the latest is here, but let's see what happens after we add the other tests I suggested |
||
Type.ClassType classType = | ||
(Type.ClassType) paramTree.accept(new PreservedAnnotationTreeVisitor(), null); | ||
return new Type.ArrayType(classType, classType.tsym); | ||
} | ||
|
||
@Override | ||
public Type visitParameterizedType(ParameterizedTypeTree tree, Boolean 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<>(); | ||
boolean hasNullableAnnotation = false; | ||
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(); | ||
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)); | ||
List<Type> genericArgType = currentTypeArgType.accept(TYPE_ARG_VISITOR, null); | ||
if (genericArgType.size() > 0) { | ||
// nested generic type; recursively preserve its nullability type argument annotations | ||
currentTypeArgType = curTypeArg.accept(new PreservedAnnotationTreeVisitor(), null); | ||
} | ||
// Type.ClassType newTypeArgType = (Type.ClassType) | ||
// currentTypeArgType.cloneWithMetadata(typeMetadata); | ||
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; | ||
} | ||
} | ||
|
||
/** | ||
* Checks that type parameter nullability is consistent between an overriding method and the | ||
* corresponding overridden method. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -760,6 +760,26 @@ 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>[]>();", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about adding the:
and
cases? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 overrideReturnTypes() { | ||
makeHelper() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming we agree with my suggestion above, this visitor can be deleted