package checkers.nullness;
import java.util.Collection;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter;
import checkers.types.*;
import checkers.types.AnnotatedTypeMirror.*;
import checkers.util.TreeUtils;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.Tree;
/**
* Handles calls to {@link java.util.Collection#toArray()} and determines
* the appropriate nullness type of the returned value.
*
* <p>
* {@link Collection#toArray()} and {@link Collection#toArray(Object[])
* Collection.toArray(T[])} method semantics cannot be captured by the
* regular type system. Namely, the nullness of the returned array
* component depends on the receiver type argument. So
*
* <pre>
* 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 of type {@code @NonNull} if the following
* conditions hold:
*
* <ol>
* <li value="1">The receiver collection type argument is {@code NonNull}</li>
* <li value="2">The passed array size is less than the collection size</li>
* </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>
* <li value="2">array creation tree of size 0, e.g.
* {@code c.toArray(new String[0])}, or</li>
* <li value="3">array creation tree of the collection size method invocation
* {@code c.toArray(new String[c.size()])}</li>
* </ol>
*
* Note: The nullness of the returned array doesn't depend on the passed
* array nullness.
*/
public class CollectionToArrayHeuristics {
private final ProcessingEnvironment env;
private final NullnessAnnotatedTypeFactory factory;
private final AnnotatedTypes atypes;
private final ExecutableElement collectionToArrayObject;
private final ExecutableElement collectionToArrayE;
private final ExecutableElement size;
private final AnnotatedDeclaredType collectionType;
public CollectionToArrayHeuristics(ProcessingEnvironment env,
NullnessAnnotatedTypeFactory factory) {
this.env = env;
this.factory = factory;
this.atypes = new AnnotatedTypes(env, factory);
this.collectionToArrayObject = getMethod("java.util.Collection", "toArray", 0);
this.collectionToArrayE = getMethod("java.util.Collection", "toArray", 1);
this.size = getMethod("java.util.Collection", "size", 0);
this.collectionType = factory.fromElement(env.getElementUtils().getTypeElement("java.util.Collection"));
}
/**
* Apply the heuristics to the given method invocation and corresponding
* {@link Collection#toArray()} type.
*
* 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 (isMethod(tree, collectionToArrayObject)) {
// simple case of collection.toArray()
boolean receiver = isNonNullReceiver(tree);
setComponentNullness(receiver, method.getReturnType());
} else if (isMethod(tree, collectionToArrayE)) {
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.clearAnnotations();
compType.addAnnotation(isNonNull ? factory.NONNULL : factory.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 (isMethod(dimension, size)) {
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 = factory.getReceiver(tree);
AnnotatedDeclaredType collection = (AnnotatedDeclaredType)atypes.asSuper(receiver, collectionType);
assert collection != null;
if (collection.getTypeArguments().isEmpty()
|| !collection.getTypeArguments().get(0).hasAnnotation(factory.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";
}
// TODO: duplicated code from MapGetHeuristics
private boolean isMethod(Tree tree, ExecutableElement method) {
if (!(tree instanceof MethodInvocationTree))
return false;
MethodInvocationTree methInvok = (MethodInvocationTree)tree;
ExecutableElement invoked = TreeUtils.elementFromUse(methInvok);
return isMethod(invoked, method);
}
private boolean isMethod(ExecutableElement questioned, ExecutableElement method) {
return (questioned.equals(method)
|| env.getElementUtils().overrides(questioned, method,
(TypeElement)questioned.getEnclosingElement()));
}
private ExecutableElement getMethod(String typeName, String methodName, int params) {
TypeElement mapElt = env.getElementUtils().getTypeElement(typeName);
for (ExecutableElement exec : ElementFilter.methodsIn(mapElt.getEnclosedElements())) {
if (exec.getSimpleName().contentEquals(methodName)
&& exec.getParameters().size() == params)
return exec;
}
throw new RuntimeException("Shouldn't be here!");
}
}