[nullability] A call to a non-const operator should also be considered mutating.

The additional test in this patch fails without the fix.

PiperOrigin-RevId: 608879021
Change-Id: I295a03fefbf4c5041e693daa211f524138e33479
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
index 3172144..1450863 100644
--- a/nullability/pointer_nullability_analysis.cc
+++ b/nullability/pointer_nullability_analysis.cc
@@ -1085,23 +1085,40 @@
   }
 }
 
+void handleNonConstMemberCall(absl::Nonnull<const CallExpr *> CE,
+                              dataflow::RecordStorageLocation *RecordLoc,
+                              const MatchFinder::MatchResult &Result,
+                              TransferState<PointerNullabilityLattice> &State) {
+  // When a non-const member function is called, reset all pointer-type fields
+  // of the receiver.
+  if (RecordLoc != nullptr) {
+    for (const auto [Field, FieldLoc] : RecordLoc->children()) {
+      if (!isSupportedRawPointerType(Field->getType())) continue;
+      auto &V = *cast<PointerValue>(State.Env.createValue(Field->getType()));
+      State.Env.setValue(*FieldLoc, V);
+    }
+    State.Lattice.clearConstMethodReturnValues(*RecordLoc);
+  }
+
+  // Perform default handling.
+  transferValue_CallExpr(CE, Result, State);
+}
+
 void transferValue_NonConstMemberCall(
     absl::Nonnull<const CXXMemberCallExpr *> MCE,
     const MatchFinder::MatchResult &Result,
     TransferState<PointerNullabilityLattice> &State) {
-  // When a non-const member function is called, reset all pointer-type fields
-  // of the implicit object.
-  if (dataflow::RecordStorageLocation *RecordLoc =
-          dataflow::getImplicitObjectLocation(*MCE, State.Env)) {
-    for (const auto [Field, FieldLoc] : RecordLoc->children()) {
-      if (!isSupportedRawPointerType(Field->getType())) continue;
-      Value *V = State.Env.createValue(Field->getType());
-      State.Env.setValue(*FieldLoc, *V);
-    }
-    State.Lattice.clearConstMethodReturnValues(*RecordLoc);
-  }
-  // The nullability of the Expr itself still needs to be handled.
-  transferValue_CallExpr(MCE, Result, State);
+  handleNonConstMemberCall(
+      MCE, dataflow::getImplicitObjectLocation(*MCE, State.Env), Result, State);
+}
+
+void transferValue_NonConstMemberOperatorCall(
+    absl::Nonnull<const CXXOperatorCallExpr *> OCE,
+    const MatchFinder::MatchResult &Result,
+    TransferState<PointerNullabilityLattice> &State) {
+  auto *RecordLoc = cast_or_null<dataflow::RecordStorageLocation>(
+      State.Env.getStorageLocation(*OCE->getArg(0)));
+  handleNonConstMemberCall(OCE, RecordLoc, Result, State);
 }
 
 void transferType_DeclRefExpr(absl::Nonnull<const DeclRefExpr *> DRE,
@@ -1469,6 +1486,9 @@
                                         transferValue_ConstMemberCall)
       .CaseOfCFGStmt<CXXMemberCallExpr>(isNonConstMemberCall(),
                                         transferValue_NonConstMemberCall)
+      .CaseOfCFGStmt<CXXOperatorCallExpr>(
+          isNonConstMemberOperatorCall(),
+          transferValue_NonConstMemberOperatorCall)
       .CaseOfCFGStmt<CallExpr>(ast_matchers::callExpr(), transferValue_CallExpr)
       .CaseOfCFGStmt<Expr>(isSmartPointerGlValue(), transferValue_SmartPointer)
       .CaseOfCFGStmt<MemberExpr>(isSmartPointerArrowMemberExpr(),
diff --git a/nullability/pointer_nullability_matchers.cc b/nullability/pointer_nullability_matchers.cc
index 47d26ce..854b68e 100644
--- a/nullability/pointer_nullability_matchers.cc
+++ b/nullability/pointer_nullability_matchers.cc
@@ -113,6 +113,10 @@
   return cxxMemberCallExpr(callee(cxxMethodDecl(unless(isConst()))));
 }
 
+Matcher<Stmt> isNonConstMemberOperatorCall() {
+  return cxxOperatorCallExpr(callee(cxxMethodDecl(unless(isConst()))));
+}
+
 Matcher<Stmt> isSmartPointerGlValue() {
   return expr(hasType(isSupportedSmartPointer()), isGLValue());
 }
diff --git a/nullability/pointer_nullability_matchers.h b/nullability/pointer_nullability_matchers.h
index 727f428..acd8b9f 100644
--- a/nullability/pointer_nullability_matchers.h
+++ b/nullability/pointer_nullability_matchers.h
@@ -47,6 +47,7 @@
 ast_matchers::internal::Matcher<CXXCtorInitializer> isCtorMemberInitializer();
 ast_matchers::internal::Matcher<Stmt> isZeroParamConstMemberCall();
 ast_matchers::internal::Matcher<Stmt> isNonConstMemberCall();
+ast_matchers::internal::Matcher<Stmt> isNonConstMemberOperatorCall();
 ast_matchers::internal::Matcher<Stmt> isSmartPointerGlValue();
 ast_matchers::internal::Matcher<Stmt> isSmartPointerArrowMemberExpr();
 ast_matchers::internal::Matcher<Stmt> isSmartPointerConstructor();
diff --git a/nullability/test/function_calls.cc b/nullability/test/function_calls.cc
index 1c81318..4c1d926 100644
--- a/nullability/test/function_calls.cc
+++ b/nullability/test/function_calls.cc
@@ -774,6 +774,7 @@
     struct C {
       int *_Nullable property() const;
       void may_mutate();
+      C &operator=(const C &);
     };
     void target() {
       C obj;
@@ -781,6 +782,11 @@
         obj.may_mutate();
         *obj.property();  // [[unsafe]]
       };
+      if (obj.property() != nullptr) {
+        // A non-const operator call may mutate as well.
+        obj = C();
+        *obj.property();  // [[unsafe]]
+      };
       if (obj.property() != nullptr) *obj.property();
     }
   )cc"));