/* * Copyright 2016 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.bugpatterns.nullness; import static com.google.errorprone.BugPattern.Category.JDK; import static com.google.errorprone.BugPattern.SeverityLevel.SUGGESTION; import com.google.errorprone.BugPattern; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.AssignmentTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; import com.google.errorprone.dataflow.nullnesspropagation.Nullness; import com.google.errorprone.dataflow.nullnesspropagation.TrustingNullnessAnalysis; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.AssignmentTree; import com.sun.source.tree.ExpressionTree; 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.Symbol; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import javax.annotation.Nullable; import javax.lang.model.element.ElementKind; /** * {@link Nullable} suggestions for fields based on values assigned to them. For simplicity this * check will not suggest annotations for fields that are never assigned in a constructor. While * fields like that <i>seem</i> like obvious candidates for being nullable, they are really not * because fields may be assigned to in methods called from a constructor or super-constructor, for * instance. We'd also need an analysis that tells us about uninitialized fields. * * @author kmb@google.com (Kevin Bierhoff) */ @BugPattern( name = "FieldMissingNullable", summary = "Fields that can be null should be annotated @Nullable", category = JDK, severity = SUGGESTION ) public class FieldMissingNullable extends BugChecker implements AssignmentTreeMatcher, VariableTreeMatcher { @Override public Description matchVariable(VariableTree tree, VisitorState state) { Symbol assigned = ASTHelpers.getSymbol(tree); if (assigned == null || assigned.getKind() != ElementKind.FIELD || assigned.type.isPrimitive()) { return Description.NO_MATCH; // not a field of nullable type } ExpressionTree expression = tree.getInitializer(); if (expression == null || ASTHelpers.constValue(expression) != null) { // This should include literals such as "true" or a string return Description.NO_MATCH; } if (TrustingNullnessAnalysis.hasNullableAnnotation(assigned)) { return Description.NO_MATCH; // field already annotated } // Don't need dataflow to tell us that null is nullable if (expression.getKind() == Tree.Kind.NULL_LITERAL) { return makeFix(tree, tree, "Initializing field with null literal"); } // OK let's see what dataflow says // TODO(kmb): Merge this method with matchAssignment once we unify nullness analysis entry point Nullness nullness = TrustingNullnessAnalysis.instance(state.context) .getFieldInitializerNullness(state.getPath(), state.context); switch (nullness) { case BOTTOM: case NONNULL: return Description.NO_MATCH; case NULL: return makeFix(tree, tree, "Initializing field with null"); case NULLABLE: return makeFix(tree, tree, "May initialize field with null"); default: throw new AssertionError("Impossible: " + nullness); } } @Override public Description matchAssignment(AssignmentTree tree, VisitorState state) { Symbol assigned = ASTHelpers.getSymbol(tree.getVariable()); if (assigned == null || assigned.getKind() != ElementKind.FIELD || assigned.type.isPrimitive()) { return Description.NO_MATCH; // not a field of nullable type } // Best-effort try to avoid running the dataflow analysis // TODO(kmb): bail on more non-null expressions, such as "this", arithmethic, logical, and &&/|| ExpressionTree expression = tree.getExpression(); if (ASTHelpers.constValue(expression) != null) { // This should include literals such as "true" or a string return Description.NO_MATCH; } if (TrustingNullnessAnalysis.hasNullableAnnotation(assigned)) { return Description.NO_MATCH; // field already annotated } VariableTree fieldDecl = findDeclaration(state, assigned); if (fieldDecl == null) { return Description.NO_MATCH; // skip fields declared elsewhere for simplicity } // Don't need dataflow to tell us that null is nullable if (expression.getKind() == Tree.Kind.NULL_LITERAL) { return makeFix(fieldDecl, tree, "Assigning null literal to field"); } // OK let's see what dataflow says Nullness nullness = TrustingNullnessAnalysis.instance(state.context) .getNullness(new TreePath(state.getPath(), expression), state.context); if (nullness == null) { // This can currently happen if the assignment is inside a lambda expression // TODO(kmb): Make dataflow work for lambda expressions return Description.NO_MATCH; } switch (nullness) { case BOTTOM: case NONNULL: return Description.NO_MATCH; case NULL: return makeFix(fieldDecl, tree, "Assigning null to field"); case NULLABLE: return makeFix(fieldDecl, tree, "May assign null to field"); default: throw new AssertionError("Impossible: " + nullness); } } @Nullable private VariableTree findDeclaration(VisitorState state, Symbol field) { JavacProcessingEnvironment javacEnv = JavacProcessingEnvironment.instance(state.context); TreePath fieldDeclPath = Trees.instance(javacEnv).getPath(field); // Skip fields declared in other compilation units since we can't make a fix for them here. if (fieldDeclPath != null && fieldDeclPath.getCompilationUnit() == state.getPath().getCompilationUnit() && (fieldDeclPath.getLeaf() instanceof VariableTree)) { return (VariableTree) fieldDeclPath.getLeaf(); } return null; } private Description makeFix(VariableTree declaration, Tree matchedTree, String message) { return buildDescription(matchedTree) .setMessage(message) .addFix( SuggestedFix.builder() .addImport("javax.annotation.Nullable") .prefixWith(declaration, "@Nullable ") .build()) .build(); } }