public NullnessHint onDataflowVisitMethodInvocation()

in nullaway/src/main/java/com/uber/nullaway/handlers/contract/ContractHandler.java [184:333]


  public NullnessHint onDataflowVisitMethodInvocation(
      MethodInvocationNode node,
      Symbol.MethodSymbol callee,
      VisitorState state,
      AccessPath.AccessPathContext apContext,
      AccessPathNullnessPropagation.SubNodeValues inputs,
      AccessPathNullnessPropagation.Updates thenUpdates,
      AccessPathNullnessPropagation.Updates elseUpdates,
      AccessPathNullnessPropagation.Updates bothUpdates) {
    Preconditions.checkNotNull(analysis);
    MethodInvocationTree tree = castToNonNull(node.getTree());
    for (String clause : ContractUtils.getContractClauses(callee, config)) {

      String[] antecedent =
          getAntecedent(clause, tree, analysis, state, callee, node.getArguments().size());
      String consequent = getConsequent(clause, tree, analysis, state, callee);

      // Find a single value constraint that is not already known. If more than one argument with
      // unknown nullness affects the method's result, then ignore this clause.
      Node arg = null;
      Nullness argAntecedentNullness = null;
      // Set to false if the rule is detected to be one we don't yet support
      boolean supported = true;

      for (int i = 0; i < antecedent.length; ++i) {
        String valueConstraint = antecedent[i].trim();
        if (valueConstraint.equals("_")) {
          // do nothing
        } else if (valueConstraint.equals("false") || valueConstraint.equals("true")) {
          // We handle boolean constraints in the case that the boolean argument is the result
          // of a null or not-null check. For example,
          // '@Contract("true -> true") boolean func(boolean v)'
          // called with 'func(obj == null)'
          // can be interpreted as equivalent to
          // '@Contract("null -> true") boolean func(@Nullable Object v)'
          // called with 'func(obj)'
          // This path unwraps null reference equality and inequality checks
          // to pass the target (in the above example, 'obj') as arg.
          Node argument = node.getArgument(i);
          // isNullTarget is the variable side of a null check. For example, both 'e == null'
          // and 'null == e' would return the node representing 'e'.
          Optional<Node> isNullTarget = argument.accept(NullEqualityVisitor.IS_NULL, inputs);
          // notNullTarget is the variable side of a not-null check. For example, both 'e != null'
          // and 'null != e' would return the node representing 'e'.
          Optional<Node> notNullTarget = argument.accept(NullEqualityVisitor.NOT_NULL, inputs);
          // It is possible for at most one of isNullTarget and notNullTarget to be present.
          Node nullTestTarget = isNullTarget.orElse(notNullTarget.orElse(null));
          if (nullTestTarget == null) {
            supported = false;
            break;
          }
          // isNullTarget is equivalent to 'null ->' while notNullTarget is equivalent
          // to '!null ->'. However, the valueConstraint may reverse the check.
          // The following table illustrates expected antecedentNullness based on
          // null comparison direction and constraint:
          //                    | (obj == null)   | (obj != null)
          // Constraint 'true'  | NULL            | NONNULL
          // Constraint 'false' | NONNULL         | NULL
          boolean booleanConstraintValue = valueConstraint.equals("true");
          Nullness antecedentNullness =
              isNullTarget.isPresent()
                  ? (booleanConstraintValue ? Nullness.NULL : Nullness.NONNULL)
                  :
                  // !isNullTarget.isPresent()  -> notNullTarget.present() must be true
                  (booleanConstraintValue ? Nullness.NONNULL : Nullness.NULL);
          Nullness targetNullness = inputs.valueOfSubNode(nullTestTarget);
          if (antecedentNullness.equals(targetNullness)) {
            // We already know this argument is satisfied so we can treat it as part of the
            // clause for the purpose of deciding the nullness of the other arguments.
            continue;
          }
          if (arg != null) {
            // More than one argument involved in the antecedent, ignore this rule
            supported = false;
            break;
          }
          arg = nullTestTarget;
          argAntecedentNullness = antecedentNullness;
        } else if (valueConstraint.equals("!null")
            && inputs.valueOfSubNode(node.getArgument(i)).equals(Nullness.NONNULL)) {
          // We already know this argument can't be null, so we can treat it as not part of the
          // clause for the purpose of deciding the non-nullness of the other arguments; do nothing
        } else if (valueConstraint.equals("null") || valueConstraint.equals("!null")) {
          if (arg != null) {
            // More than one argument involved in the antecedent, ignore this rule
            supported = false;
            break;
          }
          arg = node.getArgument(i);
          argAntecedentNullness = valueConstraint.equals("null") ? Nullness.NULL : Nullness.NONNULL;
        } else {
          String errorMessage =
              "Invalid @Contract annotation detected for method "
                  + callee
                  + ". It contains the following uparseable clause: "
                  + clause
                  + " (unknown value constraint: "
                  + valueConstraint
                  + ", see https://www.jetbrains.com/help/idea/contract-annotations.html).";
          state.reportMatch(
              analysis
                  .getErrorBuilder()
                  .createErrorDescription(
                      new ErrorMessage(
                          ErrorMessage.MessageTypes.ANNOTATION_VALUE_INVALID, errorMessage),
                      tree,
                      analysis.buildDescription(tree),
                      state,
                      null));
          supported = false;
          break;
        }
      }
      if (!supported) {
        // Too many arguments involved, or unsupported @Contract features. On to next clause in the
        // contract expression
        continue;
      }
      if (arg == null) {
        // The antecedent is unconditionally true. Check for the ... -> !null case and set the
        // return nullness accordingly
        if (consequent.equals("!null")) {
          return NullnessHint.FORCE_NONNULL;
        }
        continue;
      }
      Preconditions.checkState(
          argAntecedentNullness != null, "argAntecedentNullness should have been set");
      // The nullness of one argument is all that matters for the antecedent, let's negate the
      // consequent to fix the nullness of this argument.
      AccessPath accessPath = AccessPath.getAccessPathForNode(arg, state, apContext);
      if (accessPath == null) {
        continue;
      }
      if (consequent.equals("false") && argAntecedentNullness.equals(Nullness.NULL)) {
        // If arg being null implies the return of the method being false, then the return
        // being true implies arg is not null and we must mark it as such in the then update.
        thenUpdates.set(accessPath, Nullness.NONNULL);
      } else if (consequent.equals("true") && argAntecedentNullness.equals(Nullness.NULL)) {
        // If arg being null implies the return of the method being true, then the return being
        // false implies arg is not null and we must mark it as such in the else update.
        elseUpdates.set(accessPath, Nullness.NONNULL);
      } else if (consequent.equals("fail") && argAntecedentNullness.equals(Nullness.NULL)) {
        // If arg being null implies the method throws an exception, then we can mark it as
        // non-null on both non-exceptional exits from the method
        bothUpdates.set(accessPath, Nullness.NONNULL);
      }
    }
    return NullnessHint.UNKNOWN;
  }