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;
}