package org.checkerframework.checker.nullness; import com.sun.source.tree.AnnotatedTypeTree; import com.sun.source.tree.ArrayAccessTree; import com.sun.source.tree.AssertTree; import com.sun.source.tree.BinaryTree; import com.sun.source.tree.CatchTree; import com.sun.source.tree.CompoundAssignmentTree; import com.sun.source.tree.ConditionalExpressionTree; import com.sun.source.tree.DoWhileLoopTree; import com.sun.source.tree.EnhancedForLoopTree; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.ForLoopTree; import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.IfTree; import com.sun.source.tree.LiteralTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewArrayTree; import com.sun.source.tree.NewClassTree; import com.sun.source.tree.SwitchTree; import com.sun.source.tree.SynchronizedTree; import com.sun.source.tree.ThrowTree; import com.sun.source.tree.Tree; import com.sun.source.tree.Tree.Kind; import com.sun.source.tree.TypeCastTree; import com.sun.source.tree.UnaryTree; import com.sun.source.tree.VariableTree; import com.sun.source.tree.WhileLoopTree; import java.lang.annotation.Annotation; import java.util.Set; 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.TypeMirror; import org.checkerframework.checker.compilermsgs.qual.CompilerMessageKey; import org.checkerframework.checker.initialization.InitializationVisitor; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.common.basetype.BaseTypeChecker; import org.checkerframework.framework.flow.CFCFGBuilder; import org.checkerframework.framework.source.Result; 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.type.AnnotatedTypeMirror.AnnotatedPrimitiveType; import org.checkerframework.framework.util.QualifierPolymorphism; import org.checkerframework.javacutil.AnnotationUtils; import org.checkerframework.javacutil.InternalUtils; import org.checkerframework.javacutil.TreeUtils; import org.checkerframework.javacutil.TypesUtils; /** The visitor for the nullness type-system. */ public class NullnessVisitor extends InitializationVisitor<NullnessAnnotatedTypeFactory, NullnessValue, NullnessStore> { // Error message keys // private static final /*@CompilerMessageKey*/ String ASSIGNMENT_TYPE_INCOMPATIBLE = "assignment.type.incompatible"; private static final /*@CompilerMessageKey*/ String UNBOXING_OF_NULLABLE = "unboxing.of.nullable"; private static final /*@CompilerMessageKey*/ String KNOWN_NONNULL = "known.nonnull"; private static final /*@CompilerMessageKey*/ String LOCKING_NULLABLE = "locking.nullable"; private static final /*@CompilerMessageKey*/ String THROWING_NULLABLE = "throwing.nullable"; private static final /*@CompilerMessageKey*/ String ACCESSING_NULLABLE = "accessing.nullable"; private static final /*@CompilerMessageKey*/ String CONDITION_NULLABLE = "condition.nullable"; private static final /*@CompilerMessageKey*/ String ITERATING_NULLABLE = "iterating.over.nullable"; private static final /*@CompilerMessageKey*/ String SWITCHING_NULLABLE = "switching.nullable"; private static final /*@CompilerMessageKey*/ String DEREFERENCE_OF_NULLABLE = "dereference.of.nullable"; // Annotation and type constants private final AnnotationMirror NONNULL, NULLABLE, MONOTONIC_NONNULL; private final TypeMirror stringType; /** The element for java.util.Collection.size(). */ private final ExecutableElement collectionSize; /** The element for java.util.Collection.toArray(T). */ private final ExecutableElement collectionToArray; public NullnessVisitor(BaseTypeChecker checker, boolean useFbc) { super(checker); NONNULL = atypeFactory.NONNULL; NULLABLE = atypeFactory.NULLABLE; MONOTONIC_NONNULL = atypeFactory.MONOTONIC_NONNULL; stringType = elements.getTypeElement("java.lang.String").asType(); ProcessingEnvironment env = checker.getProcessingEnvironment(); this.collectionSize = TreeUtils.getMethod(java.util.Collection.class.getName(), "size", 0, env); this.collectionToArray = TreeUtils.getMethod(java.util.Collection.class.getName(), "toArray", 1, env); checkForAnnotatedJdk(); } @Override public NullnessAnnotatedTypeFactory createTypeFactory() { // We need to directly access useFbc from the checker, because this method gets called // by the superclass constructor and a field in this class would not be initialized // yet. Oh the pain. return new NullnessAnnotatedTypeFactory( checker, ((AbstractNullnessChecker) checker).useFbc); } @Override public boolean isValidUse( AnnotatedDeclaredType declarationType, AnnotatedDeclaredType useType, Tree tree) { // At most a single qualifier on a type, ignoring a possible PolyAll // annotation. boolean foundInit = false; boolean foundNonNull = false; Set<Class<? extends Annotation>> initQuals = atypeFactory.getInitializationAnnotations(); Set<Class<? extends Annotation>> nonNullQuals = atypeFactory.getNullnessAnnotations(); for (AnnotationMirror anno : useType.getAnnotations()) { if (QualifierPolymorphism.isPolyAll(anno)) { // ok. } else if (containsSameIgnoringValues(initQuals, anno)) { if (foundInit) { return false; } foundInit = true; } else if (containsSameIgnoringValues(nonNullQuals, anno)) { if (foundNonNull) { return false; } foundNonNull = true; } } if (tree.getKind() == Tree.Kind.VARIABLE) { Element vs = InternalUtils.symbol(tree); switch (vs.getKind()) { case EXCEPTION_PARAMETER: if (useType.hasAnnotation(NULLABLE)) { // Exception parameters cannot use Nullable // annotations. They default to NonNull. return false; } break; default: // nothing to do break; } } // The super implementation checks that useType is a subtype // of declarationType. However, declarationType by default // is NonNull, which would then forbid Nullable uses. // Therefore, don't perform this check. return true; } @Override public boolean isValidUse(AnnotatedPrimitiveType type, Tree tree) { if (tree.getKind() != Tree.Kind.TYPE_CAST && !type.hasAnnotation(NONNULL)) { // TODO: casts are sometimes inferred as @Nullable. // Find a way to correctly handle that case. return false; } return super.isValidUse(type, tree); } private boolean containsSameIgnoringValues( Set<Class<? extends Annotation>> quals, AnnotationMirror anno) { for (Class<? extends Annotation> q : quals) { if (AnnotationUtils.areSameByClass(anno, q)) { return true; } } return false; } @Override protected void commonAssignmentCheck( Tree varTree, ExpressionTree valueExp, /*@CompilerMessageKey*/ String errorKey) { // allow MonotonicNonNull to be initialized to null at declaration if (varTree.getKind() == Tree.Kind.VARIABLE) { Element elem = TreeUtils.elementFromDeclaration((VariableTree) varTree); if (atypeFactory.fromElement(elem).hasEffectiveAnnotation(MONOTONIC_NONNULL) && !checker.getLintOption( AbstractNullnessChecker.LINT_NOINITFORMONOTONICNONNULL, AbstractNullnessChecker.LINT_DEFAULT_NOINITFORMONOTONICNONNULL)) { return; } } super.commonAssignmentCheck(varTree, valueExp, errorKey); } @Override protected void commonAssignmentCheck( AnnotatedTypeMirror varType, ExpressionTree valueExp, /*@CompilerMessageKey*/ String errorKey) { // Use the valueExp as the context because data flow will have a value for that tree. // It might not have a value for the var tree. This is sound because // if data flow has determined @PolyNull is @Nullable at the RHS, then // it is also @Nullable for the LHS. atypeFactory.replacePolyQualifier(varType, valueExp); super.commonAssignmentCheck(varType, valueExp, errorKey); } @Override protected void commonAssignmentCheck( AnnotatedTypeMirror varType, AnnotatedTypeMirror valueType, Tree valueTree, /*@CompilerMessageKey*/ String errorKey) { if (TypesUtils.isPrimitive(varType.getUnderlyingType()) && !TypesUtils.isPrimitive(valueType.getUnderlyingType())) { boolean succeed = checkForNullability(valueType, valueTree, UNBOXING_OF_NULLABLE); if (!succeed) { // Only issue the unboxing of nullable error. return; } } super.commonAssignmentCheck(varType, valueType, valueTree, errorKey); } /** Case 1: Check for null dereferencing */ @Override public Void visitMemberSelect(MemberSelectTree node, Void p) { boolean isType = node.getExpression().getKind() == Kind.PARAMETERIZED_TYPE; if (!TreeUtils.isSelfAccess(node) && !isType) { checkForNullability(node.getExpression(), DEREFERENCE_OF_NULLABLE); } return super.visitMemberSelect(node, p); } /** Case 2: Check for implicit {@code .iterator} call */ @Override public Void visitEnhancedForLoop(EnhancedForLoopTree node, Void p) { checkForNullability(node.getExpression(), ITERATING_NULLABLE); return super.visitEnhancedForLoop(node, p); } /** Case 3: Check for array dereferencing */ @Override public Void visitArrayAccess(ArrayAccessTree node, Void p) { checkForNullability(node.getExpression(), ACCESSING_NULLABLE); return super.visitArrayAccess(node, p); } @Override public Void visitNewArray(NewArrayTree node, Void p) { AnnotatedArrayType type = atypeFactory.getAnnotatedType(node); AnnotatedTypeMirror componentType = type.getComponentType(); if (componentType.hasEffectiveAnnotation(NONNULL) && !isNewArrayAllZeroDims(node) && !isNewArrayInToArray(node) && !TypesUtils.isPrimitive(componentType.getUnderlyingType()) && checker.getLintOption("forbidnonnullarraycomponents", false)) { checker.report( Result.failure( "new.array.type.invalid", componentType.getAnnotations(), type.toString()), node); } return super.visitNewArray(node, p); } /** * Determine whether all dimensions given in a new array expression have zero as length. For * example "new Object[0][0];". Also true for empty dimensions, as in "new Object[] {...}". */ private static boolean isNewArrayAllZeroDims(NewArrayTree node) { boolean isAllZeros = true; for (ExpressionTree dim : node.getDimensions()) { if (dim instanceof LiteralTree) { Object val = ((LiteralTree) dim).getValue(); if (!(val instanceof Number) || !(Integer.valueOf(0).equals(val))) { isAllZeros = false; break; } } else { isAllZeros = false; break; } } return isAllZeros; } private boolean isNewArrayInToArray(NewArrayTree node) { if (node.getDimensions().size() != 1) { return false; } ExpressionTree dim = node.getDimensions().get(0); ProcessingEnvironment env = checker.getProcessingEnvironment(); if (!TreeUtils.isMethodInvocation(dim, collectionSize, env)) { return false; } ExpressionTree rcvsize = ((MethodInvocationTree) dim).getMethodSelect(); if (!(rcvsize instanceof MemberSelectTree)) { return false; } rcvsize = ((MemberSelectTree) rcvsize).getExpression(); if (!(rcvsize instanceof IdentifierTree)) { return false; } Tree encl = getCurrentPath().getParentPath().getLeaf(); if (!TreeUtils.isMethodInvocation(encl, collectionToArray, env)) { return false; } ExpressionTree rcvtoarray = ((MethodInvocationTree) encl).getMethodSelect(); if (!(rcvtoarray instanceof MemberSelectTree)) { return false; } rcvtoarray = ((MemberSelectTree) rcvtoarray).getExpression(); if (!(rcvtoarray instanceof IdentifierTree)) { return false; } return ((IdentifierTree) rcvsize).getName() == ((IdentifierTree) rcvtoarray).getName(); } /** Case 4: Check for thrown exception nullness */ @Override protected void checkThrownExpression(ThrowTree node) { checkForNullability(node.getExpression(), THROWING_NULLABLE); } /** Case 5: Check for synchronizing locks */ @Override public Void visitSynchronized(SynchronizedTree node, Void p) { checkForNullability(node.getExpression(), LOCKING_NULLABLE); return super.visitSynchronized(node, p); } @Override public Void visitAssert(AssertTree node, Void p) { // See also org.checkerframework.dataflow.cfg.CFGBuilder.CFGTranslationPhaseOne.visitAssert // In cases where neither assumeAssertionsAreEnabled nor assumeAssertionsAreDisabled are turned on // and @AssumeAssertions is not used, checkForNullability is still called since the CFGBuilder will have // generated one branch for which asserts are assumed to be enabled. boolean doVisitAssert = true; if (checker.hasOption("assumeAssertionsAreEnabled") || CFCFGBuilder.assumeAssertionsActivatedForAssertTree(checker, node)) { doVisitAssert = true; } else if (checker.hasOption("assumeAssertionsAreDisabled")) { doVisitAssert = false; } if (doVisitAssert) { checkForNullability(node.getCondition(), CONDITION_NULLABLE); return super.visitAssert(node, p); } return null; } @Override public Void visitIf(IfTree node, Void p) { checkForNullability(node.getCondition(), CONDITION_NULLABLE); return super.visitIf(node, p); } /** * Reports an error if a comparison of a @NonNull expression with the null literal is performed. */ protected void checkForRedundantTests(BinaryTree node) { final ExpressionTree leftOp = node.getLeftOperand(); final ExpressionTree rightOp = node.getRightOperand(); // respect command-line option if (!checker.getLintOption( AbstractNullnessChecker.LINT_REDUNDANTNULLCOMPARISON, AbstractNullnessChecker.LINT_DEFAULT_REDUNDANTNULLCOMPARISON)) { return; } // equality tests if ((node.getKind() == Tree.Kind.EQUAL_TO || node.getKind() == Tree.Kind.NOT_EQUAL_TO)) { AnnotatedTypeMirror left = atypeFactory.getAnnotatedType(leftOp); AnnotatedTypeMirror right = atypeFactory.getAnnotatedType(rightOp); if (leftOp.getKind() == Tree.Kind.NULL_LITERAL && right.hasEffectiveAnnotation(NONNULL)) checker.report(Result.warning(KNOWN_NONNULL, rightOp.toString()), node); else if (rightOp.getKind() == Tree.Kind.NULL_LITERAL && left.hasEffectiveAnnotation(NONNULL)) checker.report(Result.warning(KNOWN_NONNULL, leftOp.toString()), node); } } /** Case 6: Check for redundant nullness tests Case 7: unboxing case: primitive operations */ @Override public Void visitBinary(BinaryTree node, Void p) { final ExpressionTree leftOp = node.getLeftOperand(); final ExpressionTree rightOp = node.getRightOperand(); if (isUnboxingOperation(node)) { checkForNullability(leftOp, UNBOXING_OF_NULLABLE); checkForNullability(rightOp, UNBOXING_OF_NULLABLE); } checkForRedundantTests(node); return super.visitBinary(node, p); } /** Case 7: unboxing case: primitive operation */ @Override public Void visitUnary(UnaryTree node, Void p) { checkForNullability(node.getExpression(), UNBOXING_OF_NULLABLE); return super.visitUnary(node, p); } /** Case 7: unboxing case: primitive operation */ @Override public Void visitCompoundAssignment(CompoundAssignmentTree node, Void p) { // ignore String concatenation if (!isString(node)) { checkForNullability(node.getVariable(), UNBOXING_OF_NULLABLE); checkForNullability(node.getExpression(), UNBOXING_OF_NULLABLE); } return super.visitCompoundAssignment(node, p); } /** Case 7: unboxing case: casting to a primitive */ @Override public Void visitTypeCast(TypeCastTree node, Void p) { if (isPrimitive(node) && !isPrimitive(node.getExpression())) { checkForNullability(node.getExpression(), UNBOXING_OF_NULLABLE); } return super.visitTypeCast(node, p); } // ///////////// Utility methods ////////////////////////////// /** * Issues the error message if the type of the tree is not of a {@link NonNull} type. * * @param tree the tree where the error is to reported * @param errMsg the error message (must be {@link CompilerMessageKey}) * @return whether or not the check succeeded */ private boolean checkForNullability( ExpressionTree tree, /*@CompilerMessageKey*/ String errMsg) { AnnotatedTypeMirror type = atypeFactory.getAnnotatedType(tree); return checkForNullability(type, tree, errMsg); } /** * Issues the error message if an expression with this type may be null. * * @param type annotated type * @param tree the tree where the error is to reported * @param errMsg the error message (must be {@link CompilerMessageKey}) * @return whether or not the check succeeded */ private boolean checkForNullability( AnnotatedTypeMirror type, Tree tree, /*@CompilerMessageKey*/ String errMsg) { if (!type.hasEffectiveAnnotation(NONNULL)) { checker.report(Result.failure(errMsg, tree), tree); return false; } return true; } @Override protected void checkMethodInvocability( AnnotatedExecutableType method, MethodInvocationTree node) { if (!TreeUtils.isSelfAccess(node) && // Static methods don't have a receiver method.getReceiverType() != null) { // TODO: should all or some constructors be excluded? // method.getElement().getKind() != ElementKind.CONSTRUCTOR) { Set<AnnotationMirror> recvAnnos = atypeFactory.getReceiverType(node).getAnnotations(); AnnotatedTypeMirror methodReceiver = method.getReceiverType().getErased(); AnnotatedTypeMirror treeReceiver = methodReceiver.shallowCopy(false); AnnotatedTypeMirror rcv = atypeFactory.getReceiverType(node); treeReceiver.addAnnotations(rcv.getEffectiveAnnotations()); // If receiver is Nullable, then we don't want to issue a warning // about method invocability (we'd rather have only the // "dereference.of.nullable" message). if (treeReceiver.hasAnnotation(NULLABLE) || recvAnnos.contains(MONOTONIC_NONNULL)) { return; } } super.checkMethodInvocability(method, node); } /** @return true if binary operation could cause an unboxing operation */ private final boolean isUnboxingOperation(BinaryTree tree) { if (tree.getKind() == Tree.Kind.EQUAL_TO || tree.getKind() == Tree.Kind.NOT_EQUAL_TO) { // it is valid to check equality between two reference types, even // if one (or both) of them is null return isPrimitive(tree.getLeftOperand()) != isPrimitive(tree.getRightOperand()); } else { // All BinaryTree's are of type String, a primitive type or the // reference type equivalent of a primitive type. Furthermore, // Strings don't have a primitive type, and therefore only // BinaryTrees that aren't String can cause unboxing. return !isString(tree); } } /** @return true if the type of the tree is a super of String */ private final boolean isString(ExpressionTree tree) { TypeMirror type = InternalUtils.typeOf(tree); return types.isAssignable(stringType, type); } /** @return true if the type of the tree is a primitive */ private static final boolean isPrimitive(ExpressionTree tree) { return InternalUtils.typeOf(tree).getKind().isPrimitive(); } @Override public Void visitSwitch(SwitchTree node, Void p) { checkForNullability(node.getExpression(), SWITCHING_NULLABLE); return super.visitSwitch(node, p); } @Override public Void visitForLoop(ForLoopTree node, Void p) { if (node.getCondition() != null) { // Condition is null e.g. in "for (;;) {...}" checkForNullability(node.getCondition(), CONDITION_NULLABLE); } return super.visitForLoop(node, p); } @Override public Void visitNewClass(NewClassTree node, Void p) { AnnotatedDeclaredType type = atypeFactory.getAnnotatedType(node); ExpressionTree identifier = node.getIdentifier(); if (identifier instanceof AnnotatedTypeTree) { AnnotatedTypeTree t = (AnnotatedTypeTree) identifier; for (AnnotationMirror a : atypeFactory.getAnnotatedType(t).getAnnotations()) { // is this an annotation of the nullness checker? boolean nullnessCheckerAnno = containsSameIgnoringValues(atypeFactory.getNullnessAnnotations(), a); if (nullnessCheckerAnno && !AnnotationUtils.areSame(NONNULL, a)) { // The type is not non-null => warning checker.report( Result.warning("new.class.type.invalid", type.getAnnotations()), node); // Note that other consistency checks are made by isValid. } } if (t.toString().contains("@PolyNull")) { // TODO: this is a hack, but PolyNull gets substituted // afterwards checker.report( Result.warning("new.class.type.invalid", type.getAnnotations()), node); } } // TODO: It might be nicer to introduce a framework-level // isValidNewClassType or some such. return super.visitNewClass(node, p); } @Override public Void visitWhileLoop(WhileLoopTree node, Void p) { checkForNullability(node.getCondition(), CONDITION_NULLABLE); return super.visitWhileLoop(node, p); } @Override public Void visitDoWhileLoop(DoWhileLoopTree node, Void p) { checkForNullability(node.getCondition(), CONDITION_NULLABLE); return super.visitDoWhileLoop(node, p); } @Override public Void visitConditionalExpression(ConditionalExpressionTree node, Void p) { checkForNullability(node.getCondition(), CONDITION_NULLABLE); return super.visitConditionalExpression(node, p); } @Override protected void checkExceptionParameter(CatchTree node) { // BasetypeVisitor forces annotations on exception parameters to be top, // but because exceptions can never be null, the Nullness Checker // does not require this check. } }