All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.uber.nullaway.ErrorBuilder Maven / Gradle / Ivy

There is a newer version: 0.12.4
Show newest version
/*
 * Copyright (c) 2019 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.uber.nullaway.ASTHelpersBackports.hasDirectAnnotationWithSimpleName;
import static com.uber.nullaway.ASTHelpersBackports.isStatic;
import static com.uber.nullaway.ErrorMessage.MessageTypes.FIELD_NO_INIT;
import static com.uber.nullaway.ErrorMessage.MessageTypes.GET_ON_EMPTY_OPTIONAL;
import static com.uber.nullaway.ErrorMessage.MessageTypes.METHOD_NO_INIT;
import static com.uber.nullaway.ErrorMessage.MessageTypes.NONNULL_FIELD_READ_BEFORE_INIT;
import static com.uber.nullaway.NullAway.CORE_CHECK_NAME;
import static com.uber.nullaway.NullAway.INITIALIZATION_CHECK_NAME;
import static com.uber.nullaway.NullAway.OPTIONAL_CHECK_NAME;
import static com.uber.nullaway.NullAway.getTreesInstance;
import static com.uber.nullaway.Nullness.hasNullableAnnotation;

import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.errorprone.VisitorState;
import com.google.errorprone.fixes.SuggestedFix;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
import com.sun.tools.javac.util.DiagnosticSource;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
import com.uber.nullaway.fixserialization.SerializationService;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.StreamSupport;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.tools.JavaFileObject;
import org.jspecify.annotations.Nullable;

/** A class to construct error message to be displayed after the analysis finds error. */
public class ErrorBuilder {

  private final Config config;

  /** Checker name that can be used to suppress the warnings. */
  private final String suppressionName;

  /** Additional identifiers for this check, to be checked for in @SuppressWarnings annotations. */
  private final Set allNames;

  ErrorBuilder(Config config, String suppressionName, Set allNames) {
    this.config = config;
    this.suppressionName = suppressionName;
    this.allNames = allNames;
  }

  /**
   * create an error description for a nullability warning
   *
   * @param errorMessage the error message object.
   * @param descriptionBuilder the description builder for the error.
   * @param state the visitor state (used for e.g. suppression finding).
   * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable
   *     expression into a @NonNull target, and this parameter is the Symbol for that target.
   * @return the error description
   */
  public Description createErrorDescription(
      ErrorMessage errorMessage,
      Description.Builder descriptionBuilder,
      VisitorState state,
      @Nullable Symbol nonNullTarget) {
    Tree enclosingSuppressTree = suppressibleNode(state.getPath());
    return createErrorDescription(
        errorMessage, enclosingSuppressTree, descriptionBuilder, state, nonNullTarget);
  }

  /**
   * create an error description for a nullability warning
   *
   * @param errorMessage the error message object.
   * @param suggestTree the location at which a fix suggestion should be made
   * @param descriptionBuilder the description builder for the error.
   * @param state the visitor state (used for e.g. suppression finding).
   * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable
   *     expression into a @NonNull target, and this parameter is the Symbol for that target.
   * @return the error description
   */
  public Description createErrorDescription(
      ErrorMessage errorMessage,
      @Nullable Tree suggestTree,
      Description.Builder descriptionBuilder,
      VisitorState state,
      @Nullable Symbol nonNullTarget) {
    Description.Builder builder = descriptionBuilder.setMessage(errorMessage.message);
    String checkName = CORE_CHECK_NAME;
    if (errorMessage.messageType.equals(GET_ON_EMPTY_OPTIONAL)) {
      checkName = OPTIONAL_CHECK_NAME;
    } else if (errorMessage.messageType.equals(FIELD_NO_INIT)
        || errorMessage.messageType.equals(METHOD_NO_INIT)
        || errorMessage.messageType.equals(NONNULL_FIELD_READ_BEFORE_INIT)) {
      checkName = INITIALIZATION_CHECK_NAME;
    }

    // Mildly expensive state.getPath() traversal, occurs only once per potentially
    // reported error.
    if (hasPathSuppression(state.getPath(), checkName)) {
      return Description.NO_MATCH;
    }

    if (config.suggestSuppressions() && suggestTree != null) {
      builder = addSuggestedSuppression(errorMessage, suggestTree, builder, state);
    }

    if (config.serializationIsActive()) {
      // For the case of initializer errors, the leaf of state.getPath() may not be the field /
      // method on which the error is being reported (since we do a class-wide analysis to find such
      // errors).  In such cases, the suggestTree is the appropriate field / method tree, so use
      // that as the errorTree for serialization.
      Tree errorTree =
          (suggestTree != null
                  && (errorMessage.messageType.equals(FIELD_NO_INIT)
                      || errorMessage.messageType.equals(METHOD_NO_INIT)))
              ? suggestTree
              : state.getPath().getLeaf();
      SerializationService.serializeReportingError(
          config, state, errorTree, nonNullTarget, errorMessage);
    }

    // #letbuildersbuild
    return builder.build();
  }

  private static boolean canHaveSuppressWarningsAnnotation(Tree tree) {
    return tree instanceof MethodTree
        || (tree instanceof ClassTree && ((ClassTree) tree).getSimpleName().length() != 0)
        || tree instanceof VariableTree;
  }

  /**
   * Find out if a particular subchecker (e.g. NullAway.Optional) is being suppressed in a given
   * path.
   *
   * 

This requires a tree path traversal, which is expensive, but we only do this when we would * otherwise report an error, which means this won't happen for most nodes/files. * * @param treePath The path with the error location as the leaf. * @param subcheckerName The string to check for inside @SuppressWarnings * @return Whether the subchecker is being suppressed at treePath. */ private boolean hasPathSuppression(TreePath treePath, String subcheckerName) { return StreamSupport.stream(treePath.spliterator(), false) .filter(ErrorBuilder::canHaveSuppressWarningsAnnotation) .map(tree -> ASTHelpers.getSymbol(tree)) .filter(symbol -> symbol != null) .anyMatch( symbol -> symbolHasSuppressWarningsAnnotation(symbol, subcheckerName) || symbolIsExcludedClassSymbol(symbol)); } private Description.Builder addSuggestedSuppression( ErrorMessage errorMessage, Tree suggestTree, Description.Builder builder, VisitorState state) { switch (errorMessage.messageType) { case DEREFERENCE_NULLABLE: case RETURN_NULLABLE: case PASS_NULLABLE: case ASSIGN_FIELD_NULLABLE: case SWITCH_EXPRESSION_NULLABLE: if (config.getCastToNonNullMethod() != null && canBeCastToNonNull(suggestTree)) { builder = addCastToNonNullFix(suggestTree, builder, state); } else { // When there is a castToNonNull method, suggestTree is set to the expression to be // casted, which is not suppressible. For simplicity, we just always recompute the // suppressible node here. Tree suppressibleNode = suppressibleNode(state.getPath()); if (suppressibleNode != null) { builder = addSuppressWarningsFix(suppressibleNode, builder, suppressionName); } } break; case CAST_TO_NONNULL_ARG_NONNULL: builder = removeCastToNonNullFix(suggestTree, builder, state); break; case WRONG_OVERRIDE_RETURN: builder = addSuppressWarningsFix(suggestTree, builder, suppressionName); break; case WRONG_OVERRIDE_PARAM: builder = addSuppressWarningsFix(suggestTree, builder, suppressionName); 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, suppressionName); } return builder; } /** * create an error description for a generalized @Nullable value to @NonNull location assignment. * *

This includes: field assignments, method arguments and method returns * * @param errorMessage the error message object. * @param suggestTreeIfCastToNonNull the location at which a fix suggestion should be made if a * castToNonNull method is available (usually the expression to cast) * @param descriptionBuilder the description builder for the error. * @param state the visitor state for the location which triggered the error (i.e. for suppression * finding) * @param nonNullTarget if non-null, this error involved a pseudo-assignment of a @Nullable * expression into a @NonNull target, and this parameter is the Symbol for that target. * @return the error description. */ Description createErrorDescriptionForNullAssignment( ErrorMessage errorMessage, @Nullable Tree suggestTreeIfCastToNonNull, Description.Builder descriptionBuilder, VisitorState state, @Nullable Symbol nonNullTarget) { if (config.getCastToNonNullMethod() != null) { return createErrorDescription( errorMessage, suggestTreeIfCastToNonNull, descriptionBuilder, state, nonNullTarget); } else { return createErrorDescription( errorMessage, suppressibleNode(state.getPath()), descriptionBuilder, state, nonNullTarget); } } Description.Builder addSuppressWarningsFix( Tree suggestTree, Description.Builder builder, String suppressionName) { SuppressWarnings extantSuppressWarnings = null; Symbol treeSymbol = ASTHelpers.getSymbol(suggestTree); if (treeSymbol != null) { extantSuppressWarnings = treeSymbol.getAnnotation(SuppressWarnings.class); } SuggestedFix fix; if (extantSuppressWarnings == null) { fix = SuggestedFix.prefixWith( suggestTree, "@SuppressWarnings(\"" + suppressionName + "\") " + config.getAutofixSuppressionComment()); } else { // need to update the existing list of warnings List suppressions = Lists.newArrayList(extantSuppressWarnings.value()); suppressions.add(suppressionName); // find the existing annotation, so we can replace it ModifiersTree modifiers = (suggestTree instanceof MethodTree) ? ((MethodTree) suggestTree).getModifiers() : ((VariableTree) suggestTree).getModifiers(); List annotations = modifiers.getAnnotations(); // noinspection ConstantConditions com.google.common.base.Optional 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); } /** * Adapted from {@link com.google.errorprone.fixes.SuggestedFixes}. * *

TODO: actually use {@link * com.google.errorprone.fixes.SuggestedFixes#addSuppressWarnings(VisitorState, String)} instead */ private @Nullable Tree suppressibleNode(@Nullable TreePath path) { if (path == null) { return null; } return StreamSupport.stream(path.spliterator(), false) .filter(ErrorBuilder::canHaveSuppressWarningsAnnotation) .findFirst() .orElse(null); } /** * Checks if it would be appropriate to wrap {@code tree} in a {@code castToNonNull} call. There * are two cases where this method returns {@code false}: * *

    *
  1. {@code tree} represents the {@code null} literal *
  2. {@code tree} represents a {@code @Nullable} formal parameter of the enclosing method *
*/ private boolean canBeCastToNonNull(Tree tree) { switch (tree.getKind()) { case NULL_LITERAL: // never do castToNonNull(null) return false; case IDENTIFIER: // Don't wrap a @Nullable parameter in castToNonNull, as this misleads callers into thinking // they can pass in null without causing an NPE. A more appropriate fix would likely be to // make the parameter @NonNull and add casts at call sites, but that is beyond the scope of // our suggested fixes Symbol symbol = ASTHelpers.getSymbol(tree); return !(symbol != null && symbol.getKind().equals(ElementKind.PARAMETER) && hasNullableAnnotation(symbol, config)); default: return true; } } private Description.Builder addCastToNonNullFix( Tree suggestTree, Description.Builder builder, VisitorState state) { String fullMethodName = config.getCastToNonNullMethod(); if (fullMethodName == null) { throw new IllegalStateException("cast-to-non-null method not set"); } // Add a call to castToNonNull around suggestTree: String[] parts = fullMethodName.split("\\."); String shortMethodName = parts[parts.length - 1]; String replacement = shortMethodName + "(" + state.getSourceForNode(suggestTree) + ")"; 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, VisitorState state) { // Note: Here suggestTree refers to the argument being cast. We need to find the // castToNonNull(...) invocation to be replaced by it. Fortunately, state.getPath() // should be currently pointing at said call. Tree currTree = state.getPath().getLeaf(); Preconditions.checkArgument( currTree.getKind() == Tree.Kind.METHOD_INVOCATION, String.format("Expected castToNonNull invocation expression, found:\n%s", currTree)); MethodInvocationTree invTree = (MethodInvocationTree) currTree; Preconditions.checkArgument( invTree.getArguments().contains(suggestTree), String.format( "Method invocation tree %s does not contain the expression %s as an argument being cast", invTree, suggestTree)); // Remove the call to castToNonNull: SuggestedFix fix = SuggestedFix.builder().replace(invTree, state.getSourceForNode(suggestTree)).build(); return builder.addFix(fix); } /** * Reports initialization errors where a constructor fails to guarantee non-null fields are * initialized along all paths at exit points. * * @param methodSymbol Constructor symbol. * @param message Error message. * @param state The VisitorState object. * @param descriptionBuilder the description builder for the error. */ void reportInitializerError( Symbol.MethodSymbol methodSymbol, String message, VisitorState state, Description.Builder descriptionBuilder) { // Check needed here, despite check in hasPathSuppression because initialization // checking happens at the class-level (meaning state.getPath() might not include the // method itself). if (symbolHasSuppressWarningsAnnotation(methodSymbol, INITIALIZATION_CHECK_NAME) || hasDirectAnnotationWithSimpleName( methodSymbol, NullabilityUtil.NULLUNMARKED_SIMPLE_NAME)) { return; } Tree methodTree = getTreesInstance(state).getTree(methodSymbol); ErrorMessage errorMessage = new ErrorMessage(METHOD_NO_INIT, message); state.reportMatch( createErrorDescription(errorMessage, methodTree, descriptionBuilder, state, null)); } boolean symbolHasSuppressWarningsAnnotation(Symbol symbol, String suppression) { SuppressWarnings annotation = symbol.getAnnotation(SuppressWarnings.class); if (annotation != null) { for (String s : annotation.value()) { // we need to check for standard suppression here also since we may report initialization // errors outside the normal ErrorProne match* methods if (s.equals(suppression) || allNames.stream().anyMatch(s::equals)) { return true; } } } return false; } private boolean symbolIsExcludedClassSymbol(Symbol symbol) { if (symbol instanceof Symbol.ClassSymbol) { ImmutableSet excludedClassAnnotations = config.getExcludedClassAnnotations(); return ((Symbol.ClassSymbol) symbol) .getAnnotationMirrors().stream() .map(anno -> anno.getAnnotationType().toString()) .anyMatch(excludedClassAnnotations::contains); } return false; } static int getLineNumForElement(Element uninitField, VisitorState state) { Tree tree = getTreesInstance(state).getTree(uninitField); if (tree == null) { throw new RuntimeException( "When getting the line number for uninitialized field, can't get the tree from the element."); } DiagnosticPosition position = (DiagnosticPosition) tree; // Expect Tree to be JCTree and thus implement DiagnosticPosition TreePath path = state.getPath(); JCCompilationUnit compilation = (JCCompilationUnit) path.getCompilationUnit(); JavaFileObject file = compilation.getSourceFile(); DiagnosticSource source = new DiagnosticSource(file, null); return source.getLineNumber(position.getStartPosition()); } /** * Generate the message for uninitialized fields, including the line number for fields. * * @param uninitFields the set of uninitialized fields as the type of Element. * @param state the VisitorState object. * @return the error message for uninitialized fields with line numbers. */ static String errMsgForInitializer(Set uninitFields, VisitorState state) { StringBuilder message = new StringBuilder("initializer method does not guarantee @NonNull "); Element uninitField; if (uninitFields.size() == 1) { uninitField = uninitFields.iterator().next(); message.append("field "); message.append(uninitField.toString()); message.append(" (line "); message.append(getLineNumForElement(uninitField, state)); message.append(") is initialized"); } else { message.append("fields "); Iterator it = uninitFields.iterator(); while (it.hasNext()) { uninitField = it.next(); message.append( uninitField.toString() + " (line " + getLineNumForElement(uninitField, state) + ")"); if (it.hasNext()) { message.append(", "); } else { message.append(" are initialized"); } } } message.append( " along all control-flow paths (remember to check for exceptions or early returns)."); return message.toString(); } void reportInitErrorOnField(Symbol symbol, VisitorState state, Description.Builder builder) { // Check needed here, despite check in hasPathSuppression because initialization // checking happens at the class-level (meaning state.getPath() might not include the // field itself). if (symbolHasSuppressWarningsAnnotation(symbol, INITIALIZATION_CHECK_NAME)) { return; } Tree tree = getTreesInstance(state).getTree(symbol); String fieldName = symbol.toString(); if (symbol.enclClass().getNestingKind().isNested()) { String flatName = symbol.enclClass().flatName().toString(); int index = flatName.lastIndexOf(".") + 1; fieldName = flatName.substring(index) + "." + fieldName; } if (isStatic(symbol)) { state.reportMatch( createErrorDescription( new ErrorMessage( FIELD_NO_INIT, "@NonNull static field " + fieldName + " not initialized"), tree, builder, state, symbol)); } else { state.reportMatch( createErrorDescription( new ErrorMessage(FIELD_NO_INIT, "@NonNull field " + fieldName + " not initialized"), tree, builder, state, symbol)); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy