Handle accessors that directly return a pointer member variable

This is to handle the common pattern where the return value of an accessor is tested to see if it is non-null and, if so, the accessor called again to dereference the pointer. For example (`get_const()` is annotated as nullable)
```
if (foo.get_const()) {
  foo.get_const()->doThing();  // SAFE
}
```

Additionally, non-const member functions are treated as if they mutate the values of any non-static member variables of pointer type.

```
if (foo.get_const()) {
  foo.nonconst_method();
  foo.get_const()->doThing();  // UNSAFE
}

PiperOrigin-RevId: 574227988
Change-Id: Ie28fd056d576f0e41f01ffe7cbae198f60fe68e1
diff --git a/nullability/BUILD b/nullability/BUILD
index 7b06d42..4ca6a1c 100644
--- a/nullability/BUILD
+++ b/nullability/BUILD
@@ -24,6 +24,7 @@
     hdrs = ["pointer_nullability_matchers.h"],
     visibility = [
         "//nullability/inference:__pkg__",
+        "//nullability/test:__pkg__",
     ],
     deps = [
         ":type_nullability",
@@ -32,6 +33,19 @@
     ],
 )
 
+cc_test(
+    name = "pointer_nullability_matchers_test",
+    srcs = ["pointer_nullability_matchers_test.cc"],
+    deps = [
+        ":pointer_nullability_matchers",
+        "@llvm-project//clang:ast_matchers",
+        "@llvm-project//clang:testing",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
 cc_library(
     name = "pointer_nullability_analysis",
     srcs = ["pointer_nullability_analysis.cc"],
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
index 2624727..adcd9c8 100644
--- a/nullability/pointer_nullability_analysis.cc
+++ b/nullability/pointer_nullability_analysis.cc
@@ -33,6 +33,7 @@
 #include "clang/Basic/LLVM.h"
 #include "clang/Basic/Specifiers.h"
 #include "llvm/Support/Debug.h"
+#include "llvm/Support/raw_ostream.h"
 
 namespace clang::tidy::nullability {
 
@@ -461,6 +462,35 @@
                               FuncDecl->getParamDecl(i)->getType());
   }
 }
+void transferFlowSensitiveAccessorCall(
+    const CXXMemberCallExpr *MCE, const MatchFinder::MatchResult &Result,
+    TransferState<PointerNullabilityLattice> &State) {
+  auto *member = Result.Nodes.getNodeAs<clang::ValueDecl>("member-decl");
+  dataflow::RecordStorageLocation *RecordLoc =
+      dataflow::getImplicitObjectLocation(*MCE, State.Env);
+  StorageLocation *Loc = RecordLoc->getChild(*member);
+  if (auto *PointerVal = dyn_cast<PointerValue>(State.Env.getValue(*Loc))) {
+    State.Env.setValue(*MCE, *PointerVal);
+    initPointerFromTypeNullability(*PointerVal, MCE, State);
+  }
+}
+
+void transferFlowSensitiveNonConstMemberCall(
+    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 (!isSupportedPointerType(Field->getType())) continue;
+      Value *V = State.Env.createValue(Field->getType());
+      State.Env.setValue(*FieldLoc, *V);
+    }
+  }
+  // The nullability of the Expr itself still needs to be handled.
+  transferFlowSensitiveCallExpr(MCE, Result, State);
+}
 
 // If nullability for the decl D has been overridden, patch N to reflect it.
 // (N is the nullability of an access to D).
@@ -783,6 +813,10 @@
       // pointers to the non-flow-sensitive part of the analysis.
       .CaseOfCFGStmt<Expr>(isNullPointerLiteral(),
                            transferFlowSensitiveNullPointer)
+      .CaseOfCFGStmt<CXXMemberCallExpr>(isSupportedPointerAccessorCall(),
+                                        transferFlowSensitiveAccessorCall)
+      .CaseOfCFGStmt<CXXMemberCallExpr>(isNonConstMemberCall(),
+                                        transferFlowSensitiveNonConstMemberCall)
       .CaseOfCFGStmt<CallExpr>(isCallExpr(), transferFlowSensitiveCallExpr)
       .CaseOfCFGStmt<Expr>(isPointerExpr(), transferFlowSensitivePointer)
       // Handles comparison between 2 pointers.
diff --git a/nullability/pointer_nullability_matchers.cc b/nullability/pointer_nullability_matchers.cc
index be9ca0f..00b1720 100644
--- a/nullability/pointer_nullability_matchers.cc
+++ b/nullability/pointer_nullability_matchers.cc
@@ -13,24 +13,36 @@
 
 using ast_matchers::anyOf;
 using ast_matchers::binaryOperator;
+using ast_matchers::callee;
 using ast_matchers::callExpr;
+using ast_matchers::compoundStmt;
 using ast_matchers::cxxConstructExpr;
 using ast_matchers::cxxCtorInitializer;
+using ast_matchers::cxxMemberCallExpr;
+using ast_matchers::cxxMethodDecl;
 using ast_matchers::cxxThisExpr;
+using ast_matchers::decl;
 using ast_matchers::expr;
+using ast_matchers::has;
 using ast_matchers::hasAnyOperatorName;
+using ast_matchers::hasBody;
 using ast_matchers::hasCastKind;
+using ast_matchers::hasDeclaration;
 using ast_matchers::hasOperands;
 using ast_matchers::hasOperatorName;
 using ast_matchers::hasReturnValue;
 using ast_matchers::hasType;
 using ast_matchers::hasUnaryOperand;
+using ast_matchers::ignoringParenImpCasts;
 using ast_matchers::implicitCastExpr;
 using ast_matchers::isArrow;
+using ast_matchers::isConst;
 using ast_matchers::isMemberInitializer;
 using ast_matchers::memberExpr;
 using ast_matchers::returnStmt;
+using ast_matchers::statementCountIs;
 using ast_matchers::unaryOperator;
+using ast_matchers::unless;
 using ast_matchers::internal::Matcher;
 
 Matcher<Stmt> isPointerExpr() { return expr(hasType(isSupportedPointer())); }
@@ -63,4 +75,19 @@
   return cxxCtorInitializer(isMemberInitializer());
 }
 
+Matcher<Stmt> isNonConstMemberCall() {
+  return cxxMemberCallExpr(callee(cxxMethodDecl(unless(isConst()))));
+}
+
+Matcher<Stmt> isSupportedPointerAccessorCall() {
+  return cxxMemberCallExpr(callee(cxxMethodDecl(hasBody(compoundStmt(
+      statementCountIs(1),
+      has(returnStmt(has(implicitCastExpr(
+          hasCastKind(CK_LValueToRValue),
+          has(ignoringParenImpCasts(
+              memberExpr(has(ignoringParenImpCasts(cxxThisExpr())),
+                         hasType(isSupportedPointer()),
+                         hasDeclaration(decl().bind("member-decl"))))))))))))));
+}
+
 }  // namespace clang::tidy::nullability
diff --git a/nullability/pointer_nullability_matchers.h b/nullability/pointer_nullability_matchers.h
index ea0ed26..4e67076 100644
--- a/nullability/pointer_nullability_matchers.h
+++ b/nullability/pointer_nullability_matchers.h
@@ -30,6 +30,8 @@
 ast_matchers::internal::Matcher<Stmt> isPointerReturn();
 ast_matchers::internal::Matcher<Stmt> isConstructExpr();
 ast_matchers::internal::Matcher<CXXCtorInitializer> isCtorMemberInitializer();
+ast_matchers::internal::Matcher<Stmt> isNonConstMemberCall();
+ast_matchers::internal::Matcher<Stmt> isSupportedPointerAccessorCall();
 
 }  // namespace nullability
 }  // namespace tidy
diff --git a/nullability/pointer_nullability_matchers_test.cc b/nullability/pointer_nullability_matchers_test.cc
new file mode 100644
index 0000000..075b5ac
--- /dev/null
+++ b/nullability/pointer_nullability_matchers_test.cc
@@ -0,0 +1,63 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "nullability/pointer_nullability_matchers.h"
+
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Testing/TestAST.h"
+#include "llvm/ADT/StringRef.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang::tidy::nullability {
+namespace {
+
+using ast_matchers::match;
+
+template <typename MatcherT>
+bool matches(llvm::StringRef base_input, llvm::StringRef test_input,
+             MatcherT Matcher) {
+  TestAST InputAST(base_input.str() + test_input.str());
+  return !match(Matcher, InputAST.context()).empty();
+}
+
+TEST(PointerNullabilityTest, MatchMemberFunctions) {
+  llvm::StringRef Input(R"cc(
+    struct DummyStruct {
+      int *p;
+    };
+    class C {
+     public:
+      int *_Nullable get() const { return x; }
+      int *_Nullable get_member_in_parens() const { return (x); }
+      int *_Nullable get_this_in_parens() const { return (this)->x; }
+      int *_Nullable get(int i) const { return x; }
+      int *_Nullable get_nonconst() { return x; }
+      int *_Nullable get_external() { return ds.p; }
+      void mutate(){};
+
+     private:
+      int *x;
+      DummyStruct ds;
+    };
+  )cc");
+
+  EXPECT_TRUE(matches(Input, "void target(){ C().get(); }",
+                      isSupportedPointerAccessorCall()));
+  EXPECT_TRUE(matches(Input, "void target(){ C().get_member_in_parens(); }",
+                      isSupportedPointerAccessorCall()));
+  EXPECT_TRUE(matches(Input, "void target(){ C().get_this_in_parens(); }",
+                      isSupportedPointerAccessorCall()));
+  EXPECT_TRUE(matches(Input, "void target(){ C().get(0); }",
+                      isSupportedPointerAccessorCall()));
+  EXPECT_TRUE(matches(Input, "void target(){ C().get_nonconst(); }",
+                      isSupportedPointerAccessorCall()));
+
+  EXPECT_FALSE(matches(Input, "void target(){ C().mutate(); }",
+                       isSupportedPointerAccessorCall()));
+  EXPECT_FALSE(matches(Input, "void target(){ C().get_external(); }",
+                       isSupportedPointerAccessorCall()));
+}
+
+}  // namespace
+}  // namespace clang::tidy::nullability