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

Generics checks for method overriding #755

Merged
merged 57 commits into from
Aug 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
362ee2f
all changes for handling method overrides
NikitaAware Mar 31, 2023
4a690c2
Merge branch 'master' into try-method-match
msridhar Apr 5, 2023
ac1e7f8
code for pretty printing generic types
msridhar Apr 26, 2023
241793d
better printing of types in errors related to generic types
msridhar Apr 27, 2023
7c86f30
fix compile error on JDK 8
msridhar Apr 27, 2023
899f615
Merge branch 'generic-type-pretty' into try-method-match
msridhar Apr 27, 2023
90b0c69
fix
msridhar Apr 27, 2023
74837a1
cleanup
msridhar Apr 27, 2023
6e3f2d0
better error message
msridhar Apr 27, 2023
56cddbe
WIP
msridhar Apr 27, 2023
b9d698c
Merge branch 'master' into check-toolchains
msridhar Apr 27, 2023
56fb488
test
msridhar Apr 27, 2023
304e567
reorder
msridhar Apr 27, 2023
7381c6e
just windows
msridhar Apr 27, 2023
2689a62
Merge branch 'master' into generic-type-pretty
msridhar Apr 27, 2023
5d7e5af
Merge branch 'check-toolchains' into generic-type-pretty
msridhar Apr 27, 2023
570a03a
Merge branch 'generic-type-pretty' into try-method-match
msridhar Apr 27, 2023
7cdb1b8
Merge branch 'master' into generic-type-pretty
msridhar Apr 27, 2023
a2dbade
Merge branch 'generic-type-pretty' into try-method-match
msridhar Apr 27, 2023
608f3db
tweaks
msridhar Apr 28, 2023
39c6e7d
cleanup
msridhar Apr 28, 2023
ac24db5
more cleanup
msridhar Apr 28, 2023
eb1ecae
cleanup
msridhar Apr 28, 2023
3bbd1d6
docs
msridhar Apr 30, 2023
ee73b04
more
msridhar May 6, 2023
e56cb12
more
msridhar May 6, 2023
05782eb
more
msridhar May 6, 2023
90a4530
more
msridhar May 6, 2023
a769e30
cleanup tests
msridhar May 6, 2023
6630bbf
another rename
msridhar May 6, 2023
fb3f418
Merge branch 'master' into try-method-match
msridhar May 8, 2023
564c8dc
Merge branch 'master' into try-method-match
msridhar May 14, 2023
662606a
fix nullaway error
msridhar May 14, 2023
3de870c
Merge branch 'master' into try-method-match
msridhar Jun 14, 2023
f7d8dc9
Merge branch 'master' into try-method-match
msridhar Jun 14, 2023
08596f0
fix class ordering in test
msridhar Jun 14, 2023
97ec62a
bug fix: did not handle direct dereference of method call expression …
msridhar Jun 15, 2023
8bca16e
test interactions with @Contract
msridhar Jun 15, 2023
242b2e0
simplify using new API
msridhar Jun 15, 2023
631c417
better variable names
msridhar Jun 15, 2023
9024a67
rename variables and add a TODO
msridhar Jun 15, 2023
be06ff1
add a comment
msridhar Jun 15, 2023
bb848fa
Merge branch 'master' into try-method-match
msridhar Jun 21, 2023
83b82fb
Merge branch 'master' into try-method-match
msridhar Jun 23, 2023
bc3b181
Merge branch 'master' into try-method-match
msridhar Jul 8, 2023
df25816
Merge branch 'master' into try-method-match
msridhar Jul 15, 2023
9582cff
Merge branch 'master' into try-method-match
msridhar Jul 18, 2023
4afd3d9
Merge branch 'master' into try-method-match
msridhar Aug 11, 2023
aa2bc8c
Merge branch 'master' into try-method-match
msridhar Aug 11, 2023
0839689
clean up check
msridhar Aug 12, 2023
0dd0b87
rename type parameter
msridhar Aug 12, 2023
ee9ccd4
add true negative test
msridhar Aug 12, 2023
716e721
add true negative test
msridhar Aug 12, 2023
9e9797e
fix nullaway error
msridhar Aug 12, 2023
7ba7009
Add test for false negative
msridhar Aug 12, 2023
054aab2
rewrite condition for clarity
msridhar Aug 12, 2023
8090d47
refactor for clarity
msridhar Aug 12, 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
2 changes: 2 additions & 0 deletions nullaway/src/main/java/com/uber/nullaway/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public enum MessageTypes {
ASSIGN_GENERIC_NULLABLE,
RETURN_NULLABLE_GENERIC,
PASS_NULLABLE_GENERIC,
WRONG_OVERRIDE_RETURN_GENERIC,
WRONG_OVERRIDE_PARAM_GENERIC,
}

public String getMessage() {
Expand Down
290 changes: 289 additions & 1 deletion nullaway/src/main/java/com/uber/nullaway/GenericsChecks.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.Tree;
Expand Down Expand Up @@ -190,6 +193,38 @@ private void reportInvalidParametersNullabilityError(
errorMessage, analysis.buildDescription(paramExpression), state, null));
}

private void reportInvalidOverridingMethodReturnTypeError(
Tree methodTree, Type overriddenMethodReturnType, Type overridingMethodReturnType) {
ErrorBuilder errorBuilder = analysis.getErrorBuilder();
ErrorMessage errorMessage =
new ErrorMessage(
ErrorMessage.MessageTypes.WRONG_OVERRIDE_RETURN_GENERIC,
"Method returns "
+ prettyTypeForError(overridingMethodReturnType)
+ ", but overridden method returns "
+ prettyTypeForError(overriddenMethodReturnType)
+ ", which has mismatched type parameter nullability");
state.reportMatch(
errorBuilder.createErrorDescription(
errorMessage, analysis.buildDescription(methodTree), state, null));
}

private void reportInvalidOverridingMethodParamTypeError(
Tree formalParameterTree, Type typeParameterType, Type methodParamType) {
ErrorBuilder errorBuilder = analysis.getErrorBuilder();
ErrorMessage errorMessage =
new ErrorMessage(
ErrorMessage.MessageTypes.WRONG_OVERRIDE_PARAM_GENERIC,
"Parameter has type "
+ prettyTypeForError(methodParamType)
+ ", but overridden method has parameter type "
+ prettyTypeForError(typeParameterType)
+ ", which has mismatched type parameter nullability");
state.reportMatch(
errorBuilder.createErrorDescription(
errorMessage, analysis.buildDescription(formalParameterTree), state, null));
}

/**
* This method returns the type of the given tree, including any type use annotations.
*
Expand Down Expand Up @@ -366,7 +401,6 @@ private Type.ClassType typeWithPreservedAnnotations(ParameterizedTypeTree tree)
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);
Expand All @@ -380,6 +414,7 @@ private Type.ClassType typeWithPreservedAnnotations(ParameterizedTypeTree tree)
}
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)) {
Expand Down Expand Up @@ -512,6 +547,259 @@ public void compareGenericTypeParameterNullabilityForCall(
}
}

/**
* Checks that type parameter nullability is consistent between an overriding method and the
* corresponding overridden method.
*
* @param tree A method tree to check
* @param overridingMethod A symbol of the overriding method
* @param overriddenMethod A symbol of the overridden method
*/
public void checkTypeParameterNullnessForMethodOverriding(
MethodTree tree, Symbol.MethodSymbol overridingMethod, Symbol.MethodSymbol overriddenMethod) {
if (!config.isJSpecifyMode()) {
return;
}
// Obtain type parameters for the overridden method within the context of the overriding
// method's class
Type methodWithTypeParams =
state.getTypes().memberType(overridingMethod.owner.type, overriddenMethod);

checkTypeParameterNullnessForOverridingMethodReturnType(tree, methodWithTypeParams);
checkTypeParameterNullnessForOverridingMethodParameterType(tree, methodWithTypeParams);
}

/**
* Computes the nullability of the return type of some generic method when seen as a member of
* some class {@code C}, based on type parameter nullability within {@code C}.
*
* <p>Consider the following example:
*
* <pre>
* interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
* R apply(P p);
* }
* class C implements Fn<String, @Nullable String> {
* public @Nullable String apply(String p) {
* return null;
* }
* }
* </pre>
*
* Within the context of class {@code C}, the method {@code Fn.apply} has a return type of
* {@code @Nullable String}, since {@code @Nullable String} is passed as the type parameter for
* {@code R}. Hence, it is valid for overriding method {@code C.apply} to return {@code @Nullable
* String}.
*
* @param method the generic method
* @param enclosingType the enclosing type in which we want to know {@code method}'s return type
* nullability
* @param state Visitor state
* @param config The analysis config
* @return nullability of the return type of {@code method} in the context of {@code
* enclosingType}
*/
public static Nullness getGenericMethodReturnTypeNullness(
Symbol.MethodSymbol method, Type enclosingType, VisitorState state, Config config) {
Type overriddenMethodType = state.getTypes().memberType(enclosingType, method);
if (!(overriddenMethodType instanceof Type.MethodType)) {
throw new RuntimeException("expected method type but instead got " + overriddenMethodType);
}
return getTypeNullness(overriddenMethodType.getReturnType(), config);
}

/**
* Computes the nullness of the return of a generic method at an invocation, in the context of the
* declared type of its receiver argument. If the return type is a type variable, its nullness
* depends on the nullability of the corresponding type parameter in the receiver's type.
*
* <p>Consider the following example:
*
* <pre>
* interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
* R apply(P p);
* }
* class C implements Fn<String, @Nullable String> {
* public @Nullable String apply(String p) {
* return null;
* }
* }
* static void m() {
* Fn<String, @Nullable String> f = new C();
* f.apply("hello").hashCode(); // NPE
* }
* </pre>
*
* The declared type of {@code f} passes {@code Nullable String} as the type parameter for type
* variable {@code R}. So, the call {@code f.apply("hello")} returns {@code @Nullable} and an
* error should be reported.
*
* @param invokedMethodSymbol symbol for the invoked method
* @param tree the tree for the invocation
* @return Nullness of invocation's return type, or {@code NONNULL} if the call does not invoke an
* instance method
*/
public static Nullness getGenericReturnNullnessAtInvocation(
Symbol.MethodSymbol invokedMethodSymbol,
MethodInvocationTree tree,
VisitorState state,
Config config) {
if (!(tree.getMethodSelect() instanceof MemberSelectTree)) {
return Nullness.NONNULL;
msridhar marked this conversation as resolved.
Show resolved Hide resolved
}
Type methodReceiverType =
castToNonNull(
ASTHelpers.getType(((MemberSelectTree) tree.getMethodSelect()).getExpression()));
return getGenericMethodReturnTypeNullness(
invokedMethodSymbol, methodReceiverType, state, config);
}

/**
* Computes the nullness of a formal parameter of a generic method at an invocation, in the
* context of the declared type of its receiver argument. If the formal parameter's type is a type
* variable, its nullness depends on the nullability of the corresponding type parameter in the
* receiver's type.
*
* <p>Consider the following example:
*
* <pre>
* interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
* R apply(P p);
* }
* class C implements Fn<@Nullable String, String> {
* public String apply(@Nullable String p) {
* return "";
* }
* }
* static void m() {
* Fn<@Nullable String, String> f = new C();
* f.apply(null);
* }
* </pre>
*
* The declared type of {@code f} passes {@code Nullable String} as the type parameter for type
* variable {@code P}. So, it is legal to pass {@code null} as a parameter to {@code f.apply}.
*
* @param paramIndex parameter index
* @param invokedMethodSymbol symbol for the invoked method
* @param tree the tree for the invocation
* @return Nullness of parameter at {@code paramIndex}, or {@code NONNULL} if the call does not
* invoke an instance method
*/
public Nullness getGenericParameterNullnessAtInvocation(
int paramIndex, Symbol.MethodSymbol invokedMethodSymbol, MethodInvocationTree tree) {
if (!(tree.getMethodSelect() instanceof MemberSelectTree)) {
msridhar marked this conversation as resolved.
Show resolved Hide resolved
return Nullness.NONNULL;
}
Type methodReceiverType =
castToNonNull(
ASTHelpers.getType(((MemberSelectTree) tree.getMethodSelect()).getExpression()));
return getGenericMethodParameterNullness(paramIndex, invokedMethodSymbol, methodReceiverType);
}

/**
* Computes the nullability of a parameter type of some generic method when seen as a member of
* some class {@code C}, based on type parameter nullability within {@code C}.
*
* <p>Consider the following example:
*
* <pre>
* interface Fn<P extends @Nullable Object, R extends @Nullable Object> {
* R apply(P p);
* }
* class C implements Fn<@Nullable String, String> {
* public String apply(@Nullable String p) {
* return "";
* }
* }
* </pre>
*
* Within the context of class {@code C}, the method {@code Fn.apply} has a parameter type of
* {@code @Nullable String}, since {@code @Nullable String} is passed as the type parameter for
* {@code P}. Hence, overriding method {@code C.apply} must take a {@code @Nullable String} as a
* parameter.
*
* @param parameterIndex index of the parameter
* @param method the generic method
* @param enclosingType the enclosing type in which we want to know {@code method}'s parameter
* type nullability
* @return nullability of the relevant parameter type of {@code method} in the context of {@code
* enclosingType}
*/
public Nullness getGenericMethodParameterNullness(
int parameterIndex, Symbol.MethodSymbol method, Type enclosingType) {
Type methodType = state.getTypes().memberType(enclosingType, method);
Type paramType = methodType.getParameterTypes().get(parameterIndex);
return getTypeNullness(paramType, config);
}

/**
* This method compares the type parameter annotations for overriding method parameters with
* corresponding type parameters for the overridden method and reports an error if they don't
* match
*
* @param tree tree for overriding method
* @param overriddenMethodType type of the overridden method
*/
private void checkTypeParameterNullnessForOverridingMethodParameterType(
MethodTree tree, Type overriddenMethodType) {
List<? extends VariableTree> methodParameters = tree.getParameters();
List<Type> overriddenMethodParameterTypes = overriddenMethodType.getParameterTypes();
// TODO handle varargs; they are not handled for now
for (int i = 0; i < methodParameters.size(); i++) {
Type overridingMethodParameterType = ASTHelpers.getType(methodParameters.get(i));
Type overriddenMethodParameterType = overriddenMethodParameterTypes.get(i);
if (overriddenMethodParameterType instanceof Type.ClassType
&& overridingMethodParameterType instanceof Type.ClassType) {
if (!compareNullabilityAnnotations(
(Type.ClassType) overriddenMethodParameterType,
(Type.ClassType) overridingMethodParameterType)) {
reportInvalidOverridingMethodParamTypeError(
methodParameters.get(i),
overriddenMethodParameterType,
overridingMethodParameterType);
}
}
}
}

/**
* This method compares the type parameter annotations for an overriding method's return type with
* corresponding type parameters for the overridden method and reports an error if they don't
* match
*
* @param tree tree for overriding method
* @param overriddenMethodType type of the overridden method
*/
private void checkTypeParameterNullnessForOverridingMethodReturnType(
MethodTree tree, Type overriddenMethodType) {
Type overriddenMethodReturnType = overriddenMethodType.getReturnType();
Type overridingMethodReturnType = ASTHelpers.getType(tree.getReturnType());
if (!(overriddenMethodReturnType instanceof Type.ClassType)) {
return;
}
Preconditions.checkArgument(overridingMethodReturnType instanceof Type.ClassType);
if (!compareNullabilityAnnotations(
(Type.ClassType) overriddenMethodReturnType, (Type.ClassType) overridingMethodReturnType)) {
reportInvalidOverridingMethodReturnTypeError(
tree, overriddenMethodReturnType, overridingMethodReturnType);
}
}

/**
* @param type A type for which we need the Nullness.
* @param config The analysis config
* @return Returns the Nullness of the type based on the Nullability annotation.
*/
private static Nullness getTypeNullness(Type type, Config config) {
boolean hasNullableAnnotation =
Nullness.hasNullableAnnotation(type.getAnnotationMirrors().stream(), config);
if (hasNullableAnnotation) {
return Nullness.NULLABLE;
}
return Nullness.NONNULL;
}

/**
* Returns a pretty-printed representation of type suitable for error messages. The representation
* uses simple names rather than fully-qualified names, and retains all type-use annotations.
Expand Down
Loading