blob: 7cab42f28a0cb4cb095dfecf359608ba1a46c276 [file] [log] [blame]
/*
* Copyright (c) 2017 Uber Technologies, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.uber.nullaway;
import static com.google.errorprone.BugPattern.Category.JDK;
import static com.google.errorprone.BugPattern.SeverityLevel.WARNING;
import static com.sun.source.tree.Tree.Kind.EXPRESSION_STATEMENT;
import static com.sun.source.tree.Tree.Kind.IDENTIFIER;
import static com.sun.source.tree.Tree.Kind.PARENTHESIZED;
import static com.sun.source.tree.Tree.Kind.TYPE_CAST;
import com.google.auto.service.AutoService;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.google.errorprone.BugPattern;
import com.google.errorprone.ErrorProneFlags;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.matchers.Matcher;
import com.google.errorprone.matchers.Matchers;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.ExpressionStatementTree;
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.LambdaExpressionTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TryTree;
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 com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.Types;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.uber.nullaway.dataflow.AccessPathNullnessAnalysis;
import com.uber.nullaway.dataflow.EnclosingEnvironmentNullness;
import com.uber.nullaway.handlers.Handler;
import com.uber.nullaway.handlers.Handlers;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.NestingKind;
import javax.lang.model.type.TypeKind;
import org.checkerframework.dataflow.cfg.node.MethodInvocationNode;
import org.checkerframework.javacutil.AnnotationUtils;
/**
* Checker for nullability errors. It assumes that any field, method parameter, or return type that
* may be null is annotated with {@link Nullable}, and then checks the following rules:
*
* <ul>
* <li>no assignment of a nullable expression into a non-null field
* <li>no passing a nullable expression into a non-null parameter
* <li>no returning a nullable expression from a method with non-null return type
* <li>no field access or method invocation on an expression that is nullable
* </ul>
*
* <p>This checker also detects errors related to field initialization. For any @NonNull instance
* field <code>f</code>, this checker ensures that at least one of the following cases holds:
*
* <ol>
* <li><code>f</code> is directly initialized at its declaration
* <li><code>f</code> is always initialized in all constructors
* <li><code>f</code> is always initialized in some method annotated with @Initializer
* </ol>
*
* <p>For any @NonNull static field <code>f</code>, this checker ensures that at least one of the
* following cases holds:
*
* <ol>
* <li><code>f</code> is directly initialized at its declaration
* <li><code>f</code> is always initialized in some static initializer block
* </ol>
*/
@AutoService(BugChecker.class)
@BugPattern(
name = "NullAway",
altNames = {"CheckNullabilityTypes"},
summary = "Nullability type error.",
category = JDK,
severity = WARNING)
public class NullAway extends BugChecker
implements BugChecker.MethodInvocationTreeMatcher,
BugChecker.AssignmentTreeMatcher,
BugChecker.MemberSelectTreeMatcher,
BugChecker.ArrayAccessTreeMatcher,
BugChecker.ReturnTreeMatcher,
BugChecker.ClassTreeMatcher,
BugChecker.MethodTreeMatcher,
BugChecker.VariableTreeMatcher,
BugChecker.NewClassTreeMatcher,
BugChecker.BinaryTreeMatcher,
BugChecker.UnaryTreeMatcher,
BugChecker.ConditionalExpressionTreeMatcher,
BugChecker.IfTreeMatcher,
BugChecker.WhileLoopTreeMatcher,
BugChecker.ForLoopTreeMatcher,
BugChecker.LambdaExpressionTreeMatcher,
BugChecker.IdentifierTreeMatcher,
BugChecker.MemberReferenceTreeMatcher,
BugChecker.CompoundAssignmentTreeMatcher {
private static final String INITIALIZATION_CHECK_NAME = "NullAway.Init";
private static final Matcher<ExpressionTree> THIS_MATCHER =
(expressionTree, state) -> isThisIdentifier(expressionTree);
private final Predicate<MethodInvocationNode> nonAnnotatedMethod;
/** should we match within the current class? */
private boolean matchWithinClass = true;
private final Config config;
/**
* The handler passed to our analysis (usually a {@code CompositeHandler} including handlers for
* various APIs.
*/
private final Handler handler;
/**
* entities relevant to field initialization per class. cached for performance. nulled out in
* {@link #matchClass(ClassTree, VisitorState)}
*/
private final Map<Symbol.ClassSymbol, FieldInitEntities> class2Entities = new LinkedHashMap<>();
/**
* fields not initialized by constructors, per class. cached for performance. nulled out in {@link
* #matchClass(ClassTree, VisitorState)}
*/
private final SetMultimap<Symbol.ClassSymbol, Symbol> class2ConstructorUninit =
LinkedHashMultimap.create();
/**
* maps each top-level initialization member (constructor, init block, field decl with initializer
* expression) to the set of @NonNull fields known to be initialized before that member executes.
*
* <p>cached for performance. nulled out in {@link #matchClass(ClassTree, VisitorState)}
*/
private final Map<Symbol.ClassSymbol, Multimap<Tree, Element>> initTree2PrevFieldInit =
new LinkedHashMap<>();
/**
* dynamically computer/overriden nullness facts for certain expressions, such as specific method
* calls where we can infer a more precise set of facts than those given by the method's
* annotations.
*/
private final Map<ExpressionTree, Nullness> computedNullnessMap = new LinkedHashMap<>();
private final ImmutableSet<Class<? extends Annotation>> customSuppressionAnnotations;
/**
* Error Prone requires us to have an empty constructor for each Plugin, in addition to the
* constructor taking an ErrorProneFlags object. This constructor should not be used anywhere
* else. Checker objects constructed with this constructor will fail with IllegalStateException if
* ever used for analysis.
*/
public NullAway() {
config = new DummyOptionsConfig();
handler = Handlers.buildEmpty();
nonAnnotatedMethod = nonAnnotatedMethodCheck();
customSuppressionAnnotations = ImmutableSet.of();
}
public NullAway(ErrorProneFlags flags) {
config = new ErrorProneCLIFlagsConfig(flags);
handler = Handlers.buildDefault(config);
nonAnnotatedMethod = nonAnnotatedMethodCheck();
customSuppressionAnnotations = initCustomSuppressions();
// workaround for Checker Framework static state bug;
// See https://github.com/typetools/checker-framework/issues/1482
AnnotationUtils.clear();
}
private ImmutableSet<Class<? extends Annotation>> initCustomSuppressions() {
ImmutableSet.Builder<Class<? extends Annotation>> builder = ImmutableSet.builder();
builder.addAll(super.customSuppressionAnnotations());
for (String annotName : config.getExcludedClassAnnotations()) {
try {
builder.add(Class.forName(annotName).asSubclass(Annotation.class));
} catch (ClassNotFoundException e) {
// in this case, the annotation may be a source file currently being compiled,
// in which case we won't be able to resolve the class
}
}
return builder.build();
}
private Predicate<MethodInvocationNode> nonAnnotatedMethodCheck() {
return invocationNode ->
invocationNode == null
|| NullabilityUtil.isUnannotated(
ASTHelpers.getSymbol(invocationNode.getTree()), config);
}
@Override
public String linkUrl() {
// add a space to make it clickable from iTerm
return config.getErrorURL() + " ";
}
@Override
public Set<Class<? extends Annotation>> customSuppressionAnnotations() {
return customSuppressionAnnotations;
}
/**
* We are trying to see if (1) we are in a method guaranteed to return something non-null, and (2)
* this return statement can return something null.
*/
@Override
public Description matchReturn(ReturnTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
handler.onMatchReturn(this, tree, state);
ExpressionTree retExpr = tree.getExpression();
// let's do quick checks on returned expression first
if (retExpr == null) {
return Description.NO_MATCH;
}
// now let's check the enclosing method
TreePath enclosingMethodOrLambda =
NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(state.getPath());
if (enclosingMethodOrLambda == null) {
throw new RuntimeException("no enclosing method, lambda or initializer!");
}
if (!(enclosingMethodOrLambda.getLeaf() instanceof MethodTree
|| enclosingMethodOrLambda.getLeaf() instanceof LambdaExpressionTree)) {
throw new RuntimeException(
"return statement outside of a method or lambda! (e.g. in an initializer block)");
}
Tree leaf = enclosingMethodOrLambda.getLeaf();
Symbol.MethodSymbol methodSymbol;
if (leaf instanceof MethodTree) {
MethodTree enclosingMethod = (MethodTree) leaf;
methodSymbol = ASTHelpers.getSymbol(enclosingMethod);
} else {
// we have a lambda
methodSymbol =
NullabilityUtil.getFunctionalInterfaceMethod(
(LambdaExpressionTree) leaf, state.getTypes());
}
return checkReturnExpression(tree, retExpr, methodSymbol, state);
}
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
final Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
if (methodSymbol == null) {
throw new RuntimeException("not expecting unresolved method here");
}
handler.onMatchMethodInvocation(this, tree, state, methodSymbol);
// assuming this list does not include the receiver
List<? extends ExpressionTree> actualParams = tree.getArguments();
return handleInvocation(tree, state, methodSymbol, actualParams);
}
@Override
public Description matchNewClass(NewClassTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
if (methodSymbol == null) {
throw new RuntimeException("not expecting unresolved method here");
}
List<? extends ExpressionTree> actualParams = tree.getArguments();
if (tree.getClassBody() != null && actualParams.size() > 0) {
// passing parameters to constructor of anonymous class
// this constructor just invokes the constructor of the superclass, and
// in the AST does not have the parameter nullability annotations from the superclass.
// so, treat as if the superclass constructor is being invoked directly
// see https://github.com/uber/NullAway/issues/102
methodSymbol = getSymbolOfSuperConstructor(methodSymbol, state);
}
return handleInvocation(tree, state, methodSymbol, actualParams);
}
/**
* Updates the {@link EnclosingEnvironmentNullness} with an entry for lambda or anonymous class,
* capturing nullability info for locals just before the declaration of the entity
*
* @param tree either a lambda or a local / anonymous class
* @param state visitor state
*/
private void updateEnvironmentMapping(Tree tree, VisitorState state) {
AccessPathNullnessAnalysis analysis = getNullnessAnalysis(state);
// two notes:
// 1. we are free to take local variable information from the program point before
// the lambda / class declaration as only effectively final variables can be accessed
// from the nested scope, so the program point doesn't matter
// 2. we keep info on all locals rather than just effectively final ones for simplicity
EnclosingEnvironmentNullness.instance(state.context)
.addEnvironmentMapping(
tree, analysis.getLocalVarInfoBefore(state.getPath(), state.context));
}
private Symbol.MethodSymbol getSymbolOfSuperConstructor(
Symbol.MethodSymbol anonClassConstructorSymbol, VisitorState state) {
// get the statements in the body of the anonymous class constructor
List<? extends StatementTree> statements =
getTreesInstance(state).getTree(anonClassConstructorSymbol).getBody().getStatements();
// there should be exactly one statement, which is an invocation of the super constructor
if (statements.size() == 1) {
StatementTree stmt = statements.get(0);
if (stmt instanceof ExpressionStatementTree) {
ExpressionTree expression = ((ExpressionStatementTree) stmt).getExpression();
if (expression instanceof MethodInvocationTree) {
return ASTHelpers.getSymbol((MethodInvocationTree) expression);
}
}
}
throw new IllegalStateException("unexpected anonymous class constructor body " + statements);
}
@Override
public Description matchAssignment(AssignmentTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Type lhsType = ASTHelpers.getType(tree.getVariable());
if (lhsType != null && lhsType.isPrimitive()) {
return doUnboxingCheck(state, tree.getExpression());
}
Symbol assigned = ASTHelpers.getSymbol(tree.getVariable());
if (assigned == null || assigned.getKind() != ElementKind.FIELD) {
// not a field of nullable type
return Description.NO_MATCH;
}
if (Nullness.hasNullableAnnotation(assigned)) {
// field already annotated
return Description.NO_MATCH;
}
ExpressionTree expression = tree.getExpression();
if (mayBeNullExpr(state, expression)) {
String message = "assigning @Nullable expression to @NonNull field";
return createErrorDescriptionForNullAssignment(
MessageTypes.ASSIGN_FIELD_NULLABLE, tree, message, expression, state.getPath());
}
return Description.NO_MATCH;
}
@Override
public Description matchCompoundAssignment(CompoundAssignmentTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Type lhsType = ASTHelpers.getType(tree.getVariable());
Type stringType = state.getTypeFromString("java.lang.String");
if (lhsType != null && !lhsType.equals(stringType)) {
// both LHS and RHS could get unboxed
return doUnboxingCheck(state, tree.getVariable(), tree.getExpression());
}
return Description.NO_MATCH;
}
@Override
public Description matchArrayAccess(ArrayAccessTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Description description = matchDereference(tree.getExpression(), tree, state);
if (!description.equals(Description.NO_MATCH)) {
return description;
}
// also check for unboxing of array index expression
return doUnboxingCheck(state, tree.getIndex());
}
@Override
public Description matchMemberSelect(MemberSelectTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Symbol symbol = ASTHelpers.getSymbol(tree);
// some checks for cases where we know it is not
// a null dereference
if (symbol == null || symbol.getSimpleName().toString().equals("class") || symbol.isEnum()) {
return Description.NO_MATCH;
}
Description badDeref = matchDereference(tree.getExpression(), tree, state);
if (!badDeref.equals(Description.NO_MATCH)) {
return badDeref;
}
// if we're accessing a field of this, make sure we're not reading the field before init
if (tree.getExpression() instanceof IdentifierTree
&& ((IdentifierTree) tree.getExpression()).getName().toString().equals("this")) {
return checkForReadBeforeInit(tree, state);
}
return Description.NO_MATCH;
}
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
// if the method is overriding some other method,
// check that nullability annotations are consistent with
// overridden method (if overridden method is in an annotated
// package)
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(tree);
handler.onMatchMethod(this, tree, state, methodSymbol);
boolean isOverriding = ASTHelpers.hasAnnotation(methodSymbol, Override.class, state);
boolean exhaustiveOverride = config.exhaustiveOverride();
if (isOverriding || !exhaustiveOverride) {
Symbol.MethodSymbol closestOverriddenMethod =
getClosestOverriddenMethod(methodSymbol, state.getTypes());
if (closestOverriddenMethod == null) {
return Description.NO_MATCH;
}
return checkOverriding(closestOverriddenMethod, methodSymbol, null, state);
}
return Description.NO_MATCH;
}
/**
* checks that an overriding method does not override a {@code @Nullable} parameter with a
* {@code @NonNull} parameter
*
* @param overridingParamSymbols parameters of the overriding method
* @param overriddenMethod method being overridden
* @param lambdaExpressionTree if the overriding method is a lambda, the {@link
* LambdaExpressionTree}; otherwise {@code null}
* @param memberReferenceTree if the overriding method is a member reference (which "overrides" a
* functional interface method), the {@link MemberReferenceTree}; otherwise {@code null}
* @return
*/
private Description checkParamOverriding(
List<VarSymbol> overridingParamSymbols,
Symbol.MethodSymbol overriddenMethod,
@Nullable LambdaExpressionTree lambdaExpressionTree,
@Nullable MemberReferenceTree memberReferenceTree,
VisitorState state) {
com.sun.tools.javac.util.List<VarSymbol> superParamSymbols = overriddenMethod.getParameters();
boolean unboundMemberRef =
(memberReferenceTree != null)
&& ((JCTree.JCMemberReference) memberReferenceTree).kind.isUnbound();
// if we have an unbound method reference, the first parameter of the overridden method must be
// @NonNull, as this parameter will be used as a method receiver inside the generated lambda
if (unboundMemberRef) {
// there must be at least one parameter; otherwise code wouldn't compile
if (Nullness.hasNullableAnnotation(superParamSymbols.get(0))) {
String message =
"unbound instance method reference cannot be used, as first parameter of "
+ "functional interface method "
+ ASTHelpers.enclosingClass(overriddenMethod)
+ "."
+ overriddenMethod.toString()
+ " is @Nullable";
return createErrorDescription(
MessageTypes.WRONG_OVERRIDE_PARAM, memberReferenceTree, message, memberReferenceTree);
}
}
// for unbound member references, we need to adjust parameter indices by 1 when matching with
// overridden method
int startParam = unboundMemberRef ? 1 : 0;
// Collect @Nullable params of overriden method
ImmutableSet<Integer> nullableParamsOfOverriden;
if (NullabilityUtil.isUnannotated(overriddenMethod, config)) {
nullableParamsOfOverriden =
handler.onUnannotatedInvocationGetExplicitlyNullablePositions(
this, state, overriddenMethod, ImmutableSet.of());
} else {
ImmutableSet.Builder<Integer> builder = ImmutableSet.builder();
for (int i = startParam; i < superParamSymbols.size(); i++) {
// we need to call paramHasNullableAnnotation here since overriddenMethod may be defined
// in a class file
if (Nullness.paramHasNullableAnnotation(overriddenMethod, i)) {
builder.add(i);
}
}
nullableParamsOfOverriden = builder.build();
}
for (int i : nullableParamsOfOverriden) {
int methodParamInd = i - startParam;
VarSymbol paramSymbol = overridingParamSymbols.get(methodParamInd);
// in the case where we have a parameter of a lambda expression, we do
// *not* force the parameter to be annotated with @Nullable; instead we "inherit"
// nullability from the corresponding functional interface method.
// So, we report an error if the @Nullable annotation is missing *and*
// we don't have a lambda with implicitly typed parameters
boolean implicitlyTypedLambdaParam =
lambdaExpressionTree != null
&& NullabilityUtil.lambdaParamIsImplicitlyTyped(
lambdaExpressionTree.getParameters().get(methodParamInd));
if (!Nullness.hasNullableAnnotation(paramSymbol) && !implicitlyTypedLambdaParam) {
String message =
"parameter "
+ paramSymbol.name.toString()
+ (memberReferenceTree != null ? " of referenced method" : "")
+ " is @NonNull, but parameter in "
+ ((lambdaExpressionTree != null || memberReferenceTree != null)
? "functional interface "
: "superclass ")
+ "method "
+ ASTHelpers.enclosingClass(overriddenMethod)
+ "."
+ overriddenMethod.toString()
+ " is @Nullable";
Tree errorTree;
if (memberReferenceTree != null) {
errorTree = memberReferenceTree;
} else {
errorTree = getTreesInstance(state).getTree(paramSymbol);
}
return createErrorDescription(
MessageTypes.WRONG_OVERRIDE_PARAM, errorTree, message, errorTree);
}
}
return Description.NO_MATCH;
}
private static Trees getTreesInstance(VisitorState state) {
return Trees.instance(JavacProcessingEnvironment.instance(state.context));
}
private Description checkReturnExpression(
Tree tree, ExpressionTree retExpr, Symbol.MethodSymbol methodSymbol, VisitorState state) {
Type returnType = methodSymbol.getReturnType();
if (returnType.isPrimitive()) {
// check for unboxing
return doUnboxingCheck(state, retExpr);
}
if (returnType.toString().equals("java.lang.Void")) {
return Description.NO_MATCH;
}
if (NullabilityUtil.isUnannotated(methodSymbol, config)
|| Nullness.hasNullableAnnotation(methodSymbol)) {
return Description.NO_MATCH;
}
if (mayBeNullExpr(state, retExpr)) {
String message = "returning @Nullable expression from method with @NonNull return type";
return createErrorDescriptionForNullAssignment(
MessageTypes.RETURN_NULLABLE, tree, message, retExpr, state.getPath());
}
return Description.NO_MATCH;
}
@Override
public Description matchLambdaExpression(LambdaExpressionTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Symbol.MethodSymbol funcInterfaceMethod =
NullabilityUtil.getFunctionalInterfaceMethod(tree, state.getTypes());
// we need to update environment mapping before running the handler, as some handlers
// (like Rx nullability) run dataflow analysis
updateEnvironmentMapping(tree, state);
handler.onMatchLambdaExpression(this, tree, state, funcInterfaceMethod);
if (NullabilityUtil.isUnannotated(funcInterfaceMethod, config)) {
return Description.NO_MATCH;
}
Description description =
checkParamOverriding(
tree.getParameters().stream().map(ASTHelpers::getSymbol).collect(Collectors.toList()),
funcInterfaceMethod,
tree,
null,
state);
if (description != Description.NO_MATCH) {
return description;
}
// if the body has a return statement, that gets checked in matchReturn(). We need this code
// for lambdas with expression bodies
if (tree.getBodyKind() == LambdaExpressionTree.BodyKind.EXPRESSION
&& funcInterfaceMethod.getReturnType().getKind() != TypeKind.VOID) {
ExpressionTree resExpr = (ExpressionTree) tree.getBody();
return checkReturnExpression(tree, resExpr, funcInterfaceMethod, state);
}
return Description.NO_MATCH;
}
/**
* for method references, we check that the referenced method correctly overrides the
* corresponding functional interface method
*/
@Override
public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
Symbol.MethodSymbol referencedMethod = ASTHelpers.getSymbol(tree);
Symbol.MethodSymbol funcInterfaceSymbol =
NullabilityUtil.getFunctionalInterfaceMethod(tree, state.getTypes());
handler.onMatchMethodReference(this, tree, state, referencedMethod);
return checkOverriding(funcInterfaceSymbol, referencedMethod, tree, state);
}
/**
* check that nullability annotations of an overriding method are consistent with those in the
* overridden method (both return and parameters)
*
* @param overriddenMethod method being overridden
* @param overridingMethod overriding method
* @param memberReferenceTree if override is via a method reference, the relevant {@link
* MemberReferenceTree}; otherwise {@code null}. If non-null, overridingTree is the AST of the
* referenced method
* @param state
* @return discovered error, or {@link Description#NO_MATCH} if no error
*/
private Description checkOverriding(
Symbol.MethodSymbol overriddenMethod,
Symbol.MethodSymbol overridingMethod,
@Nullable MemberReferenceTree memberReferenceTree,
VisitorState state) {
final boolean isOverridenMethodUnannotated =
NullabilityUtil.isUnannotated(overriddenMethod, config);
final boolean overriddenMethodReturnsNonNull =
((isOverridenMethodUnannotated
&& handler.onUnannotatedInvocationGetExplicitlyNonNullReturn(
overriddenMethod, false))
|| (!isOverridenMethodUnannotated
&& !Nullness.hasNullableAnnotation(overriddenMethod)));
// if the super method returns nonnull,
// overriding method better not return nullable
if (overriddenMethodReturnsNonNull
&& Nullness.hasNullableAnnotation(overridingMethod)
&& getComputedNullness(memberReferenceTree).equals(Nullness.NULLABLE)) {
String message;
if (memberReferenceTree != null) {
message =
"referenced method returns @Nullable, but functional interface method "
+ ASTHelpers.enclosingClass(overriddenMethod)
+ "."
+ overriddenMethod.toString()
+ " returns @NonNull";
} else {
message =
"method returns @Nullable, but superclass method "
+ ASTHelpers.enclosingClass(overriddenMethod)
+ "."
+ overriddenMethod.toString()
+ " returns @NonNull";
}
Tree errorTree =
memberReferenceTree != null
? memberReferenceTree
: getTreesInstance(state).getTree(overridingMethod);
Tree suggestTree =
memberReferenceTree != null
? NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(state.getPath()).getLeaf()
: errorTree;
return createErrorDescription(
MessageTypes.WRONG_OVERRIDE_RETURN, errorTree, message, suggestTree);
}
// if any parameter in the super method is annotated @Nullable,
// overriding method cannot assume @Nonnull
return checkParamOverriding(
overridingMethod.getParameters(), overriddenMethod, null, memberReferenceTree, state);
}
@Override
public Description matchIdentifier(IdentifierTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
return checkForReadBeforeInit(tree, state);
}
private Description checkForReadBeforeInit(ExpressionTree tree, VisitorState state) {
// do a bunch of filtering. first, filter out anything outside an initializer
TreePath path = state.getPath();
TreePath enclosingBlockPath = NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(path);
if (enclosingBlockPath == null) {
// is this possible?
return Description.NO_MATCH;
}
if (!relevantInitializerMethodOrBlock(enclosingBlockPath, state)) {
return Description.NO_MATCH;
}
// now, make sure we have a field read
Symbol symbol = ASTHelpers.getSymbol(tree);
if (symbol == null) {
return Description.NO_MATCH;
}
if (!symbol.getKind().equals(ElementKind.FIELD)) {
return Description.NO_MATCH;
}
// for static fields, make sure the enclosing init is a static method or block
if (symbol.isStatic()) {
Tree enclosing = enclosingBlockPath.getLeaf();
if (enclosing instanceof MethodTree
&& !ASTHelpers.getSymbol((MethodTree) enclosing).isStatic()) {
return Description.NO_MATCH;
} else if (enclosing instanceof BlockTree && !((BlockTree) enclosing).isStatic()) {
return Description.NO_MATCH;
}
}
if (okToReadBeforeInitialized(path)) {
// writing the field, not reading it
return Description.NO_MATCH;
}
// check that the field might actually be problematic to read
FieldInitEntities entities = class2Entities.get(enclosingClassSymbol(enclosingBlockPath));
if (!(entities.nonnullInstanceFields().contains(symbol)
|| entities.nonnullStaticFields().contains(symbol))) {
// field is either nullable or initialized at declaration
return Description.NO_MATCH;
}
if (symbolHasSuppressInitalizationWarningsAnnotation(symbol)) {
// also suppress checking read before init, as we may not find explicit initialization
return Description.NO_MATCH;
}
return checkPossibleUninitFieldRead(tree, state, symbol, path, enclosingBlockPath);
}
private Symbol.ClassSymbol enclosingClassSymbol(TreePath enclosingBlockPath) {
Tree leaf = enclosingBlockPath.getLeaf();
if (leaf instanceof BlockTree) {
// parent must be a ClassTree
Tree parent = enclosingBlockPath.getParentPath().getLeaf();
return ASTHelpers.getSymbol((ClassTree) parent);
} else {
return ASTHelpers.enclosingClass(ASTHelpers.getSymbol(leaf));
}
}
private boolean relevantInitializerMethodOrBlock(
TreePath enclosingBlockPath, VisitorState state) {
Tree methodLambdaOrBlock = enclosingBlockPath.getLeaf();
if (methodLambdaOrBlock instanceof LambdaExpressionTree) {
return false;
} else if (methodLambdaOrBlock instanceof MethodTree) {
MethodTree methodTree = (MethodTree) methodLambdaOrBlock;
if (isConstructor(methodTree) && !constructorInvokesAnother(methodTree, state)) return true;
if (ASTHelpers.getSymbol(methodTree).isStatic()) {
Set<MethodTree> staticInitializerMethods =
class2Entities.get(enclosingClassSymbol(enclosingBlockPath)).staticInitializerMethods();
return staticInitializerMethods.size() == 1
&& staticInitializerMethods.contains(methodTree);
} else {
Set<MethodTree> instanceInitializerMethods =
class2Entities
.get(enclosingClassSymbol(enclosingBlockPath))
.instanceInitializerMethods();
return instanceInitializerMethods.size() == 1
&& instanceInitializerMethods.contains(methodTree);
}
} else {
// initializer or field declaration
return true;
}
}
private Description checkPossibleUninitFieldRead(
ExpressionTree tree,
VisitorState state,
Symbol symbol,
TreePath path,
TreePath enclosingBlockPath) {
if (!fieldInitializedByPreviousInitializer(symbol, enclosingBlockPath, state)
&& !fieldAlwaysInitializedBeforeRead(symbol, path, state, enclosingBlockPath)) {
return createErrorDescription(
MessageTypes.NONNULL_FIELD_READ_BEFORE_INIT,
tree,
"read of @NonNull field " + symbol + " before initialization",
path);
} else {
return Description.NO_MATCH;
}
}
/**
* @param symbol the field being read
* @param pathToRead TreePath to the read operation
* @param state visitor state
* @param enclosingBlockPath TreePath to enclosing initializer block
* @return true if within the initializer, the field is always initialized before the read
* operation, false otherwise
*/
private boolean fieldAlwaysInitializedBeforeRead(
Symbol symbol, TreePath pathToRead, VisitorState state, TreePath enclosingBlockPath) {
AccessPathNullnessAnalysis nullnessAnalysis = getNullnessAnalysis(state);
Set<Element> nonnullFields;
if (symbol.isStatic()) {
nonnullFields = nullnessAnalysis.getNonnullStaticFieldsBefore(pathToRead, state.context);
} else {
nonnullFields = new LinkedHashSet<>();
nonnullFields.addAll(
nullnessAnalysis.getNonnullFieldsOfReceiverBefore(pathToRead, state.context));
nonnullFields.addAll(safeInitByCalleeBefore(pathToRead, state, enclosingBlockPath));
}
return nonnullFields.contains(symbol);
}
/**
* computes those fields always initialized by callee safe init methods before a read operation
* (pathToRead) is invoked. See <a
* href="https://github.com/uber/NullAway/wiki/Error-Messages#initializer-method-does-not-guarantee-nonnull-field-is-initialized--nonnull-field--not-initialized">the
* docs</a> for what is considered a safe initializer method.
*/
private Set<Element> safeInitByCalleeBefore(
TreePath pathToRead, VisitorState state, TreePath enclosingBlockPath) {
Set<Element> result = new LinkedHashSet<>();
Set<Element> safeInitMethods = new LinkedHashSet<>();
Tree enclosingBlockOrMethod = enclosingBlockPath.getLeaf();
if (enclosingBlockOrMethod instanceof VariableTree) {
return Collections.emptySet();
}
BlockTree blockTree =
enclosingBlockOrMethod instanceof BlockTree
? (BlockTree) enclosingBlockOrMethod
: ((MethodTree) enclosingBlockOrMethod).getBody();
List<? extends StatementTree> statements = blockTree.getStatements();
Tree readExprTree = pathToRead.getLeaf();
int readStartPos = getStartPos((JCTree) readExprTree);
TreePath classTreePath = enclosingBlockPath;
// look for the parent ClassTree node, which represents the enclosing class / enum / interface
while (!(classTreePath.getLeaf() instanceof ClassTree)) {
classTreePath = classTreePath.getParentPath();
if (classTreePath == null) {
throw new IllegalStateException(
"could not find enclosing class / enum / interface for "
+ enclosingBlockPath.getLeaf());
}
}
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol((ClassTree) classTreePath.getLeaf());
for (int i = 0; i < statements.size(); i++) {
StatementTree curStmt = statements.get(i);
if (getStartPos((JCTree) curStmt) <= readStartPos) {
Element privMethodElem = getInvokeOfSafeInitMethod(curStmt, classSymbol, state);
if (privMethodElem != null) {
safeInitMethods.add(privMethodElem);
}
// Hack: Handling try{...}finally{...} statement, see getSafeInitMethods
if (curStmt.getKind().equals(Tree.Kind.TRY)) {
TryTree tryTree = (TryTree) curStmt;
// ToDo: Should we check initialization inside tryTree.getResources ? What is the scope of
// that initialization?
if (tryTree.getCatches().size() == 0) {
if (tryTree.getBlock() != null) {
result.addAll(
safeInitByCalleeBefore(
pathToRead, state, new TreePath(enclosingBlockPath, tryTree.getBlock())));
}
if (tryTree.getFinallyBlock() != null) {
result.addAll(
safeInitByCalleeBefore(
pathToRead,
state,
new TreePath(enclosingBlockPath, tryTree.getFinallyBlock())));
}
}
}
}
}
addGuaranteedNonNullFromInvokes(
state, getTreesInstance(state), safeInitMethods, getNullnessAnalysis(state), result);
return result;
}
private int getStartPos(JCTree tree) {
return tree.pos().getStartPosition();
}
/**
* @param fieldSymbol the field
* @param initTreePath TreePath to the initializer method / block
* @param state visitor state
* @return true if the field is always initialized (by some other initializer) before the
* initializer corresponding to initTreePath executes
*/
private boolean fieldInitializedByPreviousInitializer(
Symbol fieldSymbol, TreePath initTreePath, VisitorState state) {
TreePath enclosingClassPath = initTreePath.getParentPath();
ClassTree enclosingClass = (ClassTree) enclosingClassPath.getLeaf();
Multimap<Tree, Element> tree2Init =
initTree2PrevFieldInit.get(ASTHelpers.getSymbol(enclosingClass));
if (tree2Init == null) {
tree2Init = computeTree2Init(enclosingClassPath, state);
initTree2PrevFieldInit.put(ASTHelpers.getSymbol(enclosingClass), tree2Init);
}
return tree2Init.containsEntry(initTreePath.getLeaf(), fieldSymbol);
}
/**
* @param enclosingClassPath TreePath to class
* @param state visitor state
* @return a map from each initializer <em>i</em> to the fields known to be initialized before
* <em>i</em> executes
*/
private Multimap<Tree, Element> computeTree2Init(
TreePath enclosingClassPath, VisitorState state) {
ClassTree enclosingClass = (ClassTree) enclosingClassPath.getLeaf();
ImmutableMultimap.Builder<Tree, Element> builder = ImmutableMultimap.builder();
// NOTE: this set includes both instance and static fields
Set<Element> initThusFar = new LinkedHashSet<>();
Set<MethodTree> constructors = new LinkedHashSet<>();
AccessPathNullnessAnalysis nullnessAnalysis = getNullnessAnalysis(state);
// NOTE: we assume the members are returned in their syntactic order. This has held
// true in our testing
for (Tree memberTree : enclosingClass.getMembers()) {
if (memberTree instanceof VariableTree || memberTree instanceof BlockTree) {
// putAll does not keep a reference to initThusFar, so we don't need to make a copy here
builder.putAll(memberTree, initThusFar);
}
if (memberTree instanceof BlockTree) {
BlockTree blockTree = (BlockTree) memberTree;
// add whatever gets initialized here
TreePath memberPath = new TreePath(enclosingClassPath, memberTree);
if (blockTree.isStatic()) {
initThusFar.addAll(
nullnessAnalysis.getNonnullStaticFieldsAtExit(memberPath, state.context));
} else {
initThusFar.addAll(
nullnessAnalysis.getNonnullFieldsOfReceiverAtExit(memberPath, state.context));
}
}
if (memberTree instanceof MethodTree) {
MethodTree methodTree = (MethodTree) memberTree;
if (isConstructor(methodTree)) {
constructors.add(methodTree);
}
}
}
// all the initializer blocks have run before any code inside a constructor
constructors.stream().forEach((c) -> builder.putAll(c, initThusFar));
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(enclosingClass);
FieldInitEntities entities = class2Entities.get(classSymbol);
if (entities.instanceInitializerMethods().size() == 1) {
MethodTree initMethod = entities.instanceInitializerMethods().iterator().next();
// collect the fields that may not be initialized by *some* constructor NC
Set<Symbol> constructorUninitSymbols = class2ConstructorUninit.get(classSymbol);
// fields initialized after constructors is initThusFar + (nonNullFields - constructorUninit)
Sets.SetView<Element> initAfterConstructors =
Sets.union(
initThusFar,
Sets.difference(entities.nonnullInstanceFields(), constructorUninitSymbols));
builder.putAll(initMethod, initAfterConstructors);
}
if (entities.staticInitializerMethods().size() == 1) {
MethodTree staticInitMethod = entities.staticInitializerMethods().iterator().next();
// constructors aren't relevant here; just use initThusFar
builder.putAll(staticInitMethod, initThusFar);
}
return builder.build();
}
/**
* @param path tree path to read operation
* @return true if it is permissible to perform this read before the field has been initialized,
* false otherwise
*/
private boolean okToReadBeforeInitialized(TreePath path) {
TreePath parentPath = path.getParentPath();
Tree leaf = path.getLeaf();
Tree parent = parentPath.getLeaf();
if (parent instanceof AssignmentTree) {
// ok if it's actually a write
AssignmentTree assignment = (AssignmentTree) parent;
return assignment.getVariable().equals(leaf);
} else if (parent instanceof BinaryTree) {
// ok if we're comparing to null
BinaryTree binaryTree = (BinaryTree) parent;
Tree.Kind kind = binaryTree.getKind();
if (kind.equals(Tree.Kind.EQUAL_TO) || kind.equals(Tree.Kind.NOT_EQUAL_TO)) {
ExpressionTree left = binaryTree.getLeftOperand();
ExpressionTree right = binaryTree.getRightOperand();
return (left.equals(leaf) && right.getKind().equals(Tree.Kind.NULL_LITERAL))
|| (right.equals(leaf) && left.getKind().equals(Tree.Kind.NULL_LITERAL));
}
} else if (parent instanceof MethodInvocationTree) {
// ok if it's invoking castToNonNull and the read is the argument
MethodInvocationTree methodInvoke = (MethodInvocationTree) parent;
Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(methodInvoke);
String qualifiedName =
ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
if (qualifiedName.equals(config.getCastToNonNullMethod())) {
List<? extends ExpressionTree> arguments = methodInvoke.getArguments();
return arguments.size() == 1 && leaf.equals(arguments.get(0));
}
}
return false;
}
private boolean symbolHasSuppressInitalizationWarningsAnnotation(Symbol symbol) {
SuppressWarnings annotation = symbol.getAnnotation(SuppressWarnings.class);
if (annotation != null) {
for (String s : annotation.value()) {
// we need to check for standard suppressions here also since we may report initialization
// errors outside the normal ErrorProne match* methods
if (s.equals(INITIALIZATION_CHECK_NAME) || allNames().stream().anyMatch(s::equals)) {
return true;
}
}
}
return false;
}
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
VarSymbol symbol = ASTHelpers.getSymbol(tree);
if (symbol.type.isPrimitive() && tree.getInitializer() != null) {
return doUnboxingCheck(state, tree.getInitializer());
}
if (!symbol.getKind().equals(ElementKind.FIELD)) {
return Description.NO_MATCH;
}
ExpressionTree initializer = tree.getInitializer();
if (initializer != null) {
if (!symbol.type.isPrimitive() && !skipDueToFieldAnnotation(symbol)) {
if (mayBeNullExpr(state, initializer)) {
return createErrorDescriptionForNullAssignment(
MessageTypes.ASSIGN_FIELD_NULLABLE,
tree,
"assigning @Nullable expression to @NonNull field",
initializer,
tree);
}
}
}
return Description.NO_MATCH;
}
@Override
public Description matchClass(ClassTree tree, VisitorState state) {
// check if the class is excluded according to the filter
// if so, set the flag to match within the class to false
// NOTE: for this mechanism to work, we rely on the enclosing ClassTree
// always being visited before code within that class. We also
// assume that a single checker object is not being
// used from multiple threads
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(tree);
// we don't want to update the flag for nested classes.
// ideally we would keep a stack of flags to handle nested types,
// but this is not easy within the Error Prone APIs
NestingKind nestingKind = classSymbol.getNestingKind();
if (!nestingKind.isNested()) {
matchWithinClass = !isExcludedClass(classSymbol, state);
// since we are processing a new top-level class, invalidate any cached
// results for previous classes
handler.onMatchTopLevelClass(this, tree, state, classSymbol);
getNullnessAnalysis(state).invalidateCaches();
initTree2PrevFieldInit.clear();
class2Entities.clear();
class2ConstructorUninit.clear();
computedNullnessMap.clear();
EnclosingEnvironmentNullness.instance(state.context).clear();
}
if (matchWithinClass) {
// we need to update the environment before checking field initialization, as the latter
// may run dataflow analysis
if (nestingKind.equals(NestingKind.LOCAL) || nestingKind.equals(NestingKind.ANONYMOUS)) {
updateEnvironmentMapping(tree, state);
}
checkFieldInitialization(tree, state);
}
return Description.NO_MATCH;
}
// UNBOXING CHECKS
@Override
public Description matchBinary(BinaryTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
ExpressionTree leftOperand = tree.getLeftOperand();
ExpressionTree rightOperand = tree.getRightOperand();
Type leftType = ASTHelpers.getType(leftOperand);
Type rightType = ASTHelpers.getType(rightOperand);
if (leftType == null || rightType == null) {
throw new RuntimeException();
}
if (leftType.isPrimitive() && !rightType.isPrimitive()) {
return doUnboxingCheck(state, rightOperand);
}
if (rightType.isPrimitive() && !leftType.isPrimitive()) {
return doUnboxingCheck(state, leftOperand);
}
return Description.NO_MATCH;
}
@Override
public Description matchUnary(UnaryTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
return doUnboxingCheck(state, tree.getExpression());
}
@Override
public Description matchConditionalExpression(
ConditionalExpressionTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
return doUnboxingCheck(state, tree.getCondition());
}
@Override
public Description matchIf(IfTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
return doUnboxingCheck(state, tree.getCondition());
}
@Override
public Description matchWhileLoop(WhileLoopTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
return doUnboxingCheck(state, tree.getCondition());
}
@Override
public Description matchForLoop(ForLoopTree tree, VisitorState state) {
if (!matchWithinClass) {
return Description.NO_MATCH;
}
if (tree.getCondition() != null) {
return doUnboxingCheck(state, tree.getCondition());
}
return Description.NO_MATCH;
}
/**
* if any expression has non-primitive type, we should check that it can't be null as it is
* getting unboxed
*
* @param expressions expressions to check
* @return error Description if an error is found, otherwise NO_MATCH
*/
private Description doUnboxingCheck(VisitorState state, ExpressionTree... expressions) {
for (ExpressionTree tree : expressions) {
Type type = ASTHelpers.getType(tree);
if (type == null) {
throw new RuntimeException("was not expecting null type");
}
if (!type.isPrimitive()) {
if (mayBeNullExpr(state, tree)) {
return createErrorDescription(
MessageTypes.UNBOX_NULLABLE, tree, "unboxing of a @Nullable value", state.getPath());
}
}
}
return Description.NO_MATCH;
}
/**
* handle either a method invocation or a 'new' invocation
*
* @param tree the corresponding MethodInvocationTree or NewClassTree
* @param state visitor state
* @param methodSymbol symbol for invoked method
* @param actualParams parameters passed at call
* @return description of error or NO_MATCH if no error
*/
private Description handleInvocation(
Tree tree,
VisitorState state,
Symbol.MethodSymbol methodSymbol,
List<? extends ExpressionTree> actualParams) {
ImmutableSet<Integer> nonNullPositions = null;
if (NullabilityUtil.isUnannotated(methodSymbol, config)) {
nonNullPositions =
handler.onUnannotatedInvocationGetNonNullPositions(
this, state, methodSymbol, actualParams, ImmutableSet.of());
}
if (nonNullPositions == null) {
ImmutableSet.Builder<Integer> builder = ImmutableSet.builder();
// compute which arguments are @NonNull
List<VarSymbol> formalParams = methodSymbol.getParameters();
for (int i = 0; i < formalParams.size(); i++) {
if (i == formalParams.size() - 1 && methodSymbol.isVarArgs()) {
// eventually, handle this case properly. I *think* a null
// array could be passed in incorrectly. For now, punt
continue;
}
VarSymbol param = formalParams.get(i);
if (param.type.isPrimitive()) {
Description unboxingCheck = doUnboxingCheck(state, actualParams.get(i));
if (unboxingCheck != Description.NO_MATCH) {
return unboxingCheck;
} else {
continue;
}
}
// we need to call paramHasNullableAnnotation here since the invoked method may be defined
// in a class file
if (!Nullness.paramHasNullableAnnotation(methodSymbol, i)) {
builder.add(i);
}
}
nonNullPositions = builder.build();
}
// now actually check the arguments
// NOTE: the case of an invocation on a possibly-null reference
// is handled by matchMemberSelect()
for (int argPos : nonNullPositions) {
// make sure we are passing a non-null value
ExpressionTree actual = actualParams.get(argPos);
if (mayBeNullExpr(state, actual)) {
String message =
"passing @Nullable parameter '" + actual.toString() + "' where @NonNull is required";
return createErrorDescriptionForNullAssignment(
MessageTypes.PASS_NULLABLE, actual, message, actual, state.getPath());
}
}
// Check for @NonNull being passed to castToNonNull (if configured)
return checkCastToNonNullTakesNullable(tree, state, methodSymbol, actualParams);
}
private Description checkCastToNonNullTakesNullable(
Tree tree,
VisitorState state,
Symbol.MethodSymbol methodSymbol,
List<? extends ExpressionTree> actualParams) {
String qualifiedName =
ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
if (qualifiedName.equals(config.getCastToNonNullMethod())) {
if (actualParams.size() != 1) {
throw new RuntimeException(
"Invalid number of parameters passed to configured CastToNonNullMethod.");
}
ExpressionTree actual = actualParams.get(0);
TreePath enclosingMethodOrLambda =
NullabilityUtil.findEnclosingMethodOrLambdaOrInitializer(state.getPath());
boolean isInitializer;
if (enclosingMethodOrLambda == null) {
throw new RuntimeException("no enclosing method, lambda or initializer!");
} else if (enclosingMethodOrLambda.getLeaf() instanceof LambdaExpressionTree) {
isInitializer = false;
} else if (enclosingMethodOrLambda.getLeaf() instanceof MethodTree) {
MethodTree enclosingMethod = (MethodTree) enclosingMethodOrLambda.getLeaf();
isInitializer = isInitializerMethod(state, ASTHelpers.getSymbol(enclosingMethod));
} else {
// Initializer block
isInitializer = true;
}
MethodTree enclosingMethod = ASTHelpers.findEnclosingNode(state.getPath(), MethodTree.class);
if (!isInitializer && !mayBeNullExpr(state, actual)) {
String message =
"passing known @NonNull parameter '"
+ actual.toString()
+ "' to CastToNonNullMethod ("
+ qualifiedName
+ "). This method should only take arguments that NullAway considers @Nullable "
+ "at the invocation site, but which are known not to be null at runtime.";
return createErrorDescription(
MessageTypes.CAST_TO_NONNULL_ARG_NONNULL, tree, message, tree);
}
}
return Description.NO_MATCH;
}
/**
* check that all @NonNull fields of the class are properly initialized
*
* @param tree the class
* @param state visitor state
*/
private void checkFieldInitialization(ClassTree tree, VisitorState state) {
FieldInitEntities entities = collectEntities(tree, state);
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(tree);
class2Entities.put(classSymbol, entities);
// set of all non-null instance fields f such that *some* constructor does not initialize f
Set<Symbol> notInitializedInConstructors;
SetMultimap<MethodTree, Symbol> constructorInitInfo;
if (entities.constructors().isEmpty()) {
constructorInitInfo = null;
notInitializedInConstructors = entities.nonnullInstanceFields();
} else {
constructorInitInfo = checkConstructorInitialization(entities, state);
notInitializedInConstructors = ImmutableSet.copyOf(constructorInitInfo.values());
}
class2ConstructorUninit.putAll(classSymbol, notInitializedInConstructors);
Set<Symbol> notInitializedAtAll =
notAssignedInAnyInitializer(entities, notInitializedInConstructors, state);
SetMultimap<Element, Element> errorFieldsForInitializer = LinkedHashMultimap.create();
// non-null if we have a single initializer method
Symbol.MethodSymbol singleInitializerMethod = null;
if (entities.instanceInitializerMethods().size() == 1) {
singleInitializerMethod =
ASTHelpers.getSymbol(entities.instanceInitializerMethods().iterator().next());
}
for (Symbol uninitField : notInitializedAtAll) {
if (singleInitializerMethod != null) {
// report it on the initializer
errorFieldsForInitializer.put(singleInitializerMethod, uninitField);
} else if (constructorInitInfo == null) {
// report it on the field, except in the case where the class is externalInit and
// we have no initializer methods
if (!(isExternalInit(classSymbol) && entities.instanceInitializerMethods().isEmpty())) {
reportInitErrorOnField(uninitField, state);
}
} else {
// report it on each constructor that does not initialize it
for (MethodTree methodTree : constructorInitInfo.keySet()) {
Set<Symbol> uninitFieldsForConstructor = constructorInitInfo.get(methodTree);
if (uninitFieldsForConstructor.contains(uninitField)) {
errorFieldsForInitializer.put(ASTHelpers.getSymbol(methodTree), uninitField);
}
}
}
}
for (Element constructorElement : errorFieldsForInitializer.keySet()) {
reportInitializerError(
(Symbol.MethodSymbol) constructorElement,
errMsgForInitializer(errorFieldsForInitializer.get(constructorElement)),
state);
}
// For static fields
Set<Symbol> notInitializedStaticFields = notInitializedStatic(entities, state);
for (Symbol uninitSField : notInitializedStaticFields) {
// Always report it on the field for static fields (can't do @SuppressWarnings on a static
// initialization block
// anyways).
reportInitErrorOnField(uninitSField, state);
}
}
/**
* @param entities relevant entities from class
* @param notInitializedInConstructors those fields not initialized in some constructor
* @param state
* @return those fields from notInitializedInConstructors that are not initialized in any
* initializer method
*/
private Set<Symbol> notAssignedInAnyInitializer(
FieldInitEntities entities, Set<Symbol> notInitializedInConstructors, VisitorState state) {
Trees trees = getTreesInstance(state);
Symbol.ClassSymbol classSymbol = entities.classSymbol();
Set<Element> initInSomeInitializer = new LinkedHashSet<>();
for (MethodTree initMethodTree : entities.instanceInitializerMethods()) {
if (initMethodTree.getBody() == null) {
continue;
}
addInitializedFieldsForBlock(
state,
trees,
classSymbol,
initInSomeInitializer,
initMethodTree.getBody(),
new TreePath(state.getPath(), initMethodTree));
}
for (BlockTree block : entities.instanceInitializerBlocks()) {
addInitializedFieldsForBlock(
state,
trees,
classSymbol,
initInSomeInitializer,
block,
new TreePath(state.getPath(), block));
}
Set<Symbol> result = new LinkedHashSet<>();
for (Symbol fieldSymbol : notInitializedInConstructors) {
if (!initInSomeInitializer.contains(fieldSymbol)) {
result.add(fieldSymbol);
}
}
return result;
}
private void addInitializedFieldsForBlock(
VisitorState state,
Trees trees,
Symbol.ClassSymbol classSymbol,
Set<Element> initInSomeInitializer,
BlockTree block,
TreePath path) {
AccessPathNullnessAnalysis nullnessAnalysis = getNullnessAnalysis(state);
Set<Element> nonnullAtExit =
nullnessAnalysis.getNonnullFieldsOfReceiverAtExit(path, state.context);
initInSomeInitializer.addAll(nonnullAtExit);
Set<Element> safeInitMethods = getSafeInitMethods(block, classSymbol, state);
addGuaranteedNonNullFromInvokes(
state, trees, safeInitMethods, nullnessAnalysis, initInSomeInitializer);
}
/**
* @param entities field init info
* @param state visitor state
* @return a map from each constructor C to the nonnull fields that C does *not* initialize
*/
private SetMultimap<MethodTree, Symbol> checkConstructorInitialization(
FieldInitEntities entities, VisitorState state) {
SetMultimap<MethodTree, Symbol> result = LinkedHashMultimap.create();
Set<Symbol> nonnullInstanceFields = entities.nonnullInstanceFields();
Trees trees = getTreesInstance(state);
boolean isExternalInit = isExternalInit(entities.classSymbol());
for (MethodTree constructor : entities.constructors()) {
if (constructorInvokesAnother(constructor, state)) {
continue;
}
if (constructor.getParameters().size() == 0 && isExternalInit) {
// external framework initializes fields in this case
continue;
}
Set<Element> guaranteedNonNull =
guaranteedNonNullForConstructor(entities, state, trees, constructor);
for (Symbol fieldSymbol : nonnullInstanceFields) {
if (!guaranteedNonNull.contains(fieldSymbol)) {
result.put(constructor, fieldSymbol);
}
}
}
return result;
}
private boolean isExternalInit(Symbol.ClassSymbol classSymbol) {
return StreamSupport.stream(NullabilityUtil.getAllAnnotations(classSymbol).spliterator(), false)
.map((anno) -> anno.getAnnotationType().toString())
.anyMatch(config::isExternalInitClassAnnotation);
}
private Set<Element> guaranteedNonNullForConstructor(
FieldInitEntities entities, VisitorState state, Trees trees, MethodTree constructor) {
Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(constructor);
Set<Element> safeInitMethods =
getSafeInitMethods(constructor.getBody(), entities.classSymbol(), state);
AccessPathNullnessAnalysis nullnessAnalysis = getNullnessAnalysis(state);
Set<Element> guaranteedNonNull = new LinkedHashSet<>();
guaranteedNonNull.addAll(
nullnessAnalysis.getNonnullFieldsOfReceiverAtExit(
new TreePath(state.getPath(), constructor), state.context));
addGuaranteedNonNullFromInvokes(
state, trees, safeInitMethods, nullnessAnalysis, guaranteedNonNull);
return guaranteedNonNull;
}
/** does the constructor invoke another constructor in the same class via this(...)? */
private boolean constructorInvokesAnother(MethodTree constructor, VisitorState state) {
BlockTree body = constructor.getBody();
List<? extends StatementTree> statements = body.getStatements();
if (statements.size() > 0) {
StatementTree statementTree = statements.get(0);
if (isThisCall(statementTree, state)) {
return true;
}
}
return false;
}
private Set<Symbol> notInitializedStatic(FieldInitEntities entities, VisitorState state) {
Set<Symbol> nonNullStaticFields = entities.nonnullStaticFields();
Set<Element> initializedInStaticInitializers = new LinkedHashSet<Element>();
AccessPathNullnessAnalysis nullnessAnalysis = getNullnessAnalysis(state);
for (BlockTree initializer : entities.staticInitializerBlocks()) {
Set<Element> nonnullAtExit =
nullnessAnalysis.getNonnullStaticFieldsAtExit(
new TreePath(state.getPath(), initializer), state.context);
initializedInStaticInitializers.addAll(nonnullAtExit);
}
for (MethodTree initializerMethod : entities.staticInitializerMethods()) {
Set<Element> nonnullAtExit =
nullnessAnalysis.getNonnullStaticFieldsAtExit(
new TreePath(state.getPath(), initializerMethod), state.context);
initializedInStaticInitializers.addAll(nonnullAtExit);
}
Set<Symbol> notInitializedStaticFields = new LinkedHashSet<Symbol>();
for (Symbol field : nonNullStaticFields) {
if (!initializedInStaticInitializers.contains(field)) {
notInitializedStaticFields.add(field);
}
}
return notInitializedStaticFields;
}
private void addGuaranteedNonNullFromInvokes(
VisitorState state,
Trees trees,
Set<Element> safeInitMethods,
AccessPathNullnessAnalysis nullnessAnalysis,
Set<Element> guaranteedNonNull) {
for (Element invoked : safeInitMethods) {
Tree invokedTree = trees.getTree(invoked);
guaranteedNonNull.addAll(
nullnessAnalysis.getNonnullFieldsOfReceiverAtExit(
new TreePath(state.getPath(), invokedTree), state.context));
}
}
/**
* @param blockTree block of statements
* @param state visitor state
* @return Elements of safe init methods that are invoked as top-level statements in the method
*/
private Set<Element> getSafeInitMethods(
BlockTree blockTree, Symbol.ClassSymbol classSymbol, VisitorState state) {
Set<Element> result = new LinkedHashSet<>();
List<? extends StatementTree> statements = blockTree.getStatements();
for (StatementTree stmt : statements) {
Element privMethodElem = getInvokeOfSafeInitMethod(stmt, classSymbol, state);
if (privMethodElem != null) {
result.add(privMethodElem);
}
// Hack: If we see a try{...}finally{...} statement, without a catch, we consider the methods
// inside both blocks
// as "top level" for the purposes of finding initialization methods. Any exception happening
// there is also an
// exception of the full method.
if (stmt.getKind().equals(Tree.Kind.TRY)) {
TryTree tryTree = (TryTree) stmt;
if (tryTree.getCatches().size() == 0) {
if (tryTree.getBlock() != null) {
result.addAll(getSafeInitMethods(tryTree.getBlock(), classSymbol, state));
}
if (tryTree.getFinallyBlock() != null) {
result.addAll(getSafeInitMethods(tryTree.getFinallyBlock(), classSymbol, state));
}
}
}
}
return result;
}
/**
* A safe init method is an instance method that is either private or final (so no overriding is
* possible)
*
* @param stmt the statement
* @param enclosingClassSymbol symbol for enclosing constructor / initializer
* @param state visitor state
* @return element of safe init function if stmt invokes that function; null otherwise
*/
@Nullable
private Element getInvokeOfSafeInitMethod(
StatementTree stmt, final Symbol.ClassSymbol enclosingClassSymbol, VisitorState state) {
Matcher<ExpressionTree> invokeMatcher =
(expressionTree, s) -> {
if (!(expressionTree instanceof MethodInvocationTree)) {
return false;
}
MethodInvocationTree methodInvocationTree = (MethodInvocationTree) expressionTree;
Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(methodInvocationTree);
Set<Modifier> modifiers = symbol.getModifiers();
if ((symbol.isPrivate() || modifiers.contains(Modifier.FINAL))
&& !symbol.isStatic()
&& !modifiers.contains(Modifier.NATIVE)) {
// check it's the same class (could be an issue with inner classes)
if (ASTHelpers.enclosingClass(symbol).equals(enclosingClassSymbol)) {
// make sure the receiver is 'this'
ExpressionTree receiver = ASTHelpers.getReceiver(expressionTree);
return receiver == null || isThisIdentifier(receiver);
}
}
return false;
};
if (stmt.getKind().equals(EXPRESSION_STATEMENT)) {
ExpressionTree expression = ((ExpressionStatementTree) stmt).getExpression();
if (invokeMatcher.matches(expression, state)) {
return ASTHelpers.getSymbol(expression);
}
}
return null;
}
private boolean isThisCall(StatementTree statementTree, VisitorState state) {
if (statementTree.getKind().equals(EXPRESSION_STATEMENT)) {
ExpressionTree expression = ((ExpressionStatementTree) statementTree).getExpression();
return Matchers.methodInvocation(THIS_MATCHER).matches(expression, state);
}
return false;
}
private FieldInitEntities collectEntities(ClassTree tree, VisitorState state) {
Symbol.ClassSymbol classSymbol = ASTHelpers.getSymbol(tree);
Set<Symbol> nonnullInstanceFields = new LinkedHashSet<>();
Set<Symbol> nonnullStaticFields = new LinkedHashSet<>();
List<BlockTree> instanceInitializerBlocks = new ArrayList<>();
List<BlockTree> staticInitializerBlocks = new ArrayList<>();
Set<MethodTree> constructors = new LinkedHashSet<>();
Set<MethodTree> instanceInitializerMethods = new LinkedHashSet<>();
Set<MethodTree> staticInitializerMethods = new LinkedHashSet<>();
// we assume getMembers() returns members in the same order as the declarations
for (Tree memberTree : tree.getMembers()) {
switch (memberTree.getKind()) {
case METHOD:
// check if it is a constructor or an @Initializer method
MethodTree methodTree = (MethodTree) memberTree;
Symbol.MethodSymbol symbol = ASTHelpers.getSymbol(methodTree);
if (isConstructor(methodTree)) {
constructors.add(methodTree);
} else if (isInitializerMethod(state, symbol)) {
if (symbol.isStatic()) {
staticInitializerMethods.add(methodTree);
} else {
instanceInitializerMethods.add(methodTree);
}
}
break;
case VARIABLE:
// field declaration
VariableTree varTree = (VariableTree) memberTree;
Symbol fieldSymbol = ASTHelpers.getSymbol(varTree);
if (fieldSymbol.type.isPrimitive() || skipDueToFieldAnnotation(fieldSymbol)) {
continue;
}
if (varTree.getInitializer() != null) {
// note that we check that the initializer does the right thing in
// matchVariable()
continue;
}
if (fieldSymbol.isStatic()) {
nonnullStaticFields.add(fieldSymbol);
} else {
nonnullInstanceFields.add(fieldSymbol);
}
break;
case BLOCK:
// initializer block
BlockTree blockTree = (BlockTree) memberTree;
if (blockTree.isStatic()) {
staticInitializerBlocks.add(blockTree);
} else {
instanceInitializerBlocks.add(blockTree);
}
break;
case ENUM:
case CLASS:
case INTERFACE:
case ANNOTATION_TYPE:
// do nothing
break;
default:
throw new RuntimeException(memberTree.getKind().toString() + " " + memberTree);
}
}
return FieldInitEntities.create(
classSymbol,
ImmutableSet.copyOf(nonnullInstanceFields),
ImmutableSet.copyOf(nonnullStaticFields),
ImmutableList.copyOf(instanceInitializerBlocks),
ImmutableList.copyOf(staticInitializerBlocks),
ImmutableSet.copyOf(constructors),
ImmutableSet.copyOf(instanceInitializerMethods),
ImmutableSet.copyOf(staticInitializerMethods));
}
private boolean isConstructor(MethodTree methodTree) {
return ASTHelpers.getSymbol(methodTree).isConstructor()
&& !ASTHelpers.isGeneratedConstructor(methodTree);
}
private boolean isInitializerMethod(VisitorState state, Symbol.MethodSymbol symbol) {
if (ASTHelpers.hasDirectAnnotationWithSimpleName(symbol, "Initializer")
|| config.isKnownInitializerMethod(symbol)) {
return true;
}
for (AnnotationMirror anno : symbol.getAnnotationMirrors()) {
String annoTypeStr = anno.getAnnotationType().toString();
if (config.isInitializerMethodAnnotation(annoTypeStr)) {
return true;
}
}
Symbol.MethodSymbol closestOverriddenMethod =
getClosestOverriddenMethod(symbol, state.getTypes());
if (closestOverriddenMethod == null) {
return false;
}
return isInitializerMethod(state, closestOverriddenMethod);
}
private boolean skipDueToFieldAnnotation(Symbol fieldSymbol) {
return NullabilityUtil.getAllAnnotations(fieldSymbol)
.map(anno -> anno.getAnnotationType().toString())
.anyMatch(config::isExcludedFieldAnnotation);
}
private boolean isExcludedClass(Symbol.ClassSymbol classSymbol, VisitorState state) {
String className = classSymbol.getQualifiedName().toString();
if (config.isExcludedClass(className)) {
return true;
}
if (!config.fromAnnotatedPackage(classSymbol)) {
return true;
}
// check annotations
ImmutableSet<String> excludedClassAnnotations = config.getExcludedClassAnnotations();
return classSymbol
.getAnnotationMirrors()
.stream()
.map(anno -> anno.getAnnotationType().toString())
.anyMatch(excludedClassAnnotations::contains);
}
private boolean mayBeNullExpr(VisitorState state, ExpressionTree expr) {
expr = stripParensAndCasts(expr);
if (ASTHelpers.constValue(expr) != null) {
// This should include literals such as "true" or a string
// obviously not null
return false;
}
// the logic here is to avoid doing dataflow analysis whenever possible
Symbol exprSymbol = ASTHelpers.getSymbol(expr);
boolean exprMayBeNull;
switch (expr.getKind()) {
case NULL_LITERAL:
// obviously null
exprMayBeNull = true;
break;
case ARRAY_ACCESS:
// unsound! we cannot check for nullness of array contents yet
exprMayBeNull = false;
break;
case NEW_CLASS:
case NEW_ARRAY:
// for string concatenation, auto-boxing
case LAMBDA_EXPRESSION:
// Lambdas may return null, but the lambda literal itself should not be null
case MEMBER_REFERENCE:
// These cannot be null; the compiler would catch it
case MULTIPLY_ASSIGNMENT:
case DIVIDE_ASSIGNMENT:
case REMAINDER_ASSIGNMENT:
case PLUS_ASSIGNMENT:
case MINUS_ASSIGNMENT:
case LEFT_SHIFT_ASSIGNMENT:
case RIGHT_SHIFT_ASSIGNMENT:
case UNSIGNED_RIGHT_SHIFT_ASSIGNMENT:
case AND_ASSIGNMENT:
case XOR_ASSIGNMENT:
case OR_ASSIGNMENT:
// result of compound assignment cannot be null
case PLUS:
// rest are for auto-boxing
case MINUS:
case MULTIPLY:
case DIVIDE:
case REMAINDER:
case CONDITIONAL_AND:
case CONDITIONAL_OR:
case LOGICAL_COMPLEMENT:
case INSTANCE_OF:
case PREFIX_INCREMENT:
case PREFIX_DECREMENT:
case POSTFIX_DECREMENT:
case POSTFIX_INCREMENT:
case EQUAL_TO:
case NOT_EQUAL_TO:
case GREATER_THAN:
case GREATER_THAN_EQUAL:
case LESS_THAN:
case LESS_THAN_EQUAL:
case UNARY_MINUS:
case UNARY_PLUS:
case AND:
case OR:
case XOR:
case LEFT_SHIFT:
case RIGHT_SHIFT:
// clearly not null
exprMayBeNull = false;
break;
case MEMBER_SELECT:
exprMayBeNull = mayBeNullFieldAccess(state, expr, exprSymbol);
break;
case IDENTIFIER:
if (exprSymbol != null && exprSymbol.getKind().equals(ElementKind.FIELD)) {
// Special case: mayBeNullFieldAccess runs handler.onOverrideMayBeNullExpr before
// dataflow.
return mayBeNullFieldAccess(state, expr, exprSymbol);
} else {
// Check handler.onOverrideMayBeNullExpr before dataflow.
exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, true);
return exprMayBeNull ? nullnessFromDataflow(state, expr) : false;
}
case METHOD_INVOCATION:
// Special case: mayBeNullMethodCall runs handler.onOverrideMayBeNullExpr before dataflow.
return mayBeNullMethodCall(state, expr, (Symbol.MethodSymbol) exprSymbol);
case CONDITIONAL_EXPRESSION:
case ASSIGNMENT:
exprMayBeNull = nullnessFromDataflow(state, expr);
break;
default:
throw new RuntimeException("whoops, better handle " + expr.getKind() + " " + expr);
}
exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, exprMayBeNull);
return exprMayBeNull;
}
private boolean mayBeNullMethodCall(
VisitorState state, ExpressionTree expr, Symbol.MethodSymbol exprSymbol) {
boolean exprMayBeNull = true;
if (NullabilityUtil.isUnannotated(exprSymbol, config)) {
exprMayBeNull = false;
}
if (!Nullness.hasNullableAnnotation(exprSymbol)) {
exprMayBeNull = false;
}
exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, exprMayBeNull);
return exprMayBeNull ? nullnessFromDataflow(state, expr) : false;
}
public boolean nullnessFromDataflow(VisitorState state, ExpressionTree expr) {
Nullness nullness =
getNullnessAnalysis(state).getNullness(new TreePath(state.getPath(), expr), state.context);
if (nullness == null) {
// this may be unsound, like for field initializers
// figure out if we care
return false;
}
return nullnessToBool(nullness);
}
public AccessPathNullnessAnalysis getNullnessAnalysis(VisitorState state) {
return AccessPathNullnessAnalysis.instance(
state.context, nonAnnotatedMethod, config, this.handler);
}
private boolean mayBeNullFieldAccess(VisitorState state, ExpressionTree expr, Symbol exprSymbol) {
boolean exprMayBeNull = true;
if (!NullabilityUtil.mayBeNullFieldFromType(exprSymbol, config)) {
exprMayBeNull = false;
}
exprMayBeNull = handler.onOverrideMayBeNullExpr(this, expr, state, exprMayBeNull);
return exprMayBeNull ? nullnessFromDataflow(state, expr) : false;
}
/**
* @param kind
* @return <code>true</code> if a deference of the kind might dereference null, <code>false</code>
* otherwise
*/
private boolean kindMayDeferenceNull(ElementKind kind) {
switch (kind) {
case CLASS:
case PACKAGE:
case ENUM:
case INTERFACE:
case ANNOTATION_TYPE:
return false;
default:
return true;
}
}
private Description matchDereference(
ExpressionTree baseExpression, ExpressionTree derefExpression, VisitorState state) {
Symbol dereferenced = ASTHelpers.getSymbol(baseExpression);
if (dereferenced == null
|| dereferenced.type.isPrimitive()
|| !kindMayDeferenceNull(dereferenced.getKind())) {
// we know we don't have a null dereference here
return Description.NO_MATCH;
}
if (mayBeNullExpr(state, baseExpression)) {
String message = "dereferenced expression " + baseExpression.toString() + " is @Nullable";
return createErrorDescriptionForNullAssignment(
MessageTypes.DEREFERENCE_NULLABLE,
derefExpression,
message,
baseExpression,
state.getPath());
}
return Description.NO_MATCH;
}
/**
* create an error description for a nullability warning
*
* @param errorType the type of error encountered.
* @param errorLocTree the location of the error
* @param message the error message
* @param path the TreePath to the error location. Used to compute a suggested fix at the
* enclosing method for the error location
* @return the error description
*/
private Description createErrorDescription(
MessageTypes errorType, Tree errorLocTree, String message, TreePath path) {
MethodTree enclosingMethod = ASTHelpers.findEnclosingNode(path, MethodTree.class);
return createErrorDescription(errorType, errorLocTree, message, enclosingMethod);
}
/**
* create an error description for a nullability warning
*
* @param errorType the type of error encountered.
* @param errorLocTree the location of the error
* @param message the error message
* @param suggestTree the location at which a fix suggestion should be made
* @return the error description
*/
public Description createErrorDescription(
MessageTypes errorType, Tree errorLocTree, String message, @Nullable Tree suggestTree) {
Description.Builder builder = buildDescription(errorLocTree).setMessage(message);
if (config.suggestSuppressions() && suggestTree != null) {
switch (errorType) {
case DEREFERENCE_NULLABLE:
case RETURN_NULLABLE:
case PASS_NULLABLE:
case ASSIGN_FIELD_NULLABLE:
if (config.getCastToNonNullMethod() != null) {
builder = addCastToNonNullFix(suggestTree, builder);
} else {
builder = addSuppressWarningsFix(suggestTree, builder, canonicalName());
}
break;
case CAST_TO_NONNULL_ARG_NONNULL:
builder = removeCastToNonNullFix(suggestTree, builder);
break;
case WRONG_OVERRIDE_RETURN:
builder = addSuppressWarningsFix(suggestTree, builder, canonicalName());
break;
case WRONG_OVERRIDE_PARAM:
builder = addSuppressWarningsFix(suggestTree, builder, canonicalName());
break;
case METHOD_NO_INIT:
case FIELD_NO_INIT:
builder = addSuppressWarningsFix(suggestTree, builder, INITIALIZATION_CHECK_NAME);
break;
case ANNOTATION_VALUE_INVALID:
break;
default:
builder = addSuppressWarningsFix(suggestTree, builder, canonicalName());
}
}
// #letbuildersbuild
return builder.build();
}
/**
* create an error description for a generalized @Nullable value to @NonNull location assignment.
*
* <p>This includes: field assignments, method arguments and method returns
*
* @param errorType the type of error encountered.
* @param errorLocTree the location of the error
* @param message the error message
* @param suggestTreeIfCastToNonNull the location at which a fix suggestion should be made if a
* castToNonNull method is available (usually the expression to cast)
* @param suggestTreePathIfSuppression the location at which a fix suggestion should be made if a
* castToNonNull method is not available (usually the enclosing method, or any place
* where @SuppressWarnings can be added).
* @return the error description.
*/
private Description createErrorDescriptionForNullAssignment(
MessageTypes errorType,
Tree errorLocTree,
String message,
@Nullable Tree suggestTreeIfCastToNonNull,
@Nullable TreePath suggestTreePathIfSuppression) {
MethodTree enclosingMethod =
ASTHelpers.findEnclosingNode(suggestTreePathIfSuppression, MethodTree.class);
return createErrorDescriptionForNullAssignment(
errorType, errorLocTree, message, suggestTreeIfCastToNonNull, enclosingMethod);
}
/**
* create an error description for a generalized @Nullable value to @NonNull location assignment.
*
* <p>This includes: field assignments, method arguments and method returns
*
* @param errorType the type of error encountered.
* @param errorLocTree the location of the error
* @param message the error message
* @param suggestTreeIfCastToNonNull the location at which a fix suggestion should be made if a
* castToNonNull method is available (usually the expression to cast)
* @param suggestTreeIfSuppression the location at which a fix suggestion should be made if a
* castToNonNull method is not available (usually the enclosing method, or any place
* where @SuppressWarnings can be added).
* @return the error description.
*/
private Description createErrorDescriptionForNullAssignment(
MessageTypes errorType,
Tree errorLocTree,
String message,
@Nullable Tree suggestTreeIfCastToNonNull,
@Nullable Tree suggestTreeIfSuppression) {
if (config.getCastToNonNullMethod() != null) {
return createErrorDescription(errorType, errorLocTree, message, suggestTreeIfCastToNonNull);
} else {
return createErrorDescription(errorType, errorLocTree, message, suggestTreeIfSuppression);
}
}
private Description.Builder addCastToNonNullFix(Tree suggestTree, Description.Builder builder) {
String fullMethodName = config.getCastToNonNullMethod();
assert fullMethodName != null;
// Add a call to castToNonNull around suggestTree:
String[] parts = fullMethodName.split("\\.");
String shortMethodName = parts[parts.length - 1];
String replacement = shortMethodName + "(" + suggestTree.toString() + ")";
SuggestedFix fix =
SuggestedFix.builder()
.replace(suggestTree, replacement)
.addStaticImport(fullMethodName) // ensure castToNonNull static import
.build();
return builder.addFix(fix);
}
private Description.Builder removeCastToNonNullFix(
Tree suggestTree, Description.Builder builder) {
assert suggestTree.getKind() == Tree.Kind.METHOD_INVOCATION;
MethodInvocationTree invTree = (MethodInvocationTree) suggestTree;
final Symbol.MethodSymbol methodSymbol = ASTHelpers.getSymbol(invTree);
String qualifiedName =
ASTHelpers.enclosingClass(methodSymbol) + "." + methodSymbol.getSimpleName().toString();
if (!qualifiedName.equals(config.getCastToNonNullMethod())) {
throw new RuntimeException("suggestTree should point to the castToNonNull invocation.");
}
// Remove the call to castToNonNull:
SuggestedFix fix =
SuggestedFix.builder()
.replace(suggestTree, invTree.getArguments().get(0).toString())
.build();
return builder.addFix(fix);
}
@SuppressWarnings("unused")
private Description.Builder changeReturnNullabilityFix(
Tree suggestTree, Description.Builder builder) {
if (suggestTree.getKind() != Tree.Kind.METHOD) {
throw new RuntimeException("This should be a MethodTree");
}
SuggestedFix.Builder fixBuilder = SuggestedFix.builder();
MethodTree methodTree = (MethodTree) suggestTree;
int countNullableAnnotations = 0;
for (AnnotationTree annotationTree : methodTree.getModifiers().getAnnotations()) {
if (annotationTree.getAnnotationType().toString().endsWith("Nullable")) {
fixBuilder.delete(annotationTree);
countNullableAnnotations += 1;
}
}
assert countNullableAnnotations > 1;
return builder.addFix(fixBuilder.build());
}
@SuppressWarnings("unused")
private Description.Builder changeParamNullabilityFix(
Tree suggestTree, Description.Builder builder) {
return builder.addFix(SuggestedFix.prefixWith(suggestTree, "@Nullable "));
}
private Description.Builder addSuppressWarningsFix(
Tree suggestTree, Description.Builder builder, String checkerName) {
SuppressWarnings extantSuppressWarnings =
ASTHelpers.getAnnotation(suggestTree, SuppressWarnings.class);
SuggestedFix fix;
if (extantSuppressWarnings == null) {
fix =
SuggestedFix.prefixWith(
suggestTree,
"@SuppressWarnings(\""
+ checkerName
+ "\") "
+ config.getAutofixSuppressionComment());
} else {
// need to update the existing list of warnings
List<String> suppressions = Lists.newArrayList(extantSuppressWarnings.value());
suppressions.add(checkerName);
// find the existing annotation, so we can replace it
ModifiersTree modifiers =
(suggestTree instanceof MethodTree)
? ((MethodTree) suggestTree).getModifiers()
: ((VariableTree) suggestTree).getModifiers();
List<? extends AnnotationTree> annotations = modifiers.getAnnotations();
// noinspection ConstantConditions
com.google.common.base.Optional<? extends AnnotationTree> suppressWarningsAnnot =
Iterables.tryFind(
annotations,
annot -> annot.getAnnotationType().toString().endsWith("SuppressWarnings"));
if (!suppressWarningsAnnot.isPresent()) {
throw new AssertionError("something went horribly wrong");
}
String replacement =
"@SuppressWarnings({"
+ Joiner.on(',').join(Iterables.transform(suppressions, s -> '"' + s + '"'))
+ "}) "
+ config.getAutofixSuppressionComment();
fix = SuggestedFix.replace(suppressWarningsAnnot.get(), replacement);
}
return builder.addFix(fix);
}
@SuppressWarnings("unused")
private int depth(ExpressionTree expression) {
switch (expression.getKind()) {
case MEMBER_SELECT:
MemberSelectTree selectTree = (MemberSelectTree) expression;
return 1 + depth(selectTree.getExpression());
case METHOD_INVOCATION:
MethodInvocationTree invTree = (MethodInvocationTree) expression;
return depth(invTree.getMethodSelect());
case IDENTIFIER:
IdentifierTree varTree = (IdentifierTree) expression;
Symbol symbol = ASTHelpers.getSymbol(varTree);
return symbol.getKind().equals(ElementKind.FIELD) ? 2 : 1;
default:
return 0;
}
}
private static boolean isThisIdentifier(ExpressionTree expressionTree) {
return expressionTree.getKind().equals(IDENTIFIER)
&& ((IdentifierTree) expressionTree).getName().toString().equals("this");
}
/**
* strip out enclosing parentheses and type casts.
*
* @param expr
* @return
*/
private static ExpressionTree stripParensAndCasts(ExpressionTree expr) {
boolean someChange = true;
while (someChange) {
someChange = false;
if (expr.getKind().equals(PARENTHESIZED)) {
expr = ((ParenthesizedTree) expr).getExpression();
someChange = true;
}
if (expr.getKind().equals(TYPE_CAST)) {
expr = ((TypeCastTree) expr).getExpression();
someChange = true;
}
}
return expr;
}
private static boolean nullnessToBool(Nullness nullness) {
switch (nullness) {
case BOTTOM:
case NONNULL:
return false;
case NULL:
case NULLABLE:
return true;
default:
throw new AssertionError("Impossible: " + nullness);
}
}
/**
* find the closest ancestor method in a superclass or superinterface that method overrides
*
* @param method the subclass method
* @param types the types data structure from javac
* @return closest overridden ancestor method, or <code>null</code> if method does not override
* anything
*/
@Nullable
private Symbol.MethodSymbol getClosestOverriddenMethod(Symbol.MethodSymbol method, Types types) {
// taken from Error Prone MethodOverrides check
Symbol.ClassSymbol owner = method.enclClass();
for (Type s : types.closure(owner.type)) {
if (s.equals(owner.type)) {
continue;
}
for (Symbol m : s.tsym.members().getSymbolsByName(method.name)) {
if (!(m instanceof Symbol.MethodSymbol)) {
continue;
}
Symbol.MethodSymbol msym = (Symbol.MethodSymbol) m;
if (msym.isStatic()) {
continue;
}
if (method.overrides(msym, owner, types, /*checkReturn*/ false)) {
return msym;
}
}
}
return null;
}
private void reportInitializerError(
Symbol.MethodSymbol methodSymbol, String message, VisitorState state) {
if (symbolHasSuppressInitalizationWarningsAnnotation(methodSymbol)) {
return;
}
Tree methodTree = getTreesInstance(state).getTree(methodSymbol);
state.reportMatch(
createErrorDescription(MessageTypes.METHOD_NO_INIT, methodTree, message, methodTree));
}
private String errMsgForInitializer(Set<Element> uninitFields) {
String message = "initializer method does not guarantee @NonNull ";
if (uninitFields.size() == 1) {
message += "field " + uninitFields.iterator().next().toString() + " is initialized";
} else {
message += "fields " + Joiner.on(", ").join(uninitFields) + " are initialized";
}
message += " along all control-flow paths (remember to check for exceptions or early returns).";
return message;
}
private void reportInitErrorOnField(Symbol symbol, VisitorState state) {
if (symbolHasSuppressInitalizationWarningsAnnotation(symbol)) {
return;
}
Tree tree = getTreesInstance(state).getTree(symbol);
if (symbol.isStatic()) {
state.reportMatch(
createErrorDescription(
MessageTypes.FIELD_NO_INIT,
tree,
"@NonNull static field " + symbol + " not initialized",
tree));
} else {
state.reportMatch(
createErrorDescription(
MessageTypes.FIELD_NO_INIT,
tree,
"@NonNull field " + symbol + " not initialized",
tree));
}
}
/**
* Returns the computed nullness information from an expression. If none is available, it returns
* Nullable.
*
* <p>Computed information can be added by handlers or by the core, and should supersede that
* comming from annotations.
*
* <p>The default value of an expression without additional computed nullness information is
* always Nullable, since this method should only be called when the fact that the expression is
* NonNull is not clear from looking at annotations.
*
* @param e an expression
* @return computed nullness for e, if any, else Nullable
*/
public Nullness getComputedNullness(ExpressionTree e) {
if (computedNullnessMap.containsKey(e)) {
return computedNullnessMap.get(e);
} else {
return Nullness.NULLABLE;
}
}
/**
* Add computed nullness informat to an expression.
*
* <p>Used by handlers to communicate that an expression should has a more precise nullness than
* what is known from source annotations.
*
* @param e
* @param nullness
*/
public void setComputedNullness(ExpressionTree e, Nullness nullness) {
computedNullnessMap.put(e, nullness);
}
public enum MessageTypes {
DEREFERENCE_NULLABLE,
RETURN_NULLABLE,
PASS_NULLABLE,
ASSIGN_FIELD_NULLABLE,
WRONG_OVERRIDE_RETURN,
WRONG_OVERRIDE_PARAM,
METHOD_NO_INIT,
FIELD_NO_INIT,
UNBOX_NULLABLE,
NONNULL_FIELD_READ_BEFORE_INIT,
ANNOTATION_VALUE_INVALID,
CAST_TO_NONNULL_ARG_NONNULL;
}
@AutoValue
abstract static class FieldInitEntities {
static FieldInitEntities create(
Symbol.ClassSymbol classSymbol,
Set<Symbol> nonnullInstanceFields,
Set<Symbol> nonnullStaticFields,
List<BlockTree> instanceInitializerBlocks,
List<BlockTree> staticInitializerBlocks,
Set<MethodTree> constructors,
Set<MethodTree> instanceInitializerMethods,
Set<MethodTree> staticInitializerMethods) {
return new AutoValue_NullAway_FieldInitEntities(
classSymbol,
nonnullInstanceFields,
nonnullStaticFields,
instanceInitializerBlocks,
staticInitializerBlocks,
constructors,
instanceInitializerMethods,
staticInitializerMethods);
}
/** @return symbol for class */
abstract Symbol.ClassSymbol classSymbol();
/** @return @NonNull instance fields that are not directly initialized at declaration */
abstract Set<Symbol> nonnullInstanceFields();
/** @return @NonNull static fields that are not directly initialized at declaration */
abstract Set<Symbol> nonnullStaticFields();
/**
* @return the list of instance initializer blocks (e.g. blocks of the form `class X { { //Code
* } } ), in the order in which they appear in the class
*/
abstract List<BlockTree> instanceInitializerBlocks();
/**
* @return the list of static initializer blocks (e.g. blocks of the form `class X { static {
* //Code } } ), in the order in which they appear in the class
*/
abstract List<BlockTree> staticInitializerBlocks();
/** @return the list of constructor */
abstract Set<MethodTree> constructors();
/**
* @return the list of non-static (instance) initializer methods. This includes methods
* annotated @Initializer, as well as those specified by -XepOpt:NullAway:KnownInitializers
* or annotated with annotations passed to -XepOpt:NullAway:CustomInitializerAnnotations
*/
abstract Set<MethodTree> instanceInitializerMethods();
/**
* @return the list of static initializer methods. This includes static methods
* annotated @Initializer, as well as those specified by -XepOpt:NullAway:KnownInitializers
* or annotated with annotations passed to -XepOpt:NullAway:CustomInitializerAnnotations
*/
abstract Set<MethodTree> staticInitializerMethods();
}
}