Repository: uber/NullAway (Java)

Issue:
`case null` of switch expression is not considered
NullAway: 0.12.7
Java: 21.0.8_12

Consider the following code:
``` java
boolean hasRelevantMessage(IOException ioException)
{
    String message = ioException.getMessage();
    return switch (message)
    {
        case null -> false;
        case "Operation timed out", "Connection reset by peer", "Premature EOF", "Error writing to server" -> true;
        default -> message.contains("GOAWAY received"); // ⚠️ False positive: "dereferenced expression message is @Nullable"
    };
}
```

If `message` is null, the `case null` would be called. Consequently, `message` can't be null in the `default` case. However, NullAway reports "dereferenced expression message is @Nullable" there.

The same problem occurs when inlining `message`, i.e. using `ioException.getMessage()` both for `switch()` and for the `contains()` call.

This seems to be a regression or special case of #930 which was fixed last year.


Discussion: Thanks for the report.  This is a different issue than #930.  I'll try to get it working as soon as I get time.



Fix (patch):
diff --git a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java
index dd954d01d8..fe928370be 100644
--- a/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java
+++ b/nullaway/src/main/java/com/uber/nullaway/dataflow/AccessPathNullnessPropagation.java
@@ -411,14 +411,14 @@ public TransferResult<Nullness, NullnessStore> visitGreaterThanOrEqual(
   public TransferResult<Nullness, NullnessStore> visitEqualTo(
       EqualToNode equalToNode, TransferInput<Nullness, NullnessStore> input) {
     return handleEqualityComparison(
-        input, equalToNode.getLeftOperand(), equalToNode.getRightOperand(), true);
+        input, equalToNode.getLeftOperand(), null, equalToNode.getRightOperand(), true);
   }
 
   @Override
   public TransferResult<Nullness, NullnessStore> visitNotEqual(
       NotEqualNode notEqualNode, TransferInput<Nullness, NullnessStore> input) {
     return handleEqualityComparison(
-        input, notEqualNode.getLeftOperand(), notEqualNode.getRightOperand(), false);
+        input, notEqualNode.getLeftOperand(), null, notEqualNode.getRightOperand(), false);
   }
 
   /**
@@ -426,6 +426,8 @@ public TransferResult<Nullness, NullnessStore> visitNotEqual(
    *
    * @param input transfer input for the operation
    * @param leftOperand left operand of the comparison
+   * @param leftOperandNullness nullness of left operand if it is not a sub node of {@code
+   *     input.getNode()}, {@code null} otherwise
    * @param rightOperand right operand of the comparison
    * @param equalTo if {@code true}, the comparison is an equality comparison, otherwise it is a
    *     dis-equality ({@code !=}) comparison
@@ -434,12 +436,14 @@ public TransferResult<Nullness, NullnessStore> visitNotEqual(
   private TransferResult<Nullness, NullnessStore> handleEqualityComparison(
       TransferInput<Nullness, NullnessStore> input,
       Node leftOperand,
+      @Nullable Nullness leftOperandNullness,
       Node rightOperand,
       boolean equalTo) {
     ReadableUpdates thenUpdates = new ReadableUpdates();
     ReadableUpdates elseUpdates = new ReadableUpdates();
     SubNodeValues inputs = values(input);
-    Nullness leftVal = inputs.valueOfSubNode(leftOperand);
+    Nullness leftVal =
+        leftOperandNullness != null ? leftOperandNullness : inputs.valueOfSubNode(leftOperand);
     Nullness rightVal = inputs.valueOfSubNode(rightOperand);
     Nullness equalBranchValue = leftVal.greatestLowerBound(rightVal);
     Updates equalBranchUpdates = equalTo ? thenUpdates : elseUpdates;
@@ -965,7 +969,14 @@ public TransferResult<Nullness, NullnessStore> visitCase(
       // between the switch expression and the case operand.
       Node switchOperand = caseNode.getSwitchOperand().getExpression();
       Node caseOperand = caseOperands.get(0);
-      return handleEqualityComparison(input, switchOperand, caseOperand, true);
+      AccessPath switchOperandAccessPath =
+          AccessPath.getAccessPathForNode(switchOperand, state, apContext);
+      Nullness switchOperandNullness =
+          switchOperandAccessPath == null
+              ? null
+              : input.getRegularStore().getNullnessOfAccessPath(switchOperandAccessPath);
+      return handleEqualityComparison(
+          input, switchOperand, switchOperandNullness, caseOperand, true);
     }
   }