package org.checkerframework.checker.nullness;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
import java.util.Collection;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import org.checkerframework.framework.type.AnnotatedTypeMirror;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.util.AnnotatedTypes;
import org.checkerframework.javacutil.TreeUtils;
/**
* Handles calls to {@link java.util.Collection#toArray()} and determines the appropriate nullness
* type of the returned value.
*
* <p>The semantics of {@link Collection#toArray()} and {@link Collection#toArray(Object[])
* Collection.toArray(T[])} cannot be captured by the regular type system. Namely, the nullness of
* the returned array component depends on the receiver type argument. So
*
* <pre>{@code
* Collection<@NonNull String> c1 = ...;
* c1.toArray(); // returns @NonNull Object []
*
* Collection<@Nullable String> c2 = ...;
* c2.toArray(); // returns @Nullable Object []
* }</pre>
*
* In the case of {@link Collection#toArray(Object[]) Collection.toArray(T[])}, the type of the
* returned array depends on the passed parameter as well and its size. In particular, the returned
* array component would be of type {@code @NonNull} if the following conditions hold:
*
* <ol>
* <li value="1">The receiver collection type argument is {@code NonNull}
* <li value="2">The passed array size is less than the collection size
* </ol>
*
* While checking for the second condition, requires a runtime check, we provide heuristics to
* handle the most common cases of {@link Collection#toArray(Object[]) Collection.toArray(T[])},
* namely if the passed array is
*
* <ol>
* <li value="1">an empty array initializer, e.g. {@code c.toArray(new String[] { })},
* <li value="2">array creation tree of size 0, e.g. {@code c.toArray(new String[0])}, or
* <li value="3">array creation tree of the collection size method invocation {@code c.toArray(new
* String[c.size()])}
* </ol>
*
* Note: The nullness of the returned array doesn't depend on the passed array nullness.
*/
public class CollectionToArrayHeuristics {
private final ProcessingEnvironment processingEnv;
private final NullnessAnnotatedTypeFactory atypeFactory;
private final ExecutableElement collectionToArrayObject;
private final ExecutableElement collectionToArrayE;
private final ExecutableElement size;
private final AnnotatedDeclaredType collectionType;
public CollectionToArrayHeuristics(
ProcessingEnvironment env, NullnessAnnotatedTypeFactory factory) {
this.processingEnv = env;
this.atypeFactory = factory;
this.collectionToArrayObject =
TreeUtils.getMethod(java.util.Collection.class.getName(), "toArray", 0, env);
this.collectionToArrayE =
TreeUtils.getMethod(java.util.Collection.class.getName(), "toArray", 1, env);
this.size = TreeUtils.getMethod(java.util.Collection.class.getName(), "size", 0, env);
this.collectionType =
factory.fromElement(env.getElementUtils().getTypeElement("java.util.Collection"));
}
/**
* Apply the heuristics to the given method invocation and corresponding {@link
* Collection#toArray()} type.
*
* <p>If the method invocation is a call to {@code toArray}, then it manipulates the returned
* type of {@code method} arg to contain the appropriate nullness. Otherwise, it does nothing.
*
* @param tree method invocation tree
* @param method invoked method type
*/
public void handle(MethodInvocationTree tree, AnnotatedExecutableType method) {
if (TreeUtils.isMethodInvocation(tree, collectionToArrayObject, processingEnv)) {
// simple case of collection.toArray()
boolean receiver = isNonNullReceiver(tree);
setComponentNullness(receiver, method.getReturnType());
} else if (TreeUtils.isMethodInvocation(tree, collectionToArrayE, processingEnv)) {
assert !tree.getArguments().isEmpty() : tree;
Tree argument = tree.getArguments().get(0);
boolean isArrayCreation =
isHandledArrayCreation(argument, receiver(tree.getMethodSelect()));
boolean receiver = isNonNullReceiver(tree);
setComponentNullness(receiver && isArrayCreation, method.getReturnType());
// TODO: we need a mechanism to prevent nullable collections
// from inserting null elements into a nonnull arrays
if (!receiver) {
setComponentNullness(false, method.getParameterTypes().get(0));
}
}
}
/**
* Sets the nullness of the component of the array type.
*
* @param isNonNull indicates which annotation ({@code NonNull} or {@code Nullable}) should be
* inserted
* @param type the array type
*/
private void setComponentNullness(boolean isNonNull, AnnotatedTypeMirror type) {
assert type.getKind() == TypeKind.ARRAY;
AnnotatedTypeMirror compType = ((AnnotatedArrayType) type).getComponentType();
compType.replaceAnnotation(isNonNull ? atypeFactory.NONNULL : atypeFactory.NULLABLE);
}
/**
* Returns true if {@code argument} is one of the array creation trees that the heuristic
* handles.
*
* @param argument the tree passed to {@link Collection#toArray(Object[])
* Collection.toArray(T[])}
* @param receiver the name of the receiver collection
* @return true if the argument is handled and assume to return nonnull elements
*/
private boolean isHandledArrayCreation(Tree argument, String receiver) {
if (argument.getKind() != Tree.Kind.NEW_ARRAY) {
return false;
}
NewArrayTree newArr = (NewArrayTree) argument;
// case 1: empty array initializer
if (newArr.getInitializers() != null) {
return newArr.getInitializers().isEmpty();
}
assert !newArr.getDimensions().isEmpty();
Tree dimension = newArr.getDimensions().get(newArr.getDimensions().size() - 1);
// case 2: 0-length array creation
if (dimension.toString().equals("0")) {
return true;
}
// case 3: size()-length array creation
if (TreeUtils.isMethodInvocation(dimension, size, processingEnv)) {
MethodInvocationTree invok = (MethodInvocationTree) dimension;
String invokReceiver = receiver(invok.getMethodSelect());
return invokReceiver.equals(receiver);
}
return false;
}
/**
* Returns {@code true} if the method invocation tree receiver is collection who is known to
* contain non-null elements (i.e. its type argument is a {@code NonNull}.
*/
private boolean isNonNullReceiver(MethodInvocationTree tree) {
// check receiver
AnnotatedTypeMirror receiver = atypeFactory.getReceiverType(tree);
AnnotatedDeclaredType collection =
AnnotatedTypes.asSuper(atypeFactory, receiver, collectionType);
if (collection.getTypeArguments().isEmpty()
|| !collection
.getTypeArguments()
.get(0)
.hasEffectiveAnnotation(atypeFactory.NONNULL)) {
return false;
}
return true;
}
/**
* The name of the receiver object of the tree.
*
* @param tree either an identifier tree or a member select tree
*/
// This method is quite sloppy, but works most of the time
private String receiver(Tree tree) {
if (tree.getKind() == Tree.Kind.MEMBER_SELECT) {
return ((MemberSelectTree) tree).getExpression().toString();
} else {
return "this";
}
}
}