/*
* Copyright 2014 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.errorprone.dataflow.nullnesspropagation;
import static com.google.errorprone.dataflow.nullnesspropagation.Nullness.NONNULL;
import static com.google.errorprone.dataflow.nullnesspropagation.Nullness.NULL;
import static com.google.errorprone.dataflow.nullnesspropagation.Nullness.NULLABLE;
import static com.sun.tools.javac.code.TypeTag.BOOLEAN;
import static javax.lang.model.element.ElementKind.EXCEPTION_PARAMETER;
import static org.checkerframework.javacutil.TreeUtils.elementFromDeclaration;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.base.Verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.io.Files;
import com.google.common.primitives.UnsignedInteger;
import com.google.common.primitives.UnsignedLong;
import com.google.errorprone.dataflow.LocalStore;
import com.google.errorprone.dataflow.LocalVariableValues;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree.JCFieldAccess;
import com.sun.tools.javac.tree.JCTree.JCIdent;
import com.sun.tools.javac.util.Context;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.VariableElement;
import org.checkerframework.dataflow.analysis.Analysis;
import org.checkerframework.dataflow.cfg.CFGBuilder;
import org.checkerframework.dataflow.cfg.ControlFlowGraph;
import org.checkerframework.dataflow.cfg.UnderlyingAST;
import org.checkerframework.dataflow.cfg.node.ArrayAccessNode;
import org.checkerframework.dataflow.cfg.node.ArrayCreationNode;
import org.checkerframework.dataflow.cfg.node.AssignmentNode;
import org.checkerframework.dataflow.cfg.node.EqualToNode;
import org.checkerframework.dataflow.cfg.node.FieldAccessNode;
import org.checkerframework.dataflow.cfg.node.FunctionalInterfaceNode;
import org.checkerframework.dataflow.cfg.node.InstanceOfNode;
import org.checkerframework.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.cfg.node.NotEqualNode;
import org.checkerframework.dataflow.cfg.node.TypeCastNode;
import org.checkerframework.dataflow.cfg.node.VariableDeclarationNode;
/**
* The {@code TransferFunction} for our nullability analysis. This analysis determines, for all
* variables and parameters, whether they are definitely null ({@link Nullness#NULL}), definitely
* non-null ({@link Nullness#NONNULL}), possibly null ({@link Nullness#NULLABLE}), or are on an
* infeasible path ({@link Nullness#BOTTOM}). This analysis depends only on the code and does not
* take nullness annotations into account.
*
* <p>Each {@code visit*()} implementation provides us with information about nullability that
* applies <i>if the expression is successfully evaluated</i> (in other words, it did not throw an
* exception). For example, if {@code foo.toString()} is successfully evaluated, we know two things:
*
* <ol>
* <li>The expression itself is non-null (because {@code toString()} is in our whitelist of methods
* known to return non-null values)
* <li>{@code foo} is non-null (because it has been dereferenced without producing a {@code
* NullPointerException})
* </ol>
*
* <p>These particular two pieces of data also demonstrate the two connected but distinct systems
* that we use to track nullness:
*
* <ol>
* <li>We compute the nullability of each expression by applying rules that may reference only the
* nullability of <i>subexpressions</i>. We make the result available only to superexpressions
* (and to the {@linkplain Analysis#getValue final output of the analysis}).
* <li>We {@linkplain LocalVariableUpdates update} and {@linkplain LocalVariableValues read} the
* nullability of <i>variables</i> in a mapping that persists from node to node. This is the
* only exception to the rule that we propagate data from subexpression to superexpression only.
* The mapping is read only when visiting a {@link LocalVariableNode}. That is enough to give
* the {@code LocalVariableNode} a value that is then available to superexpressions.
* </ol>
*
* <p>A further complication is that sometimes we know the nullability of an expression only
* conditionally based on its result. For example, {@code foo == null} proves that {@code foo} is
* null in the true case (such as inside {@code if (foo == null) { ... }}) and non-null in the false
* case (such an inside an accompanying {@code else} block). This is handled by methods that accept
* multiple {@link LocalVariableUpdates} instances, one for the true case and one for the false
* case.
*
* @author deminguyen@google.com (Demi Nguyen)
*/
class NullnessPropagationTransfer extends AbstractNullnessPropagationTransfer
implements Serializable {
private static final long serialVersionUID = -2413953917354086984L;
/** Matches methods that are statically known never to return null. */
private static class ReturnValueIsNonNull implements Predicate<MethodInfo>, Serializable {
private static final long serialVersionUID = -6277529478866058532L;
private static final ImmutableSet<MemberName> METHODS_WITH_NON_NULLABLE_RETURNS =
ImmutableSet.of(
// We would love to include all the methods of Files, but readFirstLine can return null.
member(Files.class.getName(), "toString"),
// Some methods of Class can return null, e.g. getAnnotation, getCanonicalName
member(Class.class.getName(), "getName"),
member(Class.class.getName(), "getSimpleName"),
member(Class.class.getName(), "forName"),
member(Charset.class.getName(), "forName"));
// TODO(cpovirk): respect nullness annotations (and also check them to ensure correctness!)
private static final ImmutableSet<String> CLASSES_WITH_NON_NULLABLE_RETURNS =
ImmutableSet.of(
Optional.class.getName(),
Preconditions.class.getName(),
Verify.class.getName(),
String.class.getName(),
BigInteger.class.getName(),
BigDecimal.class.getName(),
UnsignedInteger.class.getName(),
UnsignedLong.class.getName());
private static final ImmutableSet<String> CLASSES_WITH_NON_NULLABLE_VALUE_OF_METHODS =
ImmutableSet.of(
// The primitive types.
Boolean.class.getName(),
Byte.class.getName(),
Character.class.getName(),
Double.class.getName(),
Float.class.getName(),
Integer.class.getName(),
Long.class.getName(),
Short.class.getName(),
// Other types.
// TODO(cpovirk): recognize the compiler-generated valueOf() methods on Enum subclasses
Enum.class.getName(),
String.class.getName());
@Override
public boolean apply(MethodInfo methodInfo) {
// Any method explicitly annotated with @Nullable is assumed to be capable of returning
// null.
for (String annotation : methodInfo.annotations()) {
if (annotation.endsWith(".Nullable")) {
return false;
}
}
if (methodInfo.method().equals("valueOf")
&& CLASSES_WITH_NON_NULLABLE_VALUE_OF_METHODS.contains(methodInfo.clazz())) {
return true;
}
if (methodInfo.isPrimitive()) {
return true;
}
if (CLASSES_WITH_NON_NULLABLE_RETURNS.contains(methodInfo.clazz())) {
return true;
}
MemberName searchMemberName = new MemberName(methodInfo.clazz(), methodInfo.method());
if (METHODS_WITH_NON_NULLABLE_RETURNS.contains(searchMemberName)) {
return true;
}
return false;
}
}
private final transient Set<VarSymbol> traversed = new HashSet<>();
private final Nullness defaultAssumption;
private final Predicate<MethodInfo> methodReturnsNonNull;
/**
* Javac context so we can {@link #fieldInitializerNullnessIfAvailable find and analyze field
* initializers}.
*/
private transient Context context;
/** Compilation unit to limit evaluating field initializers to. */
private transient CompilationUnitTree compilationUnit;
/**
* Constructs a {@link NullnessPropagationTransfer} instance with the built-in set of non-null
* returning methods.
*/
public NullnessPropagationTransfer() {
this(NULLABLE, new ReturnValueIsNonNull());
}
/**
* Constructs a {@link NullnessPropagationTransfer} instance with additional non-null returning
* methods. The additional predicate is or'ed with the predicate for the built-in set of non-null
* returning methods.
*/
public NullnessPropagationTransfer(Predicate<MethodInfo> additionalNonNullReturningMethods) {
this(NULLABLE, Predicates.or(new ReturnValueIsNonNull(), additionalNonNullReturningMethods));
}
/**
* Constructor for use by subclasses.
*
* @param defaultAssumption used if a field or method can't be resolved as well as as the default
* for local variables, field and array reads in the absence of better information
* @param methodReturnsNonNull determines whether a method's return value is known to be non-null
*/
protected NullnessPropagationTransfer(
Nullness defaultAssumption, Predicate<MethodInfo> methodReturnsNonNull) {
this.defaultAssumption = defaultAssumption;
this.methodReturnsNonNull = methodReturnsNonNull;
}
/**
* Stores the given Javac context to find and analyze field initializers. Set before analyzing a
* method and reset after.
*/
NullnessPropagationTransfer setContext(@Nullable Context context) {
// This is a best-effort check (similar to ArrayList iterators, for instance), no guarantee
Preconditions.checkArgument(
context == null || this.context == null,
"Context already set: reset after use and don't use this class concurrently");
this.context = context;
// Clear traversed set just-in-case as this marks the beginning or end of analyzing a method
this.traversed.clear();
return this;
}
/**
* Set compilation unit being analyzed, to limit analyzing field initializers to that compilation
* unit. Analyzing initializers from other compilation units tends to fail because type
* information is sometimes missing on nodes returned from {@link Trees}.
*/
NullnessPropagationTransfer setCompilationUnit(@Nullable CompilationUnitTree compilationUnit) {
this.compilationUnit = compilationUnit;
return this;
}
// Literals
@Override
Nullness visitThisLiteral() {
return NONNULL;
}
@Override
Nullness visitSuper() {
return NONNULL;
}
/**
* Note: A short literal appears as an int to the compiler, and the compiler can perform a
* narrowing conversion on the literal to cast from int to short. For example, when assigning a
* literal to a short variable, the literal does not transfer its own non-null type to the
* variable. Instead, the variable receives the non-null type from the return value of the
* conversion call.
*/
@Override
Nullness visitValueLiteral() {
return NONNULL;
}
@Override
Nullness visitNullLiteral() {
return NULL;
}
@Override
Nullness visitBitwiseOperation() {
return NONNULL;
}
@Override
Nullness visitNumericalComparison() {
return NONNULL;
}
@Override
Nullness visitNumericalOperation() {
return NONNULL;
}
@Override
Nullness visitInstanceOf(
InstanceOfNode node,
SubNodeValues inputs,
LocalVariableUpdates thenUpdates,
LocalVariableUpdates elseUpdates) {
setNonnullIfLocalVariable(thenUpdates, node.getOperand());
return NONNULL; // the result of an instanceof is a primitive boolean, so it's non-null
}
@Override
Nullness visitTypeCast(TypeCastNode node, SubNodeValues inputs) {
return hasPrimitiveType(node) ? NONNULL : inputs.valueOfSubNode(node.getOperand());
}
/**
* The result of string concatenation is always non-null. If an operand is {@code null}, it is
* converted to {@code "null"}. For more information, see JLS 15.18.1 "String Concatenation
* Operator +", and 5.1.11, "String Conversion".
*/
@Override
Nullness visitStringConcatenate() {
// TODO(user): Mark the inputs as dereferenced.
return NONNULL;
}
@Override
Nullness visitStringConversion() {
return NONNULL;
}
@Override
Nullness visitNarrowingConversion() {
return NONNULL;
}
@Override
Nullness visitWideningConversion() {
return NONNULL;
}
@Override
void visitEqualTo(
EqualToNode node,
SubNodeValues inputs,
LocalVariableUpdates thenUpdates,
LocalVariableUpdates elseUpdates) {
handleEqualityComparison(
true, node.getLeftOperand(), node.getRightOperand(), inputs, thenUpdates, elseUpdates);
}
@Override
void visitNotEqual(
NotEqualNode node,
SubNodeValues inputs,
LocalVariableUpdates thenUpdates,
LocalVariableUpdates elseUpdates) {
handleEqualityComparison(
false, node.getLeftOperand(), node.getRightOperand(), inputs, thenUpdates, elseUpdates);
}
@Override
Nullness visitAssignment(
AssignmentNode node, SubNodeValues inputs, LocalVariableUpdates updates) {
Nullness value = inputs.valueOfSubNode(node.getExpression());
Node target = node.getTarget();
if (target instanceof LocalVariableNode) {
updates.set((LocalVariableNode) target, value);
}
if (target instanceof ArrayAccessNode) {
setNonnullIfLocalVariable(updates, ((ArrayAccessNode) target).getArray());
}
if (target instanceof FieldAccessNode) {
FieldAccessNode fieldAccess = (FieldAccessNode) target;
ClassAndField targetField = tryGetFieldSymbol(target.getTree());
setReceiverNonnull(updates, fieldAccess.getReceiver(), targetField);
}
/*
* We propagate the value of the target to the value of the assignment expressions as a whole.
* We do this regardless of whether the target is a local variable. For example:
*
* String s = object.field = "foo"; // Now |s| is non-null.
*
* It's not clear to me that this is technically correct, but it works in practice with the
* bytecode generated by both javac and ecj.
*
* http://stackoverflow.com/q/12850676/28465
*/
return value;
}
/**
* Variables take their values from their past assignments (as far as they can be determined).
* Additionally, variables of primitive type are always refined to non-null.
*
* <p>(This second case is rarely of interest to us. Either the variable is being used as a
* primitive, in which case we probably wouldn't have bothered to run the nullness checker on it,
* or it's being used as an Object, in which case the compiler generates a call to {@code valueOf}
* (to autobox the value), which triggers {@link #visitMethodInvocation}.)
*
* <p>Edge case: {@code node} can be a captured local variable accessed from inside a local or
* anonymous inner class, or possibly from inside a lambda expression (even though these manifest
* as fields in bytecode). As of 7/2016 this analysis doesn't have any knowledge of captured local
* variables will essentially assume whatever default is used in {@link #values}.
*/
@Override
Nullness visitLocalVariable(LocalVariableNode node, LocalVariableValues<Nullness> values) {
return hasPrimitiveType(node) || hasNonNullConstantValue(node)
? NONNULL
: values.valueOfLocalVariable(node, defaultAssumption);
}
/**
* Refines the receiver of a field access to type non-null after a successful field access, and
* refines the value of the expression as a whole to non-null if applicable (e.g., if the field
* has a primitive type).
*
* <p>Note: If the field access occurs when the node is an l-value, the analysis won't call this
* method. Instead, it will call {@link #visitAssignment}.
*/
@Override
Nullness visitFieldAccess(FieldAccessNode node, LocalVariableUpdates updates) {
ClassAndField accessed = tryGetFieldSymbol(node.getTree());
setReceiverNonnull(updates, node.getReceiver(), accessed);
return fieldNullness(accessed);
}
/**
* Refines the accessed array to non-null after a successful array access.
*
* <p>Note: If the array access occurs when the node is an l-value, the analysis won't call this
* method. Instead, it will call {@link #visitAssignment}.
*/
@Override
Nullness visitArrayAccess(
ArrayAccessNode node, SubNodeValues inputs, LocalVariableUpdates updates) {
setNonnullIfLocalVariable(updates, node.getArray());
return hasPrimitiveType(node) ? NONNULL : defaultAssumption;
}
/**
* Refines the receiver of a method invocation to type non-null after successful invocation, and
* refines the value of the expression as a whole to non-null if applicable (e.g., if the method
* returns a primitive type).
*/
@Override
Nullness visitMethodInvocation(
MethodInvocationNode node,
LocalVariableUpdates thenUpdates,
LocalVariableUpdates elseUpdates,
LocalVariableUpdates bothUpdates) {
ClassAndMethod callee = tryGetMethodSymbol(node.getTree());
setReceiverNonnull(bothUpdates, node.getTarget().getReceiver(), callee);
setUnconditionalArgumentNullness(bothUpdates, node.getArguments(), callee);
setConditionalArgumentNullness(elseUpdates, node.getArguments(), callee);
return returnValueNullness(callee);
}
@Override
Nullness visitObjectCreation() {
return NONNULL;
}
@Override
Nullness visitArrayCreation(
ArrayCreationNode node, SubNodeValues inputs, LocalVariableUpdates updates) {
return NONNULL;
}
@Override
Nullness visitMemberReference(
FunctionalInterfaceNode node, SubNodeValues inputs, LocalVariableUpdates updates) {
// TODO(kmb,cpovirk): Mark object member reference receivers as non-null
return NONNULL; // lambdas and member references are never null :)
}
@Override
void visitVariableDeclaration(
VariableDeclarationNode node, SubNodeValues inputs, LocalVariableUpdates updates) {
/*
* We could try to handle primitives here instead of in visitLocalVariable, but it won't be
* enough because we don't see method parameters here.
*/
if (isCatchVariable(node)) {
updates.set(node, NONNULL);
}
}
private static boolean isCatchVariable(VariableDeclarationNode node) {
return elementFromDeclaration(node.getTree()).getKind() == EXCEPTION_PARAMETER;
}
/**
* Refines the {@code Nullness} of {@code LocalVariableNode}s used in an equality comparison using
* the greatest lower bound.
*
* @param equalTo whether the comparison is == (false for !=)
* @param leftNode the left-hand side of the comparison
* @param rightNode the right-hand side of the comparison
* @param inputs access to nullness values of the left and right nodes
* @param thenUpdates the local variables whose nullness values should be updated if the
* comparison returns {@code true}
* @param elseUpdates the local variables whose nullness values should be updated if the
* comparison returns {@code false}
*/
private static void handleEqualityComparison(
boolean equalTo,
Node leftNode,
Node rightNode,
SubNodeValues inputs,
LocalVariableUpdates thenUpdates,
LocalVariableUpdates elseUpdates) {
Nullness leftVal = inputs.valueOfSubNode(leftNode);
Nullness rightVal = inputs.valueOfSubNode(rightNode);
Nullness equalBranchValue = leftVal.greatestLowerBound(rightVal);
LocalVariableUpdates equalBranchUpdates = equalTo ? thenUpdates : elseUpdates;
LocalVariableUpdates notEqualBranchUpdates = equalTo ? elseUpdates : thenUpdates;
if (leftNode instanceof LocalVariableNode) {
LocalVariableNode localVar = (LocalVariableNode) leftNode;
equalBranchUpdates.set(localVar, equalBranchValue);
notEqualBranchUpdates.set(
localVar, leftVal.greatestLowerBound(rightVal.deducedValueWhenNotEqual()));
}
if (rightNode instanceof LocalVariableNode) {
LocalVariableNode localVar = (LocalVariableNode) rightNode;
equalBranchUpdates.set(localVar, equalBranchValue);
notEqualBranchUpdates.set(
localVar, rightVal.greatestLowerBound(leftVal.deducedValueWhenNotEqual()));
}
}
private static boolean hasPrimitiveType(Node node) {
return node.getType().getKind().isPrimitive();
}
private static boolean hasNonNullConstantValue(LocalVariableNode node) {
if (node.getElement() instanceof VariableElement) {
VariableElement element = (VariableElement) node.getElement();
return (element.getConstantValue() != null);
}
return false;
}
private static ClassAndField tryGetFieldSymbol(Tree tree) {
Symbol symbol = tryGetSymbol(tree);
if (symbol instanceof VarSymbol) {
return ClassAndField.make((VarSymbol) symbol);
}
return null;
}
static ClassAndMethod tryGetMethodSymbol(MethodInvocationTree tree) {
Symbol symbol = tryGetSymbol(tree.getMethodSelect());
if (symbol instanceof MethodSymbol) {
return ClassAndMethod.make((MethodSymbol) symbol);
}
return null;
}
/*
* We can't use ASTHelpers here. It's in core, which depends on jdk8, so we can't make jdk8 depend
* back on core.
*/
private static Symbol tryGetSymbol(Tree tree) {
if (tree instanceof JCIdent) {
return ((JCIdent) tree).sym;
}
if (tree instanceof JCFieldAccess) {
return ((JCFieldAccess) tree).sym;
}
return null;
}
Nullness fieldNullness(@Nullable ClassAndField accessed) {
if (accessed == null) {
return defaultAssumption;
}
if (accessed.field.equals("class")) {
return NONNULL;
}
if (accessed.isEnumConstant()) {
return NONNULL;
}
if (accessed.isPrimitive()) { // includes <array>.length
return NONNULL;
}
if (accessed.hasNonNullConstantValue()) {
return NONNULL;
}
if (accessed.isStatic() && accessed.isFinal()) {
if (CLASSES_WITH_NON_NULL_CONSTANTS.contains(accessed.clazz)) {
return NONNULL;
}
// Try to evaluate initializer.
// TODO(kmb): Consider handling final instance fields as well
Nullness initializer = fieldInitializerNullnessIfAvailable(accessed);
if (initializer != null) {
return initializer;
}
}
return defaultAssumption;
}
private Nullness returnValueNullness(@Nullable ClassAndMethod callee) {
if (callee == null) {
return defaultAssumption;
}
return methodReturnsNonNull.apply(callee) ? NONNULL : NULLABLE;
}
@Nullable
private Nullness fieldInitializerNullnessIfAvailable(ClassAndField accessed) {
if (!traversed.add(accessed.symbol)) {
// Circular dependency between initializers results in null. Note static fields can also be
// null if they're observed before initialized, but we're ignoring that case for simplicity.
// TODO(kmb): Try to recognize problems with initialization order
return NULL;
}
try {
JavacProcessingEnvironment javacEnv = JavacProcessingEnvironment.instance(context);
TreePath fieldDeclPath = Trees.instance(javacEnv).getPath(accessed.symbol);
// Skip initializers in other compilation units as analysis of such nodes can fail due to
// missing types.
if (fieldDeclPath == null
|| fieldDeclPath.getCompilationUnit() != compilationUnit
|| !(fieldDeclPath.getLeaf() instanceof VariableTree)) {
return null;
}
ExpressionTree initializer = ((VariableTree) fieldDeclPath.getLeaf()).getInitializer();
if (initializer == null) {
return null;
}
// Run flow analysis on field initializer. This is inefficient compared to just walking
// the initializer expression tree but it avoids duplicating the logic from this transfer
// function into a method that operates on Javac Nodes.
TreePath initializerPath = TreePath.getPath(fieldDeclPath, initializer);
UnderlyingAST ast = new UnderlyingAST.CFGStatement(initializerPath.getLeaf());
ControlFlowGraph cfg =
CFGBuilder.build(
initializerPath,
javacEnv,
ast,
/* assumeAssertionsEnabled */ false, /* assumeAssertionsDisabled */
false);
Analysis<Nullness, LocalStore<Nullness>, NullnessPropagationTransfer> analysis =
new Analysis<>(javacEnv, this);
analysis.performAnalysis(cfg);
return analysis.getValue(initializerPath.getLeaf());
} finally {
traversed.remove(accessed.symbol);
}
}
private static void setReceiverNonnull(
LocalVariableUpdates updates, Node receiver, Member member) {
if (!member.isStatic()) {
setNonnullIfLocalVariable(updates, receiver);
}
}
private static void setNonnullIfLocalVariable(LocalVariableUpdates updates, Node node) {
if (node instanceof LocalVariableNode) {
updates.set((LocalVariableNode) node, NONNULL);
}
}
/**
* Records which arguments are guaranteed to be non-null if the method completes without
* exception. For example, if {@code checkNotNull(foo, message)} completes successfully, then
* {@code foo} is not null.
*/
private static void setUnconditionalArgumentNullness(
LocalVariableUpdates bothUpdates, List<Node> arguments, ClassAndMethod callee) {
Set<Integer> requiredNonNullParameters = REQUIRED_NON_NULL_PARAMETERS.get(callee.name());
for (LocalVariableNode var : variablesAtIndexes(requiredNonNullParameters, arguments)) {
bothUpdates.set(var, NONNULL);
}
}
/**
* Records which arguments are guaranteed to be non-null only if the method completes by returning
* {@code true} or only if the method completes by returning {@code false}. For example, if {@code
* Strings.isNullOrEmpty(s)} returns {@code false}, then {@code s} is not null.
*/
private static void setConditionalArgumentNullness(
LocalVariableUpdates elseUpdates, List<Node> arguments, ClassAndMethod callee) {
Set<Integer> nullImpliesTrueParameters = NULL_IMPLIES_TRUE_PARAMETERS.get(callee.name());
for (LocalVariableNode var : variablesAtIndexes(nullImpliesTrueParameters, arguments)) {
elseUpdates.set(var, NONNULL);
}
}
private static Iterable<LocalVariableNode> variablesAtIndexes(
Set<Integer> indexes, List<Node> arguments) {
List<LocalVariableNode> result = new ArrayList<>();
for (Integer i : indexes) {
if (i < 0) {
i = arguments.size() + i;
}
// TODO(cpovirk): better handling of varargs
if (i >= 0 && i < arguments.size()) {
Node argument = arguments.get(i);
if (argument instanceof LocalVariableNode) {
result.add((LocalVariableNode) argument);
}
}
}
return result;
}
interface Member {
boolean isStatic();
}
private static MemberName member(Class<?> clazz, String member) {
return member(clazz.getName(), member);
}
private static MemberName member(String clazz, String member) {
return new MemberName(clazz, member);
}
private void writeObject(ObjectOutputStream out) throws IOException {
Preconditions.checkState(context == null, "Can't serialize while analyzing a method");
Preconditions.checkState(compilationUnit == null, "Can't serialize while analyzing a method");
out.defaultWriteObject();
}
@VisibleForTesting
static final class MemberName {
final String clazz;
final String member;
MemberName(String clazz, String member) {
this.clazz = clazz;
this.member = member;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof MemberName) {
MemberName other = (MemberName) obj;
return clazz.equals(other.clazz) && member.equals(other.member);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(clazz, member);
}
}
static final class ClassAndMethod implements Member, MethodInfo {
final String clazz;
final String method;
final List<String> annotations;
final boolean isStatic;
final boolean isPrimitive;
final boolean isBoolean;
private ClassAndMethod(
String clazz,
String method,
List<String> annotations,
boolean isStatic,
boolean isPrimitive,
boolean isBoolean) {
this.clazz = clazz;
this.method = method;
this.annotations = ImmutableList.copyOf(annotations);
this.isStatic = isStatic;
this.isPrimitive = isPrimitive;
this.isBoolean = isBoolean;
}
static ClassAndMethod make(MethodSymbol symbol) {
List<? extends AnnotationMirror> annotationMirrors = symbol.getAnnotationMirrors();
List<String> annotations = new ArrayList<>(annotationMirrors.size());
for (AnnotationMirror annotationMirror : annotationMirrors) {
annotations.add(annotationMirror.getAnnotationType().toString());
}
return new ClassAndMethod(
symbol.owner.getQualifiedName().toString(),
symbol.getSimpleName().toString(),
annotations,
symbol.isStatic(),
symbol.getReturnType().isPrimitive(),
symbol.getReturnType().getTag() == BOOLEAN);
}
@Override
public boolean isStatic() {
return isStatic;
}
MemberName name() {
return new MemberName(this.clazz, this.method);
}
@Override
public String clazz() {
return clazz;
}
@Override
public String method() {
return method;
}
@Override
public List<String> annotations() {
return annotations;
}
@Override
public boolean isPrimitive() {
return isPrimitive;
}
}
static final class ClassAndField implements Member {
final VarSymbol symbol;
final String clazz;
final String field;
private ClassAndField(VarSymbol symbol) {
this.symbol = symbol;
this.clazz = symbol.owner.getQualifiedName().toString();
this.field = symbol.getSimpleName().toString();
}
static ClassAndField make(VarSymbol symbol) {
return new ClassAndField(symbol);
}
@Override
public boolean isStatic() {
return symbol.isStatic();
}
public boolean isFinal() {
return (symbol.flags() & Flags.FINAL) == Flags.FINAL;
}
public boolean isPrimitive() {
return symbol.type.isPrimitive();
}
public boolean isEnumConstant() {
return symbol.isEnum();
}
public boolean hasNonNullConstantValue() {
return symbol.getConstValue() != null;
}
}
/** Classes where we know that all static final fields are non-null. */
@VisibleForTesting
static final ImmutableSet<String> CLASSES_WITH_NON_NULL_CONSTANTS =
ImmutableSet.of(
BigInteger.class.getName(),
BigDecimal.class.getName(),
UnsignedInteger.class.getName(),
UnsignedLong.class.getName(),
StandardCharsets.class.getName());
/**
* Maps from the names of null-rejecting methods to the indexes of the arguments that aren't
* permitted to be null. Indexes may be negative to indicate a position relative to the end of the
* argument list. For example, "-1" means "the final parameter."
*/
@VisibleForTesting
static final ImmutableSetMultimap<MemberName, Integer> REQUIRED_NON_NULL_PARAMETERS =
new ImmutableSetMultimap.Builder<MemberName, Integer>()
.put(member(Preconditions.class, "checkNotNull"), 0)
.put(member(Verify.class, "verifyNotNull"), 0)
.put(member("junit.framework.Assert", "assertNotNull"), -1)
.put(member("org.junit.Assert", "assertNotNull"), -1)
.build();
/**
* Maps from the names of null-querying methods to the indexes of the arguments that are compared
* against null. Indexes may be negative to indicate a position relative to the end of the
* argument list. For example, "-1" means "the final parameter."
*/
@VisibleForTesting
static final ImmutableSetMultimap<MemberName, Integer> NULL_IMPLIES_TRUE_PARAMETERS =
new ImmutableSetMultimap.Builder<MemberName, Integer>()
.put(member(Strings.class, "isNullOrEmpty"), 0)
.build();
}