blob: 1f2fcab099c4b31bdf3552e92ede45a3989414dd [file] [log] [blame]
package org.checkerframework.checker.nullness;
import com.sun.source.tree.ExpressionTree;
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 java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import org.checkerframework.common.basetype.BaseTypeChecker;
import org.checkerframework.common.value.qual.ArrayLen;
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.AnnotationUtils;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.TreeUtils;
/**
* Determines the nullness type of calls to {@link java.util.Collection#toArray()}.
*
* @checker_framework.manual #nullness-collection-toarray Nullness and conversions from collections
* to arrays
* @checker_framework.manual #constant-value-checker Constant Value Checker
*/
public class CollectionToArrayHeuristics {
/** The processing environment. */
private final ProcessingEnvironment processingEnv;
/** The checker, used for issuing diagnostic messages. */
private final BaseTypeChecker checker;
/** The type factory. */
private final NullnessAnnotatedTypeFactory atypeFactory;
/** Whether to trust {@code @ArrayLen(0)} annotations. */
private final boolean trustArrayLenZero;
/** The Collection type. */
private final AnnotatedDeclaredType collectionType;
/** The Collection.toArray(T[]) method. */
private final ExecutableElement collectionToArrayE;
/** The Collection.size() method. */
private final ExecutableElement size;
/** The ArrayLen.value field/element. */
private final ExecutableElement arrayLenValueElement;
/**
* Create a CollectionToArrayHeuristics.
*
* @param checker the checker, used for issuing diagnostic messages
* @param factory the type factory
*/
public CollectionToArrayHeuristics(
BaseTypeChecker checker, NullnessAnnotatedTypeFactory factory) {
this.processingEnv = checker.getProcessingEnvironment();
this.checker = checker;
this.atypeFactory = factory;
this.collectionType =
factory.fromElement(ElementUtils.getTypeElement(processingEnv, Collection.class));
this.collectionToArrayE =
TreeUtils.getMethod("java.util.Collection", "toArray", processingEnv, "T[]");
this.size = TreeUtils.getMethod("java.util.Collection", "size", 0, processingEnv);
this.arrayLenValueElement = TreeUtils.getMethod(ArrayLen.class, "value", 0, processingEnv);
this.trustArrayLenZero =
checker.getLintOption(
NullnessChecker.LINT_TRUSTARRAYLENZERO, NullnessChecker.LINT_DEFAULT_TRUSTARRAYLENZERO);
}
/**
* 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, collectionToArrayE, processingEnv)) {
assert !tree.getArguments().isEmpty() : tree;
ExpressionTree argument = tree.getArguments().get(0);
boolean receiverIsNonNull = receiverIsCollectionOfNonNullElements(tree);
boolean argIsHandled =
isHandledArrayCreation(argument, receiverName(tree.getMethodSelect()))
|| (trustArrayLenZero && isArrayLenZeroFieldAccess(argument));
setComponentNullness(receiverIsNonNull && argIsHandled, method.getReturnType());
// TODO: We need a mechanism to prevent nullable collections
// from inserting null elements into a nonnull arrays.
if (!receiverIsNonNull) {
setComponentNullness(false, method.getParameterTypes().get(0));
}
if (receiverIsNonNull && !argIsHandled) {
if (argument.getKind() != Tree.Kind.NEW_ARRAY) {
checker.reportWarning(tree, "toarray.nullable.elements.not.newarray");
} else {
checker.reportWarning(tree, "toarray.nullable.elements.mismatched.size");
}
}
}
}
/**
* 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 expression for 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;
// empty array initializer
if (newArr.getInitializers() != null) {
return newArr.getInitializers().isEmpty();
}
assert !newArr.getDimensions().isEmpty();
Tree dimension = newArr.getDimensions().get(newArr.getDimensions().size() - 1);
// 0-length array creation
if (dimension.toString().equals("0")) {
return true;
}
// size()-length array creation
if (TreeUtils.isMethodInvocation(dimension, size, processingEnv)) {
MethodInvocationTree invok = (MethodInvocationTree) dimension;
String invokReceiver = receiverName(invok.getMethodSelect());
return invokReceiver.equals(receiver);
}
return false;
}
/**
* Returns true if the argument is a field access expression, where the field has declared type
* {@code @ArrayLen(0)}.
*
* @param argument an expression tree
* @return true if the argument is a field access expression, where the field has declared type
* {@code @ArrayLen(0)}
*/
private boolean isArrayLenZeroFieldAccess(ExpressionTree argument) {
Element el = TreeUtils.elementFromUse(argument);
if (el != null && el.getKind().isField()) {
TypeMirror t = ElementUtils.getType(el);
if (t.getKind() == TypeKind.ARRAY) {
List<? extends AnnotationMirror> ams = t.getAnnotationMirrors();
for (AnnotationMirror am : ams) {
if (atypeFactory.areSameByClass(am, ArrayLen.class)) {
List<Integer> lens =
AnnotationUtils.getElementValueArray(am, arrayLenValueElement, Integer.class);
if (lens.size() == 1 && lens.get(0) == 0) {
return true;
}
}
}
}
}
return false;
}
/**
* Returns {@code true} if the method invocation tree receiver is collection that contains
* non-null elements (i.e. its type argument is {@code @NonNull}.
*
* @param tree a method invocation
* @return true if the receiver is a collection of non-null elements
*/
private boolean receiverIsCollectionOfNonNullElements(MethodInvocationTree tree) {
// check receiver
AnnotatedTypeMirror receiver = atypeFactory.getReceiverType(tree);
AnnotatedDeclaredType collection =
AnnotatedTypes.asSuper(atypeFactory, receiver, collectionType);
if (collection.getTypeArguments().isEmpty() // raw type
|| !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 receiverName(Tree tree) {
if (tree.getKind() == Tree.Kind.MEMBER_SELECT) {
return ((MemberSelectTree) tree).getExpression().toString();
} else {
return "this";
}
}
}