Separate analysis and diagnosis components for pointer nullability verification.

- `pointer_nullability_analysis.h/cc`: accummulate nullability information of pointers.
- `pointer_nullability_diagnosis.h/cc`: checks null safety (e.g. if a dereference is safe).
- `pointer_nullability.h/cc`: common functions for updating a PointerValue's nullability.

PiperOrigin-RevId: 464823057
diff --git a/nullability_verification/BUILD b/nullability_verification/BUILD
index a3aa604..bf12ade 100644
--- a/nullability_verification/BUILD
+++ b/nullability_verification/BUILD
@@ -24,6 +24,7 @@
     srcs = ["pointer_nullability_analysis.cc"],
     hdrs = ["pointer_nullability_analysis.h"],
     deps = [
+        ":pointer_nullability",
         ":pointer_nullability_lattice",
         ":pointer_nullability_matchers",
         "//common:check",
@@ -35,11 +36,38 @@
     ],
 )
 
+cc_library(
+    name = "pointer_nullability_diagnosis",
+    srcs = ["pointer_nullability_diagnosis.cc"],
+    hdrs = ["pointer_nullability_diagnosis.h"],
+    deps = [
+        ":pointer_nullability",
+        ":pointer_nullability_matchers",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "pointer_nullability",
+    srcs = ["pointer_nullability.cc"],
+    hdrs = ["pointer_nullability.h"],
+    deps = [
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
 cc_test(
-    name = "pointer_nullability_analysis_test",
-    srcs = ["pointer_nullability_analysis_test.cc"],
+    name = "pointer_nullability_verification_test",
+    srcs = ["pointer_nullability_verification_test.cc"],
     deps = [
         ":pointer_nullability_analysis",
+        ":pointer_nullability_diagnosis",
+        "@llvm-project//clang:basic",
         "@llvm-project//clang/unittests:dataflow_testing_support",
         "@llvm-project//llvm:Support",
         "@llvm-project//llvm:TestingSupport",
diff --git a/nullability_verification/pointer_nullability.cc b/nullability_verification/pointer_nullability.cc
new file mode 100644
index 0000000..ffd81d5
--- /dev/null
+++ b/nullability_verification/pointer_nullability.cc
@@ -0,0 +1,56 @@
+// 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_verification/pointer_nullability.h"
+
+#include "clang/Analysis/FlowSensitive/Value.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using dataflow::AtomicBoolValue;
+using dataflow::BoolValue;
+using dataflow::Environment;
+using dataflow::PointerValue;
+using dataflow::SkipPast;
+
+/// The nullness information of a pointer is represented by two properties
+/// which indicate if a pointer's nullability (i.e., if the pointer can hold
+/// null) is `Known` and if the pointer's value is `NotNull`.
+constexpr llvm::StringLiteral kKnown = "is_known";
+constexpr llvm::StringLiteral kNotNull = "is_notnull";
+
+std::pair<AtomicBoolValue&, AtomicBoolValue&> getPointerNullState(
+    const Expr* PointerExpr, const Environment& Env) {
+  auto* PointerVal =
+      cast<PointerValue>(Env.getValue(*PointerExpr, SkipPast::Reference));
+  auto& PointerKnown = *cast<AtomicBoolValue>(PointerVal->getProperty(kKnown));
+  auto& PointerNotNull =
+      *cast<AtomicBoolValue>(PointerVal->getProperty(kNotNull));
+  return {PointerKnown, PointerNotNull};
+}
+
+void initPointerBoolProperty(PointerValue& PointerVal, llvm::StringRef Name,
+                             BoolValue* BoolVal, Environment& Env) {
+  if (PointerVal.getProperty(Name) == nullptr) {
+    PointerVal.setProperty(Name,
+                           BoolVal ? *BoolVal : Env.makeAtomicBoolValue());
+  }
+}
+
+void initPointerNullState(const Expr* PointerExpr, Environment& Env,
+                          BoolValue* KnownConstraint,
+                          BoolValue* NotNullConstraint) {
+  if (auto* PointerVal = cast_or_null<PointerValue>(
+          Env.getValue(*PointerExpr, SkipPast::Reference))) {
+    initPointerBoolProperty(*PointerVal, kKnown, KnownConstraint, Env);
+    initPointerBoolProperty(*PointerVal, kNotNull, NotNullConstraint, Env);
+  }
+}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
\ No newline at end of file
diff --git a/nullability_verification/pointer_nullability.h b/nullability_verification/pointer_nullability.h
new file mode 100644
index 0000000..96444b0
--- /dev/null
+++ b/nullability_verification/pointer_nullability.h
@@ -0,0 +1,39 @@
+// 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
+
+#ifndef CRUBIT_NULLABILITY_VERIFICATION_POINTER_NULLABILITY_H_
+#define CRUBIT_NULLABILITY_VERIFICATION_POINTER_NULLABILITY_H_
+
+#include <utility>
+
+#include "clang/AST/Expr.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+/// Returns the properties representing the nullness information of a pointer.
+///
+/// The first boolean indicates if the pointer's nullability is known.
+/// The second boolean indicates if the pointer's value is not null.
+std::pair<dataflow::AtomicBoolValue&, dataflow::AtomicBoolValue&>
+getPointerNullState(const Expr* PointerExpr, const dataflow::Environment& Env);
+
+/// Sets the nullness properties on the PointerValue assigned to `PointerExpr`
+/// if not already initialised.
+///
+/// The boolean properties may be constrained by passing in the BoolValues
+/// representing true or false to `KnownConstraint` and `NotNullConstraint`.
+/// Otherwise, the properties are set to freshly created atomic booleans.
+void initPointerNullState(const Expr* PointerExpr, dataflow::Environment& Env,
+                          dataflow::BoolValue* KnownConstraint,
+                          dataflow::BoolValue* NotNullConstraint = nullptr);
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // CRUBIT_NULLABILITY_VERIFICATION_POINTER_NULLABILITY_H_
diff --git a/nullability_verification/pointer_nullability_analysis.cc b/nullability_verification/pointer_nullability_analysis.cc
index c098111..11b8efd 100644
--- a/nullability_verification/pointer_nullability_analysis.cc
+++ b/nullability_verification/pointer_nullability_analysis.cc
@@ -7,6 +7,7 @@
 #include <string>
 
 #include "common/check.h"
+#include "nullability_verification/pointer_nullability.h"
 #include "nullability_verification/pointer_nullability_lattice.h"
 #include "nullability_verification/pointer_nullability_matchers.h"
 #include "clang/AST/ASTContext.h"
@@ -25,55 +26,19 @@
 namespace nullability {
 
 using ast_matchers::MatchFinder;
-using dataflow::AtomicBoolValue;
 using dataflow::BoolValue;
 using dataflow::Environment;
 using dataflow::MatchSwitchBuilder;
-using dataflow::PointerValue;
 using dataflow::SkipPast;
 using dataflow::TransferState;
 using dataflow::Value;
 
 namespace {
 
-constexpr llvm::StringLiteral kKnown = "is_known";
-constexpr llvm::StringLiteral kNotNull = "is_notnull";
-
-std::pair<AtomicBoolValue&, AtomicBoolValue&> getPointerNullState(
-    const Expr* PointerExpr, TransferState<PointerNullabilityLattice>& State) {
-  auto* PointerVal =
-      cast<PointerValue>(State.Env.getValue(*PointerExpr, SkipPast::Reference));
-  auto& PointerKnown = *cast<AtomicBoolValue>(PointerVal->getProperty(kKnown));
-  auto& PointerNotNull =
-      *cast<AtomicBoolValue>(PointerVal->getProperty(kNotNull));
-  return {PointerKnown, PointerNotNull};
-}
-
-void initPointerBoolProperty(PointerValue& PointerVal, llvm::StringRef Name,
-                             BoolValue* BoolVal, Environment& Env) {
-  if (PointerVal.getProperty(Name) == nullptr) {
-    PointerVal.setProperty(Name,
-                           BoolVal ? *BoolVal : Env.makeAtomicBoolValue());
-  }
-}
-
-/// The nullness information of a pointer is represented by two properties which
-/// indicate if a pointer's nullability (i.e. if the pointer can hold null) is
-/// `Known` and if the pointer's value is `NotNull`.
-void initPointerNullState(const Expr* PointerExpr,
-                          TransferState<PointerNullabilityLattice>& State,
-                          BoolValue* Known, BoolValue* NotNull = nullptr) {
-  if (auto* PointerVal = cast_or_null<PointerValue>(
-          State.Env.getValue(*PointerExpr, SkipPast::Reference))) {
-    initPointerBoolProperty(*PointerVal, kKnown, Known, State.Env);
-    initPointerBoolProperty(*PointerVal, kNotNull, NotNull, State.Env);
-  }
-}
-
 void transferInitNotNullPointer(
     const Expr* NotNullPointer, const MatchFinder::MatchResult&,
     TransferState<PointerNullabilityLattice>& State) {
-  initPointerNullState(NotNullPointer, State,
+  initPointerNullState(NotNullPointer, State.Env,
                        /*Known=*/&State.Env.getBoolLiteralValue(true),
                        /*NotNull=*/&State.Env.getBoolLiteralValue(true));
 }
@@ -81,7 +46,7 @@
 void transferInitNullPointer(const Expr* NullPointer,
                              const MatchFinder::MatchResult&,
                              TransferState<PointerNullabilityLattice>& State) {
-  initPointerNullState(NullPointer, State,
+  initPointerNullState(NullPointer, State.Env,
                        /*Known=*/&State.Env.getBoolLiteralValue(true),
                        /*NotNull=*/&State.Env.getBoolLiteralValue(false));
 }
@@ -89,50 +54,21 @@
 void transferInitNullablePointer(
     const Expr* NullablePointer,
     TransferState<PointerNullabilityLattice>& State) {
-  initPointerNullState(NullablePointer, State,
+  initPointerNullState(NullablePointer, State.Env,
                        /*Known=*/&State.Env.getBoolLiteralValue(true));
 }
 
 void transferInitPointerFromDecl(
     const Expr* PointerExpr, const MatchFinder::MatchResult&,
     TransferState<PointerNullabilityLattice>& State) {
-  // TODO(wyt): Implement processing of nullability annotations. The current
-  // implementation treats unnannotated pointers as nullable.
+  // TODO(b/233582219): Implement processing of nullability annotations. The
+  // current implementation treats unnannotated pointers as nullable.
   transferInitNullablePointer(PointerExpr, State);
 }
 
-void transferPointerAccess(const Expr* PointerExpr,
-                           TransferState<PointerNullabilityLattice>& State) {
-  auto [PointerKnown, PointerNotNull] = getPointerNullState(PointerExpr, State);
-  auto& PointerNotKnownNull = State.Env.makeNot(
-      State.Env.makeAnd(PointerKnown, State.Env.makeNot(PointerNotNull)));
-  if (!State.Env.flowConditionImplies(PointerNotKnownNull)) {
-    State.Lattice.addViolation(PointerExpr);
-  }
-}
-
-void transferDereference(const UnaryOperator* UnaryOp,
-                         const MatchFinder::MatchResult&,
-                         TransferState<PointerNullabilityLattice>& State) {
-  transferPointerAccess(UnaryOp->getSubExpr(), State);
-}
-
-void transferMemberExprInvolvingPointers(
-    const MemberExpr* MemberExpr, const MatchFinder::MatchResult& Result,
-    TransferState<PointerNullabilityLattice>& State) {
-  if (MemberExpr->isArrow()) {
-    // Base expr is a pointer, check that (->) access is safe
-    transferPointerAccess(MemberExpr->getBase(), State);
-  }
-  if (MemberExpr->getType()->isAnyPointerType()) {
-    // Accessed member is a pointer, initialise its nullability
-    transferInitPointerFromDecl(MemberExpr, Result, State);
-  }
-}
-
-// TODO(wyt): Implement promotion of nullability knownness for initially unknown
-// pointers when there is evidence that it is nullable, for example when the
-// pointer is compared to nullptr, or casted to boolean.
+// TODO(b/233582219): Implement promotion of nullability knownness for initially
+// unknown pointers when there is evidence that it is nullable, for example
+// when the pointer is compared to nullptr, or casted to boolean.
 void transferNullCheckComparison(
     const BinaryOperator* BinaryOp, const MatchFinder::MatchResult& result,
     TransferState<PointerNullabilityLattice>& State) {
@@ -149,8 +85,10 @@
                         ? State.Env.makeNot(PointerComparison)
                         : PointerComparison;
 
-  auto [LHSKnown, LHSNotNull] = getPointerNullState(BinaryOp->getLHS(), State);
-  auto [RHSKnown, RHSNotNull] = getPointerNullState(BinaryOp->getRHS(), State);
+  auto [LHSKnown, LHSNotNull] =
+      getPointerNullState(BinaryOp->getLHS(), State.Env);
+  auto [RHSKnown, RHSNotNull] =
+      getPointerNullState(BinaryOp->getRHS(), State.Env);
   auto& LHSKnownNotNull = State.Env.makeAnd(LHSKnown, LHSNotNull);
   auto& RHSKnownNotNull = State.Env.makeAnd(RHSKnown, RHSNotNull);
   auto& LHSKnownNull =
@@ -173,7 +111,7 @@
     const Expr* CastExpr, const MatchFinder::MatchResult&,
     TransferState<PointerNullabilityLattice>& State) {
   auto [PointerKnown, PointerNotNull] =
-      getPointerNullState(CastExpr->IgnoreImplicit(), State);
+      getPointerNullState(CastExpr->IgnoreImplicit(), State.Env);
   auto& CastExprLoc = State.Env.createStorageLocation(*CastExpr);
   State.Env.setValue(CastExprLoc, PointerNotNull);
   State.Env.setStorageLocation(*CastExpr, CastExprLoc);
@@ -186,12 +124,7 @@
       .CaseOf<Expr>(isCXXThisExpr(), transferInitNotNullPointer)
       .CaseOf<Expr>(isAddrOf(), transferInitNotNullPointer)
       .CaseOf<Expr>(isNullPointerLiteral(), transferInitNullPointer)
-      // Handles initialization of null states of member pointers and safety of
-      // member access (->) on pointers
-      .CaseOf<MemberExpr>(isMemberExprInvolvingPointers(),
-                          transferMemberExprInvolvingPointers)
-      // Handles pointer dereferencing (*ptr)
-      .CaseOf<UnaryOperator>(isPointerDereference(), transferDereference)
+      .CaseOf<MemberExpr>(isMemberOfPointerType(), transferInitPointerFromDecl)
       // Handles comparison between 2 pointers
       .CaseOf<BinaryOperator>(isPointerCheckBinOp(),
                               transferNullCheckComparison)
diff --git a/nullability_verification/pointer_nullability_analysis.h b/nullability_verification/pointer_nullability_analysis.h
index f15c358..30ea8f7 100644
--- a/nullability_verification/pointer_nullability_analysis.h
+++ b/nullability_verification/pointer_nullability_analysis.h
@@ -26,6 +26,8 @@
 // TODO(b/233582219): Implement tracking of nullability to distinguish
 // safe/unsafe accesses
 
+/// Analyses constructs in the source code to collect nullability information
+/// about pointers at each program point.
 class PointerNullabilityAnalysis
     : public dataflow::DataflowAnalysis<PointerNullabilityAnalysis,
                                         PointerNullabilityLattice> {
diff --git a/nullability_verification/pointer_nullability_analysis_test.cc b/nullability_verification/pointer_nullability_analysis_test.cc
deleted file mode 100644
index 77f64bf..0000000
--- a/nullability_verification/pointer_nullability_analysis_test.cc
+++ /dev/null
@@ -1,638 +0,0 @@
-// 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_verification/pointer_nullability_analysis.h"
-
-#include <string>
-
-#include "third_party/llvm/llvm-project/clang/unittests/Analysis/FlowSensitive/TestingSupport.h"
-#include "llvm/ADT/ArrayRef.h"
-#include "llvm/ADT/StringRef.h"
-#include "llvm/Support/Error.h"
-#include "llvm/Testing/Support/Error.h"
-#include "third_party/llvm/llvm-project/llvm/utils/unittest/googletest/include/gtest/gtest.h"
-
-namespace clang {
-namespace tidy {
-namespace nullability {
-namespace {
-
-using ::testing::Pair;
-using ::testing::Test;
-using ::testing::UnorderedElementsAre;
-
-using dataflow::DataflowAnalysisState;
-using dataflow::Environment;
-using dataflow::test::checkDataflow;
-
-MATCHER(IsSafe, "") { return arg.Lattice.isSafe(); }
-MATCHER(IsUnsafe, "") { return !arg.Lattice.isSafe(); }
-
-class PointerNullabilityTest : public Test {
- protected:
-  template <typename Matcher>
-  void expectDataflow(llvm::StringRef Code, Matcher Expectations) {
-    ASSERT_THAT_ERROR(
-        checkDataflow<PointerNullabilityAnalysis>(
-            Code, "target",
-            [](ASTContext &ASTCtx, Environment &) {
-              return PointerNullabilityAnalysis(ASTCtx);
-            },
-            [&Expectations](
-                llvm::ArrayRef<
-                    std::pair<std::string,
-                              DataflowAnalysisState<PointerNullabilityLattice>>>
-                    Results,
-                ASTContext &) { EXPECT_THAT(Results, Expectations); },
-            {"-fsyntax-only", "-std=c++17", "-Wno-unused-value"}),
-        llvm::Succeeded());
-  }
-};
-
-TEST_F(PointerNullabilityTest, NoPointerOperations) {
-  std::string Code = R"(
-    void target() {
-      1 + 2;
-      /*[[safe]]*/
-    }
-  )";
-  expectDataflow(Code, UnorderedElementsAre(Pair("safe", IsSafe())));
-}
-
-TEST_F(PointerNullabilityTest, DereferenceWithoutACheck) {
-  std::string Code = R"(
-    void target(int* maybeNull) {
-      *maybeNull;
-      /*[[unsafe]]*/
-    }
-  )";
-  expectDataflow(Code, UnorderedElementsAre(Pair("unsafe", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, InitializedWithNullPtrLiteral) {
-  std::string NullPtr = R"(
-    void target() {
-      int *null = nullptr;
-      *null;
-      /*[[unsafe]]*/
-    }
-  )";
-  expectDataflow(NullPtr, UnorderedElementsAre(Pair("unsafe", IsUnsafe())));
-
-  std::string ZeroAsNull = R"(
-    void target() {
-      int *null = 0;
-      *null;
-      /*[[unsafe]]*/
-    }
-  )";
-  expectDataflow(ZeroAsNull, UnorderedElementsAre(Pair("unsafe", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, InitializedWithAddressOf) {
-  std::string Code = R"(
-    void target(int x) {
-      int *nonNull = &x;
-      *nonNull;
-      /*[[safe]]*/
-    }
-  )";
-  expectDataflow(Code, UnorderedElementsAre(Pair("safe", IsSafe())));
-}
-
-TEST_F(PointerNullabilityTest, InitializedWithOtherPointer) {
-  std::string DerefCopyOfNonNull = R"(
-    void target(int x) {
-      int *nonNull = &x;
-      int *nonNullCopy = nonNull;
-      *nonNullCopy;
-      /*[[safe]]*/
-    }
-  )";
-  expectDataflow(DerefCopyOfNonNull,
-                 UnorderedElementsAre(Pair("safe", IsSafe())));
-
-  std::string DerefCopyOfNullable = R"(
-    void target(int* nullable) {
-      int *nullableCopy = nullable;
-      *nullableCopy;
-      /*[[unsafe]]*/
-    }
-  )";
-  expectDataflow(DerefCopyOfNullable,
-                 UnorderedElementsAre(Pair("unsafe", IsUnsafe())));
-
-  std::string DerefCopyOfNullableCheckOriginal = R"(
-    void target(int* nullable) {
-      int *nullableCopy = nullable;
-      if (nullable) {
-        *nullableCopy;
-        /*[[safe]]*/
-      } else {
-        *nullableCopy;
-        /*[[unsafe-1]]*/
-      }
-      *nullableCopy;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      DerefCopyOfNullableCheckOriginal,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string DerefNullableCheckCopy = R"(
-    void target(int* nullable) {
-      int *nullableCopy = nullable;
-      if (nullableCopy) {
-        *nullable;
-        /*[[safe]]*/
-      } else {
-        *nullable;
-        /*[[unsafe-1]]*/
-      }
-      *nullable;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      DerefNullableCheckCopy,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, CheckByComparisonToNullPtr) {
-  std::string NENullRight = R"(
-    void target(int *maybeNull) {
-      if (maybeNull != nullptr) {
-        *maybeNull;
-        /*[[safe]]*/
-      } else {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      NENullRight,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string NENullLeft = R"(
-    void target(int *maybeNull) {
-      if (nullptr != maybeNull) {
-        *maybeNull;
-        /*[[safe]]*/
-      } else {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      NENullLeft,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string EQNullRight = R"(
-    void target(int* maybeNull) {
-      if (maybeNull == nullptr) {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      } else {
-        *maybeNull;
-        /*[[safe]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      EQNullRight,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string EQNullLeft = R"(
-    void target(int* maybeNull) {
-      if (nullptr == maybeNull) {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      } else {
-        *maybeNull;
-        /*[[safe]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      EQNullLeft,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, CheckByImplicitCastToBool) {
-  std::string PointerAsBool = R"(
-    void target(int* maybeNull) {
-      if (maybeNull) {
-        *maybeNull;
-        /*[[safe]]*/
-      } else {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      PointerAsBool,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string PointerAsBoolNegated = R"(
-    void target(int* maybeNull) {
-      if (!maybeNull) {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      } else {
-        *maybeNull;
-        /*[[safe]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      PointerAsBoolNegated,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, CheckByComparisonToOtherNullPtr) {
-  std::string NEOtherNullPtr = R"(
-    void target(int* maybeNull) {
-      int *null = nullptr;
-      if (maybeNull != null) {
-        *maybeNull;
-        /*[[safe]]*/
-      } else {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      NEOtherNullPtr,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string EQOtherNullPtr = R"(
-    void target(int* maybeNull) {
-      int *null = nullptr;
-      if (maybeNull == null) {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      } else {
-        *maybeNull;
-        /*[[safe]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      EQOtherNullPtr,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, CheckByComparisonToOtherNonNullPtr) {
-  std::string NEOtherNonNullPtr = R"(
-    void target(int* maybeNull, int x) {
-      int* nonNull = &x;
-      if (maybeNull != nonNull) {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      } else {
-        *maybeNull;
-        /*[[safe]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(NEOtherNonNullPtr,
-                 UnorderedElementsAre(Pair("unsafe-1", IsUnsafe()),
-                                      Pair("unsafe-2", IsUnsafe()),
-                                      Pair("safe", IsSafe())));
-
-  std::string EQOtherNonNullPtr = R"(
-    void target(int* maybeNull, int x) {
-      int* nonNull = &x;
-      if (maybeNull == nonNull) {
-        *maybeNull;
-        /*[[safe]]*/
-      } else {
-        *maybeNull;
-        /*[[unsafe-1]]*/
-      }
-      *maybeNull;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      EQOtherNonNullPtr,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, CheckByComparisonToOtherUnknownPtr) {
-  std::string NEOtherUnknownPtr = R"(
-    void target(int* x, int* y) {
-      if (x != y) {
-        *x;
-        /*[[unsafe-1]]*/
-      } else {
-        *x;
-        /*[[unsafe-2]]*/
-      }
-      *x;
-      /*[[unsafe-3]]*/
-    }
-  )";
-  expectDataflow(NEOtherUnknownPtr,
-                 UnorderedElementsAre(Pair("unsafe-1", IsUnsafe()),
-                                      Pair("unsafe-2", IsUnsafe()),
-                                      Pair("unsafe-3", IsUnsafe())));
-
-  std::string EQOtherUnknownPtr = R"(
-    void target(int* x, int* y) {
-      if (x == y) {
-        *x;
-        /*[[unsafe-1]]*/
-      } else {
-        *x;
-        /*[[unsafe-2]]*/
-      }
-      *x;
-      /*[[unsafe-3]]*/
-    }
-  )";
-  expectDataflow(EQOtherUnknownPtr,
-                 UnorderedElementsAre(Pair("unsafe-1", IsUnsafe()),
-                                      Pair("unsafe-2", IsUnsafe()),
-                                      Pair("unsafe-3", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, BinaryExpressions) {
-  std::string And = R"(
-    void target(int* x, int* y) {
-      if (x && y) {
-        *x;
-        /*[[safe]]*/
-      } else {
-        *x;
-        /*[[unsafe-1]]*/
-      }
-      *x;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(And, UnorderedElementsAre(Pair("safe", IsSafe()),
-                                           Pair("unsafe-1", IsUnsafe()),
-                                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string Or = R"(
-    void target(int* x, int* y) {
-      if (x || y) {
-        *x;
-        /*[[unsafe-1]]*/
-      } else {
-        *x;
-        /*[[unsafe-2]]*/
-      }
-      *x;
-      /*[[unsafe-3]]*/
-    }
-  )";
-  expectDataflow(Or, UnorderedElementsAre(Pair("unsafe-1", IsUnsafe()),
-                                          Pair("unsafe-2", IsUnsafe()),
-                                          Pair("unsafe-3", IsUnsafe())));
-
-  std::string AndNegatedBoth = R"(
-    void target(int* x, int* y) {
-      if (!x && !y) {
-        *x;
-        /*[[unsafe-1]]*/
-      } else {
-        *x;
-        /*[[unsafe-2]]*/
-      }
-      *x;
-      /*[[unsafe-3]]*/
-    }
-  )";
-  expectDataflow(AndNegatedBoth,
-                 UnorderedElementsAre(Pair("unsafe-1", IsUnsafe()),
-                                      Pair("unsafe-2", IsUnsafe()),
-                                      Pair("unsafe-3", IsUnsafe())));
-
-  std::string OrNegatedBoth = R"(
-    void target(int* x, int* y) {
-      if (!x || !y) {
-        *x;
-        /*[[unsafe-1]]*/
-      } else {
-        *x;
-        /*[[safe]]*/
-      }
-      *x;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      OrNegatedBoth,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, MemberPointers) {
-  std::string DerefStructMember = R"(
-    struct Foo {
-      Foo* ptr;
-    };
-    void target(Foo foo) {
-      if (foo.ptr) {
-        *foo.ptr;
-        /*[[safe]]*/
-      } else {
-        *foo.ptr;
-        /*[[unsafe-1]]*/
-      }
-      *foo.ptr;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      DerefStructMember,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string DerefStructMemberInsideMemberFunction = R"(
-    struct Foo {
-      Foo* ptr;
-      void target() {
-        if (ptr) {
-          *ptr;
-          /*[[safe]]*/
-        } else {
-          *ptr;
-          /*[[unsafe-1]]*/
-        }
-        *ptr;
-        /*[[unsafe-2]]*/
-      }
-    };
-  )";
-  expectDataflow(
-      DerefStructMemberInsideMemberFunction,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string DerefClassMember = R"(
-    class Foo {
-     public:
-      Foo* ptr;
-    };
-    void target(Foo foo) {
-      if (foo.ptr) {
-        *foo.ptr;
-        /*[[safe]]*/
-      } else {
-        *foo.ptr;
-        /*[[unsafe-1]]*/
-      }
-      *foo.ptr;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      DerefClassMember,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-TEST_F(PointerNullabilityTest, MemberAccessOnPointer) {
-  std::string MemberAccess = R"(
-    struct Foo {
-      void foo();
-    };
-    void target(Foo* foo) {
-      if (foo) {
-        foo->foo();
-        /*[[safe]]*/
-      } else {
-        foo->foo();
-        /*[[unsafe-1]]*/
-      }
-      foo->foo();
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      MemberAccess,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-
-  std::string MemberAccessOnImplicitThis = R"(
-    struct Foo {
-      void foo();
-      void target() {
-        foo();
-        /*[[safe]]*/
-      }
-    };
-  )";
-  expectDataflow(MemberAccessOnImplicitThis,
-                 UnorderedElementsAre(Pair("safe", IsSafe())));
-
-  std::string MemberAccessOnExplicitThis = R"(
-    struct Foo {
-      void foo();
-      void target() {
-        this->foo();
-        /*[[safe]]*/
-      }
-    };
-  )";
-  expectDataflow(MemberAccessOnExplicitThis,
-                 UnorderedElementsAre(Pair("safe", IsSafe())));
-
-  std::string MemberAccessOnCopyOfThis = R"(
-    struct Foo {
-      void foo();
-      void target() {
-        Foo *thisCopy = this;
-        thisCopy->foo();
-        /*[[safe]]*/
-      }
-    };
-  )";
-  expectDataflow(MemberAccessOnCopyOfThis,
-                 UnorderedElementsAre(Pair("safe", IsSafe())));
-
-  std::string AccessChainOnlyCheckOnFirst = R"(
-    struct Foo {
-      Foo* foo;
-    };
-    void target(Foo* foo) {
-      if (foo) {
-        foo->foo->foo;
-        /*[[unsafe-1]]*/
-      } else {
-        foo->foo->foo;
-        /*[[unsafe-2]]*/
-      }
-      foo->foo->foo;
-      /*[[unsafe-3]]*/
-    }
-  )";
-  expectDataflow(AccessChainOnlyCheckOnFirst,
-                 UnorderedElementsAre(Pair("unsafe-1", IsUnsafe()),
-                                      Pair("unsafe-2", IsUnsafe()),
-                                      Pair("unsafe-3", IsUnsafe())));
-
-  std::string AccessChainCheckOnAll = R"(
-    struct Foo {
-      Foo* foo;
-    };
-    void target(Foo* foo) {
-      if (foo && foo->foo) {
-        foo->foo->foo;
-        /*[[safe]]*/
-      } else {
-        foo->foo;
-        /*[[unsafe-1]]*/
-      }
-      foo->foo;
-      /*[[unsafe-2]]*/
-    }
-  )";
-  expectDataflow(
-      AccessChainCheckOnAll,
-      UnorderedElementsAre(Pair("safe", IsSafe()), Pair("unsafe-1", IsUnsafe()),
-                           Pair("unsafe-2", IsUnsafe())));
-}
-
-}  // namespace
-}  // namespace nullability
-}  // namespace tidy
-}  // namespace clang
diff --git a/nullability_verification/pointer_nullability_diagnosis.cc b/nullability_verification/pointer_nullability_diagnosis.cc
new file mode 100644
index 0000000..6c4e4e6
--- /dev/null
+++ b/nullability_verification/pointer_nullability_diagnosis.cc
@@ -0,0 +1,62 @@
+// 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_verification/pointer_nullability_diagnosis.h"
+
+#include "nullability_verification/pointer_nullability.h"
+#include "nullability_verification/pointer_nullability_matchers.h"
+#include "clang/AST/Expr.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using ast_matchers::MatchFinder;
+using dataflow::Environment;
+
+namespace {
+
+llvm::Optional<const Stmt*> diagnosePointerAccess(const Stmt* PointerAccessExpr,
+                                                  const Expr* PointerExpr,
+                                                  const Environment& Env) {
+  auto [PointerKnown, PointerNotNull] = getPointerNullState(PointerExpr, Env);
+  auto& PointerNotKnownNull =
+      Env.makeNot(Env.makeAnd(PointerKnown, Env.makeNot(PointerNotNull)));
+  if (!Env.flowConditionImplies(PointerNotKnownNull)) {
+    return PointerAccessExpr;
+  }
+  return llvm::None;
+}
+
+llvm::Optional<const Stmt*> diagnoseDereference(const UnaryOperator* UnaryOp,
+                                                const MatchFinder::MatchResult&,
+                                                const Environment& Env) {
+  return diagnosePointerAccess(UnaryOp, UnaryOp->getSubExpr(), Env);
+}
+
+llvm::Optional<const Stmt*> diagnoseArrow(
+    const MemberExpr* MemberExpr, const MatchFinder::MatchResult& Result,
+    const Environment& Env) {
+  return diagnosePointerAccess(MemberExpr, MemberExpr->getBase(), Env);
+}
+
+auto buildDiagnoser() {
+  return dataflow::MatchSwitchBuilder<const Environment,
+                                      llvm::Optional<const Stmt*>>()
+      // (*)
+      .CaseOf<UnaryOperator>(isPointerDereference(), diagnoseDereference)
+      // (->)
+      .CaseOf<MemberExpr>(isPointerArrow(), diagnoseArrow)
+      .Build();
+}
+
+}  // namespace
+
+PointerNullabilityDiagnoser::PointerNullabilityDiagnoser()
+    : Diagnoser(buildDiagnoser()) {}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
\ No newline at end of file
diff --git a/nullability_verification/pointer_nullability_diagnosis.h b/nullability_verification/pointer_nullability_diagnosis.h
new file mode 100644
index 0000000..4d92456
--- /dev/null
+++ b/nullability_verification/pointer_nullability_diagnosis.h
@@ -0,0 +1,48 @@
+// 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
+
+#ifndef CRUBIT_NULLABILITY_VERIFICATION_POINTER_NULLABILITY_DIAGNOSIS_H_
+#define CRUBIT_NULLABILITY_VERIFICATION_POINTER_NULLABILITY_DIAGNOSIS_H_
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Stmt.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/MatchSwitch.h"
+#include "llvm/ADT/Optional.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+/// Checks that nullable pointers are used safely, using nullability information
+/// that is collected by `PointerNullabilityAnalysis`.
+///
+/// Examples of null safety violations include dereferencing nullable pointers
+/// without null checks, and assignments between pointers of incompatible
+/// nullability.
+class PointerNullabilityDiagnoser {
+ public:
+  PointerNullabilityDiagnoser();
+
+  /// Returns the pointer to the statement if null safety is violated, otherwise
+  /// the optional is empty.
+  ///
+  /// TODO(b/233582219): Extend diagnosis to return more information, e.g. the
+  /// type of violation.
+  llvm::Optional<const Stmt*> diagnose(const Stmt* Stmt, ASTContext& Ctx,
+                                       const dataflow::Environment& Env) {
+    return Diagnoser(*Stmt, Ctx, Env);
+  }
+
+ private:
+  dataflow::MatchSwitch<const dataflow::Environment,
+                        llvm::Optional<const Stmt*>>
+      Diagnoser;
+};
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // CRUBIT_NULLABILITY_VERIFICATION_POINTER_NULLABILITY_DIAGNOSIS_H_
diff --git a/nullability_verification/pointer_nullability_matchers.cc b/nullability_verification/pointer_nullability_matchers.cc
index e1c95d5..300547a 100644
--- a/nullability_verification/pointer_nullability_matchers.cc
+++ b/nullability_verification/pointer_nullability_matchers.cc
@@ -48,9 +48,10 @@
 Matcher<Stmt> isImplicitCastPointerToBool() {
   return implicitCastExpr(hasCastKind(CK_PointerToBoolean));
 }
-Matcher<Stmt> isMemberExprInvolvingPointers() {
-  return memberExpr(anyOf(isArrow(), hasType(isAnyPointer())));
+Matcher<Stmt> isMemberOfPointerType() {
+  return memberExpr(hasType(isAnyPointer()));
 }
+Matcher<Stmt> isPointerArrow() { return memberExpr(isArrow()); }
 Matcher<Stmt> isCXXThisExpr() { return cxxThisExpr(); }
 }  // namespace nullability
 }  // namespace tidy
diff --git a/nullability_verification/pointer_nullability_matchers.h b/nullability_verification/pointer_nullability_matchers.h
index 23b42b8..c5562ae 100644
--- a/nullability_verification/pointer_nullability_matchers.h
+++ b/nullability_verification/pointer_nullability_matchers.h
@@ -12,7 +12,8 @@
 namespace nullability {
 
 ast_matchers::internal::Matcher<Stmt> isPointerVariableReference();
-ast_matchers::internal::Matcher<Stmt> isMemberExprInvolvingPointers();
+ast_matchers::internal::Matcher<Stmt> isMemberOfPointerType();
+ast_matchers::internal::Matcher<Stmt> isPointerArrow();
 ast_matchers::internal::Matcher<Stmt> isCXXThisExpr();
 ast_matchers::internal::Matcher<Stmt> isNullPointerLiteral();
 ast_matchers::internal::Matcher<Stmt> isAddrOf();
diff --git a/nullability_verification/pointer_nullability_verification_test.cc b/nullability_verification/pointer_nullability_verification_test.cc
new file mode 100644
index 0000000..0ae5cc4
--- /dev/null
+++ b/nullability_verification/pointer_nullability_verification_test.cc
@@ -0,0 +1,458 @@
+// 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 <string>
+
+#include "nullability_verification/pointer_nullability_analysis.h"
+#include "nullability_verification/pointer_nullability_diagnosis.h"
+#include "clang/Basic/SourceManager.h"
+#include "third_party/llvm/llvm-project/clang/unittests/Analysis/FlowSensitive/TestingSupport.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseSet.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Testing/Support/Error.h"
+#include "third_party/llvm/llvm-project/llvm/utils/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+using dataflow::Environment;
+using dataflow::TypeErasedDataflowAnalysisState;
+using dataflow::test::AnalysisData;
+using dataflow::test::checkDataflow;
+using ::testing::ContainerEq;
+using ::testing::Test;
+
+void checkDiagnostics(llvm::StringRef SourceCode) {
+  std::vector<const Stmt *> Diagnostics;
+  PointerNullabilityDiagnoser Diagnoser;
+  ASSERT_THAT_ERROR(
+      checkDataflow<PointerNullabilityAnalysis>(
+          SourceCode, ast_matchers::hasName("target"),
+          [](ASTContext &ASTCtx, Environment &) {
+            return PointerNullabilityAnalysis(ASTCtx);
+          },
+          [&Diagnostics, &Diagnoser](
+              ASTContext &Ctx, const Stmt *Stmt,
+              const TypeErasedDataflowAnalysisState &State) {
+            auto StmtDiagnostics = Diagnoser.diagnose(Stmt, Ctx, State.Env);
+            if (StmtDiagnostics.has_value()) {
+              Diagnostics.push_back(StmtDiagnostics.value());
+            }
+          },
+          [&Diagnostics](AnalysisData AnalysisData) {
+            llvm::DenseSet<unsigned> ExpectedLines, ActualLines;
+            auto &SrcMgr = AnalysisData.ASTCtx.getSourceManager();
+            for (auto [Stmt, _] : AnalysisData.Annotations) {
+              ExpectedLines.insert(
+                  SrcMgr.getPresumedLineNumber(Stmt->getBeginLoc()));
+            }
+            for (auto *Stmt : Diagnostics) {
+              ActualLines.insert(
+                  SrcMgr.getPresumedLineNumber(Stmt->getBeginLoc()));
+            }
+            EXPECT_THAT(ActualLines, ContainerEq(ExpectedLines));
+          },
+          {"-fsyntax-only", "-std=c++17", "-Wno-unused-value"}),
+      llvm::Succeeded());
+}
+
+TEST(PointerNullabilityTest, NoPointerOperations) {
+  checkDiagnostics(R"(
+    void target() {
+      1 + 2;
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, DereferenceWithoutACheck) {
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, InitializedWithNullPtrLiteral) {
+  checkDiagnostics(R"(
+    void target() {
+      int *null = nullptr;
+      *null; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target() {
+      int *null = 0;
+      *null; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, InitializedWithAddressOf) {
+  checkDiagnostics(R"(
+    void target(int x) {
+      int *nonNull = &x;
+      *nonNull;
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, InitializedWithOtherPointer) {
+  checkDiagnostics(R"(
+    void target(int x) {
+      int *nonNull = &x;
+      int *nonNullCopy = nonNull;
+      *nonNullCopy;
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* nullable) {
+      int *nullableCopy = nullable;
+      *nullableCopy; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* nullable) {
+      int *nullableCopy = nullable;
+      if (nullable) {
+        *nullableCopy;
+      } else {
+        *nullableCopy; // [[unsafe]]
+      }
+      *nullableCopy; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* nullable) {
+      int *nullableCopy = nullable;
+      if (nullableCopy) {
+        *nullable;
+      } else {
+        *nullable; // [[unsafe]]
+      }
+      *nullable; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, CheckByComparisonToNullPtr) {
+  checkDiagnostics(R"(
+    void target(int *maybeNull) {
+      if (maybeNull != nullptr) {
+        *maybeNull;
+      } else {
+        *maybeNull; // [[unsafe]]
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int *maybeNull) {
+      if (nullptr != maybeNull) {
+        *maybeNull;
+      } else {
+        *maybeNull; // [[unsafe]]
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      if (maybeNull == nullptr) {
+        *maybeNull; // [[unsafe]]
+      } else {
+        *maybeNull;
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      if (nullptr == maybeNull) {
+        *maybeNull; // [[unsafe]]
+      } else {
+        *maybeNull;
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, CheckByImplicitCastToBool) {
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      if (maybeNull) {
+        *maybeNull;
+      } else {
+        *maybeNull; // [[unsafe]]
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      if (!maybeNull) {
+        *maybeNull; // [[unsafe]]
+      } else {
+        *maybeNull;
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, CheckByComparisonToOtherNullPtr) {
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      int *null = nullptr;
+      if (maybeNull != null) {
+        *maybeNull;
+      } else {
+        *maybeNull; // [[unsafe]]
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* maybeNull) {
+      int *null = nullptr;
+      if (maybeNull == null) {
+        *maybeNull; // [[unsafe]]
+      } else {
+        *maybeNull;
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, CheckByComparisonToOtherNonNullPtr) {
+  checkDiagnostics(R"(
+    void target(int* maybeNull, int x) {
+      int* nonNull = &x;
+      if (maybeNull != nonNull) {
+        *maybeNull; // [[unsafe]]
+      } else {
+        *maybeNull;
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* maybeNull, int x) {
+      int* nonNull = &x;
+      if (maybeNull == nonNull) {
+        *maybeNull;
+      } else {
+        *maybeNull; // [[unsafe]]
+      }
+      *maybeNull; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, CheckByComparisonToOtherUnknownPtr) {
+  checkDiagnostics(R"(
+    void target(int* x, int* y) {
+      if (x != y) {
+        *x; // [[unsafe]]
+      } else {
+        *x; // [[unsafe]]
+      }
+      *x; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* x, int* y) {
+      if (x == y) {
+        *x; // [[unsafe]]
+      } else {
+        *x; // [[unsafe]]
+      }
+      *x; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, BinaryExpressions) {
+  checkDiagnostics(R"(
+    void target(int* x, int* y) {
+      if (x && y) {
+        *x;
+      } else {
+        *x; // [[unsafe]]
+      }
+      *x; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* x, int* y) {
+      if (x || y) {
+        *x; // [[unsafe]]
+      } else {
+        *x; // [[unsafe]]
+      }
+      *x; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* x, int* y) {
+      if (!x && !y) {
+        *x; // [[unsafe]]
+      } else {
+        *x; // [[unsafe]]
+      }
+      *x; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    void target(int* x, int* y) {
+      if (!x || !y) {
+        *x; // [[unsafe]]
+      } else {
+        *x;
+      }
+      *x; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, MemberPointers) {
+  checkDiagnostics(R"(
+    struct Foo {
+      Foo* ptr;
+    };
+    void target(Foo foo) {
+      if (foo.ptr) {
+        *foo.ptr;
+      } else {
+        *foo.ptr; // [[unsafe]]
+      }
+      *foo.ptr; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    struct Foo {
+      Foo* ptr;
+      void target() {
+        if (ptr) {
+          *ptr;
+        } else {
+          *ptr; // [[unsafe]]
+        }
+        *ptr; // [[unsafe]]
+      }
+    };
+  )");
+
+  checkDiagnostics(R"(
+    class Foo {
+     public:
+      Foo* ptr;
+    };
+    void target(Foo foo) {
+      if (foo.ptr) {
+        *foo.ptr;
+      } else {
+        *foo.ptr; // [[unsafe]]
+      }
+      *foo.ptr; // [[unsafe]]
+    }
+  )");
+}
+
+TEST(PointerNullabilityTest, MemberAccessOnPointer) {
+  checkDiagnostics(R"(
+    struct Foo {
+      void foo();
+    };
+    void target(Foo* foo) {
+      if (foo) {
+        foo->foo();
+      } else {
+        foo->foo(); // [[unsafe]]
+      }
+      foo->foo(); // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    struct Foo {
+      void foo();
+      void target() {
+        foo();
+      }
+    };
+  )");
+
+  checkDiagnostics(R"(
+    struct Foo {
+      void foo();
+      void target() {
+        this->foo();
+      }
+    };
+  )");
+
+  checkDiagnostics(R"(
+    struct Foo {
+      void foo();
+      void target() {
+        Foo *thisCopy = this;
+        thisCopy->foo();
+      }
+    };
+  )");
+
+  checkDiagnostics(R"(
+    struct Foo {
+      Foo* foo;
+    };
+    void target(Foo* foo) {
+      if (foo) {
+        foo->foo->foo; // [[unsafe]]
+      } else {
+        foo->foo->foo; // [[unsafe]]
+      }
+      foo->foo->foo; // [[unsafe]]
+    }
+  )");
+
+  checkDiagnostics(R"(
+    struct Foo {
+      Foo* foo;
+    };
+    void target(Foo* foo) {
+      if (foo && foo->foo) {
+        foo->foo->foo;
+      } else {
+        foo->foo; // [[unsafe]]
+      }
+      foo->foo; // [[unsafe]]
+    }
+  )");
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang