Rename /nullability_verification to /nullability in preparation for adding/relocating inference functionality here.

PiperOrigin-RevId: 528495113
diff --git a/nullability/BUILD b/nullability/BUILD
new file mode 100644
index 0000000..7272c47
--- /dev/null
+++ b/nullability/BUILD
@@ -0,0 +1,88 @@
+# Verification and Inference for null safety
+
+package(default_applicable_licenses = ["//:license"])
+
+cc_library(
+    name = "pointer_nullability_lattice",
+    hdrs = ["pointer_nullability_lattice.h"],
+    deps = [
+        "@absl//absl/container:flat_hash_map",
+        "@absl//absl/log:check",
+        "@llvm-project//clang:analysis",
+    ],
+)
+
+cc_library(
+    name = "pointer_nullability_matchers",
+    srcs = ["pointer_nullability_matchers.cc"],
+    hdrs = ["pointer_nullability_matchers.h"],
+    deps = [
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+    ],
+)
+
+cc_library(
+    name = "pointer_nullability_analysis",
+    srcs = ["pointer_nullability_analysis.cc"],
+    hdrs = ["pointer_nullability_analysis.h"],
+    visibility = ["//nullability/test:__pkg__"],
+    deps = [
+        ":pointer_nullability",
+        ":pointer_nullability_lattice",
+        ":pointer_nullability_matchers",
+        "@absl//absl/log:check",
+        "@absl//absl/strings",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+        "@llvm-project//clang:basic",
+    ],
+)
+
+cc_library(
+    name = "pointer_nullability_diagnosis",
+    srcs = ["pointer_nullability_diagnosis.cc"],
+    hdrs = ["pointer_nullability_diagnosis.h"],
+    visibility = ["//nullability/test:__pkg__"],
+    deps = [
+        ":pointer_nullability",
+        ":pointer_nullability_lattice",
+        ":pointer_nullability_matchers",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+        "@llvm-project//clang:basic",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "pointer_nullability",
+    srcs = [
+        "pointer_nullability.cc",
+        "type_nullability.cc",
+    ],
+    hdrs = ["pointer_nullability.h"],
+    deps = [
+        ":pointer_nullability_lattice",
+        "@absl//absl/log:check",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:basic",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "pointer_nullability_test",
+    srcs = ["pointer_nullability_test.cc"],
+    deps = [
+        ":pointer_nullability",
+        "@llvm-project//clang:testing",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//third-party/unittest:gmock",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
diff --git a/nullability/formal_methods/gradual_nullability_code_synthesis.smt2 b/nullability/formal_methods/gradual_nullability_code_synthesis.smt2
new file mode 100644
index 0000000..0c2544b
--- /dev/null
+++ b/nullability/formal_methods/gradual_nullability_code_synthesis.smt2
@@ -0,0 +1,1578 @@
+;; 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
+
+;; Run: cvc5 --lang smt --incremental --fmf-bound gradual_nullability_code_synthesis.smt2
+
+(set-option :produce-models true)
+(set-option :produce-assertions true)
+(set-option :produce-assignments true)
+(set-option :produce-unsat-cores true)
+(set-logic HO_ALL)
+
+(declare-datatype FlowConditions
+  ((make-FlowConditions
+    (_get-fc-1 Bool)
+    (_get-fc-2 Bool)
+    (_get-fc-3 Bool)
+    (_get-fc-4 Bool)
+    (_get-fc-5 Bool)
+    (_get-fc-6 Bool))))
+
+;; Redefine selectors as regular functions, because predefined ones
+;; can't be converted to values of function type for some reason.
+(define-fun get-fc-1 ((fcs FlowConditions)) Bool
+  (_get-fc-1 fcs))
+
+(define-fun get-fc-2 ((fcs FlowConditions)) Bool
+  (_get-fc-2 fcs))
+
+(define-fun get-fc-3 ((fcs FlowConditions)) Bool
+  (_get-fc-3 fcs))
+
+(define-fun get-fc-4 ((fcs FlowConditions)) Bool
+  (_get-fc-4 fcs))
+
+(define-fun get-fc-5 ((fcs FlowConditions)) Bool
+  (_get-fc-5 fcs))
+
+(define-fun get-fc-6 ((fcs FlowConditions)) Bool
+  (_get-fc-6 fcs))
+
+(define-fun join-fc ((c Bool) (fc-then Bool) (fc-else Bool)) Bool
+  (or (and c fc-then)
+      (and (not c) fc-else)))
+
+(assert
+  (forall ((c Bool) (fc-then Bool) (fc-else Bool))
+    (= (join-fc c fc-then fc-else)
+       (join-fc (not c) fc-else fc-then))))
+
+(declare-datatype PointerValue
+  ((make-PointerValue
+    (get-x0 Bool)
+    (get-x1 Bool)
+)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Solution: unconstrained declarations.
+
+;; Flow condition conjuncts that constrain a pointer according to its
+;; annotation.
+(declare-fun fc-conj--ptr-is-null (PointerValue) Bool)
+(declare-fun fc-conj--ptr-is-unknown (PointerValue) Bool)
+(declare-fun fc-conj--ptr-is-nonnull (PointerValue) Bool)
+(declare-fun fc-conj--ptr-is-nullable (PointerValue) Bool)
+
+;; Flow condition conjunct that constrains the result of comparing two
+;; pointers for equality.
+;;
+;; Args: lhs, rhs, are-equal.
+(declare-fun fc-conj--ptrs-were-compared (PointerValue PointerValue Bool) Bool)
+
+;; Flow condition conjunct that constrains the new joined pointer,
+;; by combining the pointers coming from the "then" and the "else" branches
+;; of an "if" statement.
+;;
+;; Args: condition, ptr-then, ptr-else, ptr-joined.
+(declare-fun fc-conj--join-ptr (Bool PointerValue PointerValue PointerValue) Bool)
+
+;; Safety criteria, tells us whether it is safe to dereference a given
+;; pointer at a given program point. The program point is identified
+;; by its flow condition.
+;;
+;; Args: flow-condition, pointer.
+(declare-fun is-unsafe-to-deref (Bool PointerValue) Bool)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Solution S1: two bits, no quantifiers
+;; - Bit 1 represents if the pointer's nullability is known
+;; - Bit 2 represents if the pointer is not null
+
+(define-fun enable-solution-s1 () Bool false)
+
+(assert (=> enable-solution-s1
+  (forall ((p PointerValue))
+    (= (fc-conj--ptr-is-null p)
+       (= p (make-PointerValue true false))))))
+
+(assert (=> enable-solution-s1
+  (forall ((p PointerValue))
+    (= (fc-conj--ptr-is-unknown p)
+        (or
+          (= p (make-PointerValue false false))
+          (= p (make-PointerValue false true)))))))
+
+(assert (=> enable-solution-s1
+  (forall ((p PointerValue))
+    (= (fc-conj--ptr-is-nonnull p)
+       (= p (make-PointerValue true true))))))
+
+(assert (=> enable-solution-s1
+  (forall ((p PointerValue))
+    (= (fc-conj--ptr-is-nullable p)
+        (or
+          (= p (make-PointerValue true false))
+          (= p (make-PointerValue true true)))))))
+
+(assert (=> enable-solution-s1
+  (forall ((lhs PointerValue) (rhs PointerValue) (eq Bool))
+    (= (fc-conj--ptrs-were-compared lhs rhs eq)
+       (and
+         ;; nullptr == nullptr
+         (=> (and (fc-conj--ptr-is-null lhs) (fc-conj--ptr-is-null rhs))
+             eq)
+
+         ;; nullptr != nonnull
+         (=> (and (fc-conj--ptr-is-null lhs) (fc-conj--ptr-is-nonnull rhs))
+             (not eq))
+
+         ;; nonnull != nullptr
+         (=> (and (fc-conj--ptr-is-nonnull lhs) (fc-conj--ptr-is-null rhs))
+             (not eq)))))))
+
+(assert (=> enable-solution-s1
+  (forall ((c Bool)
+           (ptr-then PointerValue)
+           (ptr-else PointerValue)
+           (ptr-joined PointerValue))
+    (= (fc-conj--join-ptr c ptr-then ptr-else ptr-joined)
+       (or (and c (= ptr-joined ptr-then))
+           (and (not c) (= ptr-joined ptr-else)))))))
+
+(assert (=> enable-solution-s1
+  (forall ((fc Bool) (p PointerValue))
+    (= (is-unsafe-to-deref fc p)
+       (and fc (fc-conj--ptr-is-null p))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Solution S2: ??? bits, with quantifiers.
+;; TODO: Implement.
+
+(define-fun enable-solution-s2 () Bool false)
+
+;(assert (=> enable-solution-s2
+;  (forall ((p PointerValue))
+;    (= (fc-conj--ptr-is-null p)
+;       TODO))))
+
+;(assert (=> enable-solution-s2
+;  (forall ((p PointerValue))
+;    (= (fc-conj--ptr-is-unknown p)
+;       TODO))))
+
+;(assert (=> enable-solution-s2
+;  (forall ((p PointerValue))
+;    (= (fc-conj--ptr-is-nonnull p)
+;       TODO))))
+
+;(assert (=> enable-solution-s2
+;  (forall ((p PointerValue))
+;    (= (fc-conj--ptr-is-nullable p)
+;        TODO))))
+
+;(assert (=> enable-solution-s2
+;  (forall ((lhs PointerValue) (rhs PointerValue) (eq Bool))
+;    (= (fc-conj--ptrs-were-compared lhs rhs eq)
+;       TODO))))
+
+;(assert (=> enable-solution-s2
+;  (forall ((c Bool)
+;           (ptr-then PointerValue)
+;           (ptr-else PointerValue)
+;           (ptr-joined PointerValue))
+;    (= (fc-conj--join-ptr c ptr-then ptr-else ptr-joined)
+;       TODO))))
+
+;(assert (=> enable-solution-s2
+;  (forall ((fc Bool) (p PointerValue))
+;    (= (is-unsafe-to-deref fc p)
+;       TODO))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Check the solution.
+
+(echo "Checking satisfiability of the selected solution.")
+(check-sat)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Properties that the solution must satisfy.
+
+(define-fun is-valid-pointer ((p PointerValue)) Bool
+  (or (fc-conj--ptr-is-null p)
+      (fc-conj--ptr-is-unknown p)
+      (fc-conj--ptr-is-nonnull p)
+      (fc-conj--ptr-is-nullable p)))
+
+;; Reflexivity for fc-conj--ptrs-were-compared.
+(assert
+  (forall ((p PointerValue))
+    (=> (is-valid-pointer p)
+        (fc-conj--ptrs-were-compared p p true))))
+
+;; Symmetry for fc-conj--ptrs-were-compared.
+(assert
+  (forall ((p1 PointerValue) (p2 PointerValue) (are-equal Bool))
+    (=> (and (is-valid-pointer p1) (is-valid-pointer p2))
+        (= (fc-conj--ptrs-were-compared p1 p2 are-equal)
+           (fc-conj--ptrs-were-compared p2 p1 are-equal)))))
+
+;; Transitivity for fc-conj--ptrs-were-compared.
+(assert
+  (=>
+    ;; Transitivity does not hold in S1.
+    ;; Consider (nonnull == unknown), (unknown == null).
+    ;; However (nonnull == null) never holds.
+    ;; ```
+    ;; void target(int * _NonNull x, int *y) {
+    ;;   if (x == y && y == nullptr) {
+    ;;     // dead code that we can't detect
+    ;;   }
+    ;; }
+    ;; ```
+    ;; TODO: How big of a problem is it?
+    (not enable-solution-s1)
+    (forall ((p1 PointerValue) (p2 PointerValue) (p3 PointerValue))
+      (=> (and (is-valid-pointer p1)
+               (is-valid-pointer p2)
+               (is-valid-pointer p3)
+               (fc-conj--ptrs-were-compared p1 p2 true)
+               (fc-conj--ptrs-were-compared p2 p3 true))
+          (fc-conj--ptrs-were-compared p1 p3 true)))))
+
+;; Symmetry for fc-conj--join-ptr with regards to pointers.
+(assert
+  (forall ((b Bool) (p1 PointerValue) (p2 PointerValue) (p3 PointerValue))
+    (= (fc-conj--join-ptr b p1 p2 p3)
+       (fc-conj--join-ptr (not b) p2 p1 p3))))
+
+(echo "Checking whether the selected solution satisfies the properties.")
+(check-sat)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example DerefAllUnchecked
+;;
+;; ```
+;; void target(
+;;     int *ptr_unknown,
+;;     int * _NonNull ptr_nonnull,
+;;     int * _Nullable ptr_nullable) {
+;;   int *ptr_nullptr = nullptr;
+;;   // (1)
+;;   *ptr_unknown; // safe
+;;   *ptr_nonnull; // safe
+;;   *ptr_nullable; // unsafe
+;;   *ptr_nullptr; // unsafe
+;; }
+;; ```
+
+(declare-datatype State-DerefAllUnchecked
+  ((make-State-DerefAllUnchecked
+    (_get-ptr_unknown PointerValue)
+    (_get-ptr_nonnull PointerValue)
+    (_get-ptr_nullable PointerValue)
+    (_get-ptr_nullptr PointerValue))))
+
+(define-fun get-ptr_unknown-DerefAllUnchecked
+  ((state State-DerefAllUnchecked)) PointerValue
+  (_get-ptr_unknown state))
+
+(define-fun get-ptr_nonnull-DerefAllUnchecked
+  ((state State-DerefAllUnchecked)) PointerValue
+  (_get-ptr_nonnull state))
+
+(define-fun get-ptr_nullable-DerefAllUnchecked
+  ((state State-DerefAllUnchecked)) PointerValue
+  (_get-ptr_nullable state))
+
+(define-fun get-ptr_nullptr-DerefAllUnchecked
+  ((state State-DerefAllUnchecked)) PointerValue
+  (_get-ptr_nullptr state))
+
+(define-fun run-DerefAllUnchecked
+  ((state State-DerefAllUnchecked))
+  FlowConditions
+  (match state
+    (((make-State-DerefAllUnchecked
+           ptr_unknown ptr_nonnull ptr_nullable ptr_nullptr)
+      (let ((fc-1 (and (fc-conj--ptr-is-unknown ptr_unknown)
+                       (fc-conj--ptr-is-nonnull ptr_nonnull)
+                       (fc-conj--ptr-is-nullable ptr_nullable)
+                       (fc-conj--ptr-is-null ptr_nullptr))))
+        (make-FlowConditions fc-1 false false false false false))))))
+
+(define-fun is-reachable-DerefAllUnchecked
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-DerefAllUnchecked))
+    (let ((fcs (run-DerefAllUnchecked state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-DerefAllUnchecked
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-DerefAllUnchecked PointerValue)))
+  Bool
+  (exists ((state State-DerefAllUnchecked))
+    (let ((fcs (run-DerefAllUnchecked state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-DerefAllUnchecked get-fc-1)
+     :named DerefAllUnchecked-reachable-1))
+
+(assert
+  (! (not (is-unsafe-deref-DerefAllUnchecked get-fc-1 get-ptr_unknown-DerefAllUnchecked))
+     :named DerefAllUnchecked-deref-unknown))
+
+(assert
+  (! (not (is-unsafe-deref-DerefAllUnchecked get-fc-1 get-ptr_nonnull-DerefAllUnchecked))
+     :named DerefAllUnchecked-deref-nonnull))
+
+(assert
+  (! (is-unsafe-deref-DerefAllUnchecked get-fc-1 get-ptr_nullable-DerefAllUnchecked)
+     :named DerefAllUnchecked-deref-nullable))
+
+(assert
+  (! (is-unsafe-deref-DerefAllUnchecked get-fc-1 get-ptr_nullptr-DerefAllUnchecked)
+     :named DerefAllUnchecked-deref-nullptr))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example MixedNullableAndNonNull
+;;
+;; ```
+;; void target(int * _Nullable x, bool b) {
+;;   // (1)
+;;   int i;
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (2)
+;;     *x; // unsafe
+;;     x = &i;
+;;     // (3)
+;;     *x; // safe
+;;   }
+;;   // (4)
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (5)
+;;     *x; // safe
+;;   } else {
+;;     // (6)
+;;     *x; // unsafe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-MixedNullableAndNonNull
+  ((make-State-MixedNullableAndNonNull
+    (_get-x-1 PointerValue)
+    (_get-x-3 PointerValue)
+    (_get-x-4 PointerValue)
+    (b Bool))))
+
+(define-fun get-x-1-MixedNullableAndNonNull
+  ((state State-MixedNullableAndNonNull)) PointerValue
+  (_get-x-1 state))
+
+(define-fun get-x-3-MixedNullableAndNonNull
+  ((state State-MixedNullableAndNonNull)) PointerValue
+  (_get-x-3 state))
+
+(define-fun get-x-4-MixedNullableAndNonNull
+  ((state State-MixedNullableAndNonNull)) PointerValue
+  (_get-x-4 state))
+
+(define-fun run-MixedNullableAndNonNull
+  ((state State-MixedNullableAndNonNull))
+  FlowConditions
+  (match state
+    (((make-State-MixedNullableAndNonNull x-1 x-3 x-4 b)
+      (let ((fc-1 (fc-conj--ptr-is-nullable x-1)))
+      (let ((fc-2 (and fc-1 b)))
+      (let ((fc-3 (and fc-2 (fc-conj--ptr-is-nonnull x-3))))
+      (let ((fc-4 (and (join-fc b fc-3 fc-1)
+                       (fc-conj--join-ptr b x-3 x-1 x-4))))
+      (let ((fc-5 (and fc-4 b)))
+      (let ((fc-6 (and fc-4 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 fc-5 fc-6)))))))))))
+
+(define-fun is-reachable-MixedNullableAndNonNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-MixedNullableAndNonNull))
+    (let ((fcs (run-MixedNullableAndNonNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-MixedNullableAndNonNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-MixedNullableAndNonNull PointerValue)))
+  Bool
+  (exists ((state State-MixedNullableAndNonNull))
+    (let ((fcs (run-MixedNullableAndNonNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-MixedNullableAndNonNull get-fc-1)
+     :named MixedNullableAndNonNull-reachable-1))
+
+(assert
+  (! (is-reachable-MixedNullableAndNonNull get-fc-2)
+     :named MixedNullableAndNonNull-reachable-2))
+
+(assert
+  (! (is-reachable-MixedNullableAndNonNull get-fc-3)
+     :named MixedNullableAndNonNull-reachable-3))
+
+(assert
+  (! (is-reachable-MixedNullableAndNonNull get-fc-4)
+     :named MixedNullableAndNonNull-reachable-4))
+
+(assert
+  (! (is-reachable-MixedNullableAndNonNull get-fc-5)
+     :named MixedNullableAndNonNull-reachable-5))
+
+(assert
+  (! (is-reachable-MixedNullableAndNonNull get-fc-6)
+     :named MixedNullableAndNonNull-reachable-6))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndNonNull get-fc-1 get-x-1-MixedNullableAndNonNull)
+     :named MixedNullableAndNonNull-deref-1))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndNonNull get-fc-2 get-x-1-MixedNullableAndNonNull)
+     :named MixedNullableAndNonNull-deref-2))
+
+(assert
+  (! (not (is-unsafe-deref-MixedNullableAndNonNull get-fc-3 get-x-3-MixedNullableAndNonNull))
+     :named MixedNullableAndNonNull-deref-3))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndNonNull get-fc-4 get-x-4-MixedNullableAndNonNull)
+     :named MixedNullableAndNonNull-deref-4))
+
+(assert
+  (! (not (is-unsafe-deref-MixedNullableAndNonNull get-fc-5 get-x-4-MixedNullableAndNonNull))
+     :named MixedNullableAndNonNull-deref-5))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndNonNull get-fc-6 get-x-4-MixedNullableAndNonNull)
+     :named MixedNullableAndNonNull-deref-6))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example MixedNullableAndUnknown
+;;
+;; ```
+;; void target(int * _Nullable x, bool b) {
+;;   // (1)
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (2)
+;;     *x; // unsafe
+;;     x = MakeUnknown();
+;;     // (3)
+;;     *x; // safe
+;;   }
+;;   // (4)
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (5)
+;;     *x; // safe
+;;   } else {
+;;     // (6)
+;;     *x; // unsafe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-MixedNullableAndUnknown
+  ((make-State-MixedNullableAndUnknown
+    (_get-x-1 PointerValue)
+    (_get-x-3 PointerValue)
+    (_get-x-4 PointerValue)
+    (b Bool))))
+
+(define-fun get-x-1-MixedNullableAndUnknown
+  ((state State-MixedNullableAndUnknown)) PointerValue
+  (_get-x-1 state))
+
+(define-fun get-x-3-MixedNullableAndUnknown
+  ((state State-MixedNullableAndUnknown)) PointerValue
+  (_get-x-3 state))
+
+(define-fun get-x-4-MixedNullableAndUnknown
+  ((state State-MixedNullableAndUnknown)) PointerValue
+  (_get-x-4 state))
+
+(define-fun run-MixedNullableAndUnknown
+  ((state State-MixedNullableAndUnknown))
+  FlowConditions
+  (match state
+    (((make-State-MixedNullableAndUnknown x-1 x-3 x-4 b)
+      (let ((fc-1 (fc-conj--ptr-is-nullable x-1)))
+      (let ((fc-2 (and fc-1 b)))
+      (let ((fc-3 (and fc-2 (fc-conj--ptr-is-unknown x-3))))
+      (let ((fc-4 (and (join-fc b fc-3 fc-1)
+                       (fc-conj--join-ptr b x-3 x-1 x-4))))
+      (let ((fc-5 (and fc-4 b)))
+      (let ((fc-6 (and fc-4 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 fc-5 fc-6)))))))))))
+
+(define-fun is-reachable-MixedNullableAndUnknown
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-MixedNullableAndUnknown))
+    (let ((fcs (run-MixedNullableAndUnknown state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-MixedNullableAndUnknown
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-MixedNullableAndUnknown PointerValue)))
+  Bool
+  (exists ((state State-MixedNullableAndUnknown))
+    (let ((fcs (run-MixedNullableAndUnknown state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-MixedNullableAndUnknown get-fc-1)
+     :named MixedNullableAndUnknown-reachable-1))
+
+(assert
+  (! (is-reachable-MixedNullableAndUnknown get-fc-2)
+     :named MixedNullableAndUnknown-reachable-2))
+
+(assert
+  (! (is-reachable-MixedNullableAndUnknown get-fc-3)
+     :named MixedNullableAndUnknown-reachable-3))
+
+(assert
+  (! (is-reachable-MixedNullableAndUnknown get-fc-4)
+     :named MixedNullableAndUnknown-reachable-4))
+
+(assert
+  (! (is-reachable-MixedNullableAndUnknown get-fc-5)
+     :named MixedNullableAndUnknown-reachable-5))
+
+(assert
+  (! (is-reachable-MixedNullableAndUnknown get-fc-6)
+     :named MixedNullableAndUnknown-reachable-6))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndUnknown get-fc-1 get-x-1-MixedNullableAndUnknown)
+     :named MixedNullableAndUnknown-deref-1))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndUnknown get-fc-2 get-x-1-MixedNullableAndUnknown)
+     :named MixedNullableAndUnknown-deref-2))
+
+(assert
+  (! (not (is-unsafe-deref-MixedNullableAndUnknown get-fc-3 get-x-3-MixedNullableAndUnknown))
+     :named MixedNullableAndUnknown-deref-3))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndUnknown get-fc-4 get-x-4-MixedNullableAndUnknown)
+     :named MixedNullableAndUnknown-deref-4))
+
+(assert
+  (! (not (is-unsafe-deref-MixedNullableAndUnknown get-fc-5 get-x-4-MixedNullableAndUnknown))
+     :named MixedNullableAndUnknown-deref-5))
+
+(assert
+  (! (is-unsafe-deref-MixedNullableAndUnknown get-fc-6 get-x-4-MixedNullableAndUnknown)
+     :named MixedNullableAndUnknown-deref-6))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example MixedUnknownAndNull
+;;
+;; ```
+;; void target(int *x, bool b) {
+;;   // (1)
+;;   *x; // safe
+;;   if (b) {
+;;     // (2)
+;;     *x; // safe
+;;     x = nullptr;
+;;     // (3)
+;;     *x; // unsafe
+;;   }
+;;   // (4)
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (5)
+;;     *x; // unsafe
+;;   } else {
+;;     // (6)
+;;     *x; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-MixedUnknownAndNull
+  ((make-State-MixedUnknownAndNull
+    (_get-x-1 PointerValue)
+    (_get-x-3 PointerValue)
+    (_get-x-4 PointerValue)
+    (b Bool))))
+
+(define-fun get-x-1-MixedUnknownAndNull
+  ((state State-MixedUnknownAndNull)) PointerValue
+  (_get-x-1 state))
+
+(define-fun get-x-3-MixedUnknownAndNull
+  ((state State-MixedUnknownAndNull)) PointerValue
+  (_get-x-3 state))
+
+(define-fun get-x-4-MixedUnknownAndNull
+  ((state State-MixedUnknownAndNull)) PointerValue
+  (_get-x-4 state))
+
+(define-fun run-MixedUnknownAndNull
+  ((state State-MixedUnknownAndNull))
+  FlowConditions
+  (match state
+    (((make-State-MixedUnknownAndNull x-1 x-3 x-4 b)
+      (let ((fc-1 (fc-conj--ptr-is-unknown x-1)))
+      (let ((fc-2 (and fc-1 b)))
+      (let ((fc-3 (and fc-2 (fc-conj--ptr-is-null x-3))))
+      (let ((fc-4 (and (join-fc b fc-3 fc-1)
+                       (fc-conj--join-ptr b x-3 x-1 x-4))))
+      (let ((fc-5 (and fc-4 b)))
+      (let ((fc-6 (and fc-4 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 fc-5 fc-6)))))))))))
+
+(define-fun is-reachable-MixedUnknownAndNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-MixedUnknownAndNull))
+    (let ((fcs (run-MixedUnknownAndNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-MixedUnknownAndNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-MixedUnknownAndNull PointerValue)))
+  Bool
+  (exists ((state State-MixedUnknownAndNull))
+    (let ((fcs (run-MixedUnknownAndNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNull get-fc-1)
+     :named MixedUnknownAndNull-reachable-1))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNull get-fc-2)
+     :named MixedUnknownAndNull-reachable-2))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNull get-fc-3)
+     :named MixedUnknownAndNull-reachable-3))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNull get-fc-4)
+     :named MixedUnknownAndNull-reachable-4))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNull get-fc-5)
+     :named MixedUnknownAndNull-reachable-5))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNull get-fc-6)
+     :named MixedUnknownAndNull-reachable-6))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNull get-fc-1 get-x-1-MixedUnknownAndNull))
+     :named MixedUnknownAndNull-deref-1))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNull get-fc-2 get-x-1-MixedUnknownAndNull))
+     :named MixedUnknownAndNull-deref-2))
+
+(assert
+  (! (is-unsafe-deref-MixedUnknownAndNull get-fc-3 get-x-3-MixedUnknownAndNull)
+     :named MixedUnknownAndNull-deref-3))
+
+(assert
+  (! (is-unsafe-deref-MixedUnknownAndNull get-fc-4 get-x-4-MixedUnknownAndNull)
+     :named MixedUnknownAndNull-deref-4))
+
+(assert
+  (! (is-unsafe-deref-MixedUnknownAndNull get-fc-5 get-x-4-MixedUnknownAndNull)
+     :named MixedUnknownAndNull-deref-5))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNull get-fc-6 get-x-4-MixedUnknownAndNull))
+     :named MixedUnknownAndNull-deref-6))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example MixedUnknownAndNonNull
+;;
+;; ```
+;; void target(int *x, bool b) {
+;;   // (1)
+;;   *x; // safe
+;;   if (b) {
+;;     // (2)
+;;     *x; // safe
+;;     x = MakeNonNull();
+;;     // (3)
+;;     *x; // safe
+;;   }
+;;   // (4)
+;;   *x; // safe
+;;   if (b) {
+;;     // (5)
+;;     *x; // safe
+;;   } else {
+;;     // (6)
+;;     *x; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-MixedUnknownAndNonNull
+  ((make-State-MixedUnknownAndNonNull
+    (_get-x-1 PointerValue)
+    (_get-x-3 PointerValue)
+    (_get-x-4 PointerValue)
+    (b Bool))))
+
+(define-fun get-x-1-MixedUnknownAndNonNull
+  ((state State-MixedUnknownAndNonNull)) PointerValue
+  (_get-x-1 state))
+
+(define-fun get-x-3-MixedUnknownAndNonNull
+  ((state State-MixedUnknownAndNonNull)) PointerValue
+  (_get-x-3 state))
+
+(define-fun get-x-4-MixedUnknownAndNonNull
+  ((state State-MixedUnknownAndNonNull)) PointerValue
+  (_get-x-4 state))
+
+(define-fun run-MixedUnknownAndNonNull
+  ((state State-MixedUnknownAndNonNull))
+  FlowConditions
+  (match state
+    (((make-State-MixedUnknownAndNonNull x-1 x-3 x-4 b)
+      (let ((fc-1 (fc-conj--ptr-is-unknown x-1)))
+      (let ((fc-2 (and fc-1 b)))
+      (let ((fc-3 (and fc-2 (fc-conj--ptr-is-nonnull x-3))))
+      (let ((fc-4 (and (join-fc b fc-3 fc-1)
+                       (fc-conj--join-ptr b x-3 x-1 x-4))))
+      (let ((fc-5 (and fc-4 b)))
+      (let ((fc-6 (and fc-4 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 fc-5 fc-6)))))))))))
+
+(define-fun is-reachable-MixedUnknownAndNonNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-MixedUnknownAndNonNull))
+    (let ((fcs (run-MixedUnknownAndNonNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-MixedUnknownAndNonNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-MixedUnknownAndNonNull PointerValue)))
+  Bool
+  (exists ((state State-MixedUnknownAndNonNull))
+    (let ((fcs (run-MixedUnknownAndNonNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNonNull get-fc-1)
+     :named MixedUnknownAndNonNull-reachable-1))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNonNull get-fc-2)
+     :named MixedUnknownAndNonNull-reachable-2))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNonNull get-fc-3)
+     :named MixedUnknownAndNonNull-reachable-3))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNonNull get-fc-4)
+     :named MixedUnknownAndNonNull-reachable-4))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNonNull get-fc-5)
+     :named MixedUnknownAndNonNull-reachable-5))
+
+(assert
+  (! (is-reachable-MixedUnknownAndNonNull get-fc-6)
+     :named MixedUnknownAndNonNull-reachable-6))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNonNull get-fc-1 get-x-1-MixedUnknownAndNonNull))
+     :named MixedUnknownAndNonNull-deref-1))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNonNull get-fc-2 get-x-1-MixedUnknownAndNonNull))
+     :named MixedUnknownAndNonNull-deref-2))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNonNull get-fc-3 get-x-3-MixedUnknownAndNonNull))
+     :named MixedUnknownAndNonNull-deref-3))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNonNull get-fc-4 get-x-4-MixedUnknownAndNonNull))
+     :named MixedUnknownAndNonNull-deref-4))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNonNull get-fc-5 get-x-4-MixedUnknownAndNonNull))
+     :named MixedUnknownAndNonNull-deref-5))
+
+(assert
+  (! (not (is-unsafe-deref-MixedUnknownAndNonNull get-fc-6 get-x-4-MixedUnknownAndNonNull))
+     :named MixedUnknownAndNonNull-deref-6))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareNullAndNull
+;;
+;; ```
+;; void target() {
+;;   int *x = nullptr;
+;;   int *y = nullptr;
+;;   // (1)
+;;   bool b = x == y;
+;;   // (2)
+;;   if (b) {
+;;     // (3)
+;;     *x; // unsafe
+;;     *y; // unsafe
+;;   } else {
+;;     // (4) - unreachable
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareNullAndNull
+  ((make-State-CompareNullAndNull
+    (_get-x PointerValue)
+    (_get-y PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareNullAndNull
+  ((state State-CompareNullAndNull)) PointerValue
+  (_get-x state))
+
+(define-fun get-y-CompareNullAndNull
+  ((state State-CompareNullAndNull)) PointerValue
+  (_get-y state))
+
+(define-fun run-CompareNullAndNull
+  ((state State-CompareNullAndNull))
+  FlowConditions
+  (match state
+    (((make-State-CompareNullAndNull x y b)
+      (let ((fc-1 (and (fc-conj--ptr-is-null x)
+                       (fc-conj--ptr-is-null y))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x y b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareNullAndNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareNullAndNull))
+    (let ((fcs (run-CompareNullAndNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareNullAndNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareNullAndNull PointerValue)))
+  Bool
+  (exists ((state State-CompareNullAndNull))
+    (let ((fcs (run-CompareNullAndNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-CompareNullAndNull get-fc-1)
+     :named CompareNullAndNull-reachable-1))
+
+(assert
+  (! (is-reachable-CompareNullAndNull get-fc-2)
+     :named CompareNullAndNull-reachable-2))
+
+(assert
+  (! (is-reachable-CompareNullAndNull get-fc-3)
+     :named CompareNullAndNull-reachable-3))
+
+(assert
+  (! (not (is-reachable-CompareNullAndNull get-fc-4))
+     :named CompareNullAndNull-reachable-4))
+
+(assert
+  (! (is-unsafe-deref-CompareNullAndNull get-fc-3 get-x-CompareNullAndNull)
+     :named CompareNullAndNull-deref-3-x))
+
+(assert
+  (! (is-unsafe-deref-CompareNullAndNull get-fc-3 get-y-CompareNullAndNull)
+     :named CompareNullAndNull-deref-3-y))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareUnknownAndUnknown
+;;
+;; ```
+;; void target(int *x, int *y) {
+;;   // (1)
+;;   bool b = x == y;
+;;   // (2)
+;;   if (b) {
+;;     // (3)
+;;     *x; // safe
+;;     *y; // safe
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;     *y; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareUnknownAndUnknown
+  ((make-State-CompareUnknownAndUnknown
+    (_get-x PointerValue)
+    (_get-y PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareUnknownAndUnknown
+  ((state State-CompareUnknownAndUnknown)) PointerValue
+  (_get-x state))
+
+(define-fun get-y-CompareUnknownAndUnknown
+  ((state State-CompareUnknownAndUnknown)) PointerValue
+  (_get-y state))
+
+(define-fun run-CompareUnknownAndUnknown
+  ((state State-CompareUnknownAndUnknown))
+  FlowConditions
+  (match state
+    (((make-State-CompareUnknownAndUnknown x y b)
+      (let ((fc-1 (and (fc-conj--ptr-is-unknown x)
+                       (fc-conj--ptr-is-unknown y))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x y b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareUnknownAndUnknown
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareUnknownAndUnknown))
+    (let ((fcs (run-CompareUnknownAndUnknown state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareUnknownAndUnknown
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareUnknownAndUnknown PointerValue)))
+  Bool
+  (exists ((state State-CompareUnknownAndUnknown))
+    (let ((fcs (run-CompareUnknownAndUnknown state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-CompareUnknownAndUnknown get-fc-1)
+     :named CompareUnknownAndUnknown-reachable-1))
+
+(assert
+  (! (is-reachable-CompareUnknownAndUnknown get-fc-2)
+     :named CompareUnknownAndUnknown-reachable-2))
+
+(assert
+  (! (is-reachable-CompareUnknownAndUnknown get-fc-3)
+     :named CompareUnknownAndUnknown-reachable-3))
+
+(assert
+  (! (is-reachable-CompareUnknownAndUnknown get-fc-4)
+     :named CompareUnknownAndUnknown-reachable-4))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndUnknown get-fc-3 get-x-CompareUnknownAndUnknown))
+     :named CompareUnknownAndUnknown-deref-3-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndUnknown get-fc-3 get-y-CompareUnknownAndUnknown))
+     :named CompareUnknownAndUnknown-deref-3-y))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndUnknown get-fc-4 get-x-CompareUnknownAndUnknown))
+     :named CompareUnknownAndUnknown-deref-4-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndUnknown get-fc-4 get-y-CompareUnknownAndUnknown))
+     :named CompareUnknownAndUnknown-deref-4-y))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareNonNullAndNonNull
+;;
+;; ```
+;; void target(int * _NonNull x, int * _NonNull y) {
+;;   // (1)
+;;   bool b = x == y;
+;;   // (2)
+;;   if (b) {
+;;     // (3)
+;;     *x; // safe
+;;     *y; // safe
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;     *y; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareNonNullAndNonNull
+  ((make-State-CompareNonNullAndNonNull
+    (_get-x PointerValue)
+    (_get-y PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareNonNullAndNonNull
+  ((state State-CompareNonNullAndNonNull)) PointerValue
+  (_get-x state))
+
+(define-fun get-y-CompareNonNullAndNonNull
+  ((state State-CompareNonNullAndNonNull)) PointerValue
+  (_get-y state))
+
+(define-fun run-CompareNonNullAndNonNull
+  ((state State-CompareNonNullAndNonNull))
+  FlowConditions
+  (match state
+    (((make-State-CompareNonNullAndNonNull x y b)
+      (let ((fc-1 (and (fc-conj--ptr-is-nonnull x)
+                       (fc-conj--ptr-is-nonnull y))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x y b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareNonNullAndNonNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareNonNullAndNonNull))
+    (let ((fcs (run-CompareNonNullAndNonNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareNonNullAndNonNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareNonNullAndNonNull PointerValue)))
+  Bool
+  (exists ((state State-CompareNonNullAndNonNull))
+    (let ((fcs (run-CompareNonNullAndNonNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNonNull get-fc-1)
+     :named CompareNonNullAndNonNull-reachable-1))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNonNull get-fc-2)
+     :named CompareNonNullAndNonNull-reachable-2))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNonNull get-fc-3)
+     :named CompareNonNullAndNonNull-reachable-3))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNonNull get-fc-4)
+     :named CompareNonNullAndNonNull-reachable-4))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNonNull get-fc-3 get-x-CompareNonNullAndNonNull))
+     :named CompareNonNullAndNonNull-deref-3-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNonNull get-fc-3 get-y-CompareNonNullAndNonNull))
+     :named CompareNonNullAndNonNull-deref-3-y))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNonNull get-fc-4 get-x-CompareNonNullAndNonNull))
+     :named CompareNonNullAndNonNull-deref-4-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNonNull get-fc-4 get-y-CompareNonNullAndNonNull))
+     :named CompareNonNullAndNonNull-deref-4-y))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareNonNullAndNull
+;;
+;; ```
+;; void target(int * _NonNull x) {
+;;   // (1)
+;;   *x; // safe
+;;   bool b = x == nullptr;
+;;   // (2)
+;;   *x; // safe
+;;   if (b) {
+;;     // (3) - unreachable
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareNonNullAndNull
+  ((make-State-CompareNonNullAndNull
+    (_get-the-nullptr PointerValue)
+    (_get-x PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareNonNullAndNull
+  ((state State-CompareNonNullAndNull)) PointerValue
+  (_get-x state))
+
+(define-fun run-CompareNonNullAndNull
+  ((state State-CompareNonNullAndNull))
+  FlowConditions
+  (match state
+    (((make-State-CompareNonNullAndNull the-nullptr x b)
+      (let ((fc-1 (and (fc-conj--ptr-is-null the-nullptr)
+                       (fc-conj--ptr-is-nonnull x))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x the-nullptr b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareNonNullAndNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareNonNullAndNull))
+    (let ((fcs (run-CompareNonNullAndNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareNonNullAndNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareNonNullAndNull PointerValue)))
+  Bool
+  (exists ((state State-CompareNonNullAndNull))
+    (let ((fcs (run-CompareNonNullAndNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNull get-fc-1)
+     :named CompareNonNullAndNull-reachable-1))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNull get-fc-2)
+     :named CompareNonNullAndNull-reachable-2))
+
+(assert
+  (! (not (is-reachable-CompareNonNullAndNull get-fc-3))
+     :named CompareNonNullAndNull-reachable-3))
+
+(assert
+  (! (is-reachable-CompareNonNullAndNull get-fc-4)
+     :named CompareNonNullAndNull-reachable-4))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNull get-fc-1 get-x-CompareNonNullAndNull))
+     :named CompareNonNullAndNull-deref-1))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNull get-fc-2 get-x-CompareNonNullAndNull))
+     :named CompareNonNullAndNull-deref-2))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNonNullAndNull get-fc-4 get-x-CompareNonNullAndNull))
+     :named CompareNonNullAndNull-deref-4))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareNullableAndNull
+;;
+;; ```
+;; void target(int * _Nullable x) {
+;;   // (1)
+;;   *x; // unsafe
+;;   bool b = x == nullptr;
+;;   // (2)
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (3)
+;;     *x; // unsafe
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareNullableAndNull
+  ((make-State-CompareNullableAndNull
+    (_get-the-nullptr PointerValue)
+    (_get-x PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareNullableAndNull
+  ((state State-CompareNullableAndNull)) PointerValue
+  (_get-x state))
+
+(define-fun run-CompareNullableAndNull
+  ((state State-CompareNullableAndNull))
+  FlowConditions
+  (match state
+    (((make-State-CompareNullableAndNull the-nullptr x b)
+      (let ((fc-1 (and (fc-conj--ptr-is-null the-nullptr)
+                       (fc-conj--ptr-is-nullable x))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x the-nullptr b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareNullableAndNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareNullableAndNull))
+    (let ((fcs (run-CompareNullableAndNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareNullableAndNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareNullableAndNull PointerValue)))
+  Bool
+  (exists ((state State-CompareNullableAndNull))
+    (let ((fcs (run-CompareNullableAndNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-CompareNullableAndNull get-fc-1)
+     :named CompareNullableAndNull-reachable-1))
+
+(assert
+  (! (is-reachable-CompareNullableAndNull get-fc-2)
+     :named CompareNullableAndNull-reachable-2))
+
+(assert
+  (! (is-reachable-CompareNullableAndNull get-fc-3)
+     :named CompareNullableAndNull-reachable-3))
+
+(assert
+  (! (is-reachable-CompareNullableAndNull get-fc-4)
+     :named CompareNullableAndNull-reachable-4))
+
+(assert
+  (! (is-unsafe-deref-CompareNullableAndNull get-fc-1 get-x-CompareNullableAndNull)
+     :named CompareNullableAndNull-deref-1))
+
+(assert
+  (! (is-unsafe-deref-CompareNullableAndNull get-fc-2 get-x-CompareNullableAndNull)
+     :named CompareNullableAndNull-deref-2))
+
+(assert
+  (! (is-unsafe-deref-CompareNullableAndNull get-fc-3 get-x-CompareNullableAndNull)
+     :named CompareNullableAndNull-deref-3))
+
+(assert
+  (! (not (is-unsafe-deref-CompareNullableAndNull get-fc-4 get-x-CompareNullableAndNull))
+     :named CompareNullableAndNull-deref-4))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareUnknownAndNullSimple
+;;
+;; ```
+;; void target(int *x) {
+;;   // (1)
+;;   *x; // safe - false negative
+;;   bool b = x == nullptr;
+;;   // (2)
+;;   *x; // safe - false negative
+;;   if (b) {
+;;     // (3)
+;;     *x; // safe - false negative
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareUnknownAndNullSimple
+  ((make-State-CompareUnknownAndNullSimple
+    (_get-the-nullptr PointerValue)
+    (_get-x PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareUnknownAndNullSimple
+  ((state State-CompareUnknownAndNullSimple)) PointerValue
+  (_get-x state))
+
+(define-fun run-CompareUnknownAndNullSimple
+  ((state State-CompareUnknownAndNullSimple))
+  FlowConditions
+  (match state
+    (((make-State-CompareUnknownAndNullSimple the-nullptr x b)
+      (let ((fc-1 (and (fc-conj--ptr-is-null the-nullptr)
+                       (fc-conj--ptr-is-unknown x))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x the-nullptr b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareUnknownAndNullSimple
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareUnknownAndNullSimple))
+    (let ((fcs (run-CompareUnknownAndNullSimple state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareUnknownAndNullSimple
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareUnknownAndNullSimple PointerValue)))
+  Bool
+  (exists ((state State-CompareUnknownAndNullSimple))
+    (let ((fcs (run-CompareUnknownAndNullSimple state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert (=> enable-solution-s1
+  (! (is-reachable-CompareUnknownAndNullSimple get-fc-1)
+     :named CompareUnknownAndNullSimple-reachable-1)))
+
+(assert (=> enable-solution-s1
+  (! (is-reachable-CompareUnknownAndNullSimple get-fc-2)
+     :named CompareUnknownAndNullSimple-reachable-2)))
+
+(assert (=> enable-solution-s1
+  (! (is-reachable-CompareUnknownAndNullSimple get-fc-3)
+     :named CompareUnknownAndNullSimple-reachable-3)))
+
+(assert (=> enable-solution-s1
+  (! (is-reachable-CompareUnknownAndNullSimple get-fc-4)
+     :named CompareUnknownAndNullSimple-reachable-4)))
+
+(assert (=> enable-solution-s1
+  (! (not (is-unsafe-deref-CompareUnknownAndNullSimple get-fc-1 get-x-CompareUnknownAndNullSimple))
+     :named CompareUnknownAndNullSimple-deref-1)))
+
+(assert (=> enable-solution-s1
+  (! (not (is-unsafe-deref-CompareUnknownAndNullSimple get-fc-2 get-x-CompareUnknownAndNullSimple))
+     :named CompareUnknownAndNullSimple-deref-2)))
+
+(assert (=> enable-solution-s1
+  (! (not (is-unsafe-deref-CompareUnknownAndNullSimple get-fc-3 get-x-CompareUnknownAndNullSimple))
+     :named CompareUnknownAndNullSimple-deref-3)))
+
+(assert (=> enable-solution-s1
+  (! (not (is-unsafe-deref-CompareUnknownAndNullSimple get-fc-4 get-x-CompareUnknownAndNullSimple))
+     :named CompareUnknownAndNullSimple-deref-4)))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareUnknownAndNonNull
+;;
+;; ```
+;; void target(int *x, int * _NonNull y) {
+;;   // (1)
+;;   *x; // safe
+;;   *y; // safe
+;;   bool b = x == y;
+;;   // (2)
+;;   *x; // safe
+;;   *y; // safe
+;;   if (b) {
+;;     // (3)
+;;     *x; // safe
+;;     *y; // safe
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;     *y; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareUnknownAndNonNull
+  ((make-State-CompareUnknownAndNonNull
+    (_get-x PointerValue)
+    (_get-y PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareUnknownAndNonNull
+  ((state State-CompareUnknownAndNonNull)) PointerValue
+  (_get-x state))
+
+(define-fun get-y-CompareUnknownAndNonNull
+  ((state State-CompareUnknownAndNonNull)) PointerValue
+  (_get-y state))
+
+(define-fun run-CompareUnknownAndNonNull
+  ((state State-CompareUnknownAndNonNull))
+  FlowConditions
+  (match state
+    (((make-State-CompareUnknownAndNonNull x y b)
+      (let ((fc-1 (and (fc-conj--ptr-is-unknown x)
+                       (fc-conj--ptr-is-nonnull y))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x y b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareUnknownAndNonNull
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareUnknownAndNonNull))
+    (let ((fcs (run-CompareUnknownAndNonNull state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareUnknownAndNonNull
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareUnknownAndNonNull PointerValue)))
+  Bool
+  (exists ((state State-CompareUnknownAndNonNull))
+    (let ((fcs (run-CompareUnknownAndNonNull state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert
+  (! (is-reachable-CompareUnknownAndNonNull get-fc-1)
+     :named CompareUnknownAndNonNull-reachable-1))
+
+(assert
+  (! (is-reachable-CompareUnknownAndNonNull get-fc-2)
+     :named CompareUnknownAndNonNull-reachable-2))
+
+(assert
+  (! (is-reachable-CompareUnknownAndNonNull get-fc-3)
+     :named CompareUnknownAndNonNull-reachable-3))
+
+(assert
+  (! (is-reachable-CompareUnknownAndNonNull get-fc-4)
+     :named CompareUnknownAndNonNull-reachable-4))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-1 get-x-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-1-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-1 get-y-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-1-y))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-2 get-x-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-2-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-2 get-y-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-2-y))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-3 get-x-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-3-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-3 get-y-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-3-y))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-4 get-x-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-4-x))
+
+(assert
+  (! (not (is-unsafe-deref-CompareUnknownAndNonNull get-fc-4 get-y-CompareUnknownAndNonNull))
+     :named CompareUnknownAndNonNull-deref-4-y))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example CompareUnknownAndNullAdvanced
+;;
+;; ```
+;; void target(int *x) {
+;;   // (1)
+;;   *x; // unsafe
+;;   bool b = x == nullptr;
+;;   // (2)
+;;   *x; // unsafe
+;;   if (b) {
+;;     // (3)
+;;     *x; // unsafe
+;;   } else {
+;;     // (4)
+;;     *x; // safe
+;;   }
+;; }
+;; ```
+
+(declare-datatype State-CompareUnknownAndNullAdvanced
+  ((make-State-CompareUnknownAndNullAdvanced
+    (_get-the-nullptr PointerValue)
+    (_get-x PointerValue)
+    (_get-b Bool))))
+
+(define-fun get-x-CompareUnknownAndNullAdvanced
+  ((state State-CompareUnknownAndNullAdvanced)) PointerValue
+  (_get-x state))
+
+(define-fun run-CompareUnknownAndNullAdvanced
+  ((state State-CompareUnknownAndNullAdvanced))
+  FlowConditions
+  (match state
+    (((make-State-CompareUnknownAndNullAdvanced the-nullptr x b)
+      (let ((fc-1 (and (fc-conj--ptr-is-null the-nullptr)
+                       (fc-conj--ptr-is-unknown x))))
+      (let ((fc-2 (and fc-1 (fc-conj--ptrs-were-compared x the-nullptr b))))
+      (let ((fc-3 (and fc-2 b)))
+      (let ((fc-4 (and fc-2 (not b))))
+        (make-FlowConditions fc-1 fc-2 fc-3 fc-4 false false)))))))))
+
+(define-fun is-reachable-CompareUnknownAndNullAdvanced
+  ((fc-getter (-> FlowConditions Bool)))
+  Bool
+  (exists ((state State-CompareUnknownAndNullAdvanced))
+    (let ((fcs (run-CompareUnknownAndNullAdvanced state)))
+      (fc-getter fcs))))
+
+(define-fun is-unsafe-deref-CompareUnknownAndNullAdvanced
+  ((fc-getter (-> FlowConditions Bool))
+   (ptr-getter (-> State-CompareUnknownAndNullAdvanced PointerValue)))
+  Bool
+  (exists ((state State-CompareUnknownAndNullAdvanced))
+    (let ((fcs (run-CompareUnknownAndNullAdvanced state)))
+      (is-unsafe-to-deref (fc-getter fcs) (ptr-getter state)))))
+
+(assert (=> (not enable-solution-s1)
+  (! (is-reachable-CompareUnknownAndNullAdvanced get-fc-1)
+     :named CompareUnknownAndNullAdvanced-reachable-1)))
+
+(assert (=> (not enable-solution-s1)
+  (! (is-reachable-CompareUnknownAndNullAdvanced get-fc-2)
+     :named CompareUnknownAndNullAdvanced-reachable-2)))
+
+(assert (=> (not enable-solution-s1)
+  (! (is-reachable-CompareUnknownAndNullAdvanced get-fc-3)
+     :named CompareUnknownAndNullAdvanced-reachable-3)))
+
+(assert (=> (not enable-solution-s1)
+  (! (is-reachable-CompareUnknownAndNullAdvanced get-fc-4)
+     :named CompareUnknownAndNullAdvanced-reachable-4)))
+
+(assert (=> (not enable-solution-s1)
+  ;; The dereference at (1) is actually unsafe, but the structure of the
+  ;; dataflow analysis defined in this file, can't detect that. The issue is
+  ;; that the flow condition at (1) does not have information about what happens
+  ;; later.
+  (! (not (is-unsafe-deref-CompareUnknownAndNullAdvanced get-fc-1 get-x-CompareUnknownAndNullAdvanced))
+     :named CompareUnknownAndNullAdvanced-deref-1)))
+
+(assert (=> (not enable-solution-s1)
+  (! (is-unsafe-deref-CompareUnknownAndNullAdvanced get-fc-2 get-x-CompareUnknownAndNullAdvanced)
+     :named CompareUnknownAndNullAdvanced-deref-2)))
+
+(assert (=> (not enable-solution-s1)
+  (! (is-unsafe-deref-CompareUnknownAndNullAdvanced get-fc-3 get-x-CompareUnknownAndNullAdvanced)
+     :named CompareUnknownAndNullAdvanced-deref-3)))
+
+(assert (=> (not enable-solution-s1)
+  (! (not (is-unsafe-deref-CompareUnknownAndNullAdvanced get-fc-4 get-x-CompareUnknownAndNullAdvanced))
+     :named CompareUnknownAndNullAdvanced-deref-4)))
+
+(echo "Verifying the selected solution against test cases.")
+(check-sat)
+(get-unsat-core)
+(get-value (fc-conj--ptr-is-null))
+(get-value (fc-conj--ptr-is-unknown))
+(get-value (fc-conj--ptr-is-nonnull))
+(get-value (fc-conj--ptr-is-nullable))
+(get-value (fc-conj--ptrs-were-compared))
+(get-value (fc-conj--join-ptr))
+(get-value (is-unsafe-to-deref))
+
+; vim: set syntax=scheme:
diff --git a/nullability/formal_methods/optional.smt2 b/nullability/formal_methods/optional.smt2
new file mode 100644
index 0000000..5ed2495
--- /dev/null
+++ b/nullability/formal_methods/optional.smt2
@@ -0,0 +1,121 @@
+;; 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
+
+;; Run: cvc5 --lang smt --incremental optional.smt2
+
+(set-option :produce-models true)
+(set-option :produce-assertions true)
+(set-option :produce-assignments true)
+(set-option :produce-unsat-cores true)
+(set-logic HO_ALL)
+
+(define-fun join-fc ((c Bool) (fc-then Bool) (fc-else Bool)) Bool
+  (or
+    (and c fc-then)
+    (and (not c) fc-else)))
+
+(declare-datatype OptionalValue
+  ((make-optional-value (get-x0 Bool))))
+
+(define-fun get-has-value ((opt OptionalValue)) Bool
+  (get-x0 opt))
+
+(define-fun is-unsafe-to-unwrap ((fc Bool) (opt OptionalValue)) Bool
+  (and fc (not (get-has-value opt))))
+
+(echo "=============================")
+(echo "=== Example Regular-Check ===")
+(push 1)
+
+; void foo(optional<int> x) {
+;   // (1)
+;   x.value();
+;   if (x.has_value()) {
+;     // (2)
+;     x.value();
+;   }
+; }
+
+(declare-fun x () OptionalValue)
+
+(define-fun fc-1 () Bool
+  true)
+
+(define-fun fc-2 () Bool
+  (get-has-value x))
+
+(push 1)
+  (assert (is-unsafe-to-unwrap fc-1 x))
+  (echo "Expected: sat")
+  (echo "Actual:")
+  (check-sat)
+  (get-value (fc-1 fc-2 x))
+(pop 1)
+
+(push 1)
+  (assert (is-unsafe-to-unwrap fc-2 x))
+  (echo "Expected: unsat")
+  (echo "Actual:")
+  (check-sat)
+  (get-value (fc-1 fc-2 x))
+(pop 1)
+
+(pop 1)
+
+(echo "============================")
+(echo "=== Example Mixed-Values ===")
+(push 1)
+
+; void foo(optional<int> x, bool b) {
+;   // (1)
+;   if (b) {
+;     // (2)
+;     x = 42;
+;     // (3)
+;   }
+;   // (4)
+;   x.value();
+;   if (b) {
+;     // (5)
+;     x.value();
+;   }
+; }
+
+(declare-fun x () OptionalValue)
+(declare-fun b () Bool)
+
+(define-fun fc-1 () Bool
+  true)
+
+(define-fun fc-2 () Bool
+  (and fc-1 b))
+
+(define-fun fc-3 () Bool
+  (and fc-2 (get-has-value x)))
+
+(define-fun fc-4 () Bool
+  (join-fc b fc-3 fc-1))
+
+(define-fun fc-5 () Bool
+  (and fc-4 b))
+
+(push 1)
+  (assert (is-unsafe-to-unwrap fc-4 x))
+  (echo "Expected: sat")
+  (echo "Actual:")
+  (check-sat)
+  (get-value (fc-1 fc-2 fc-3 fc-4 fc-5 x b))
+(pop 1)
+
+(push 1)
+  (assert (is-unsafe-to-unwrap fc-5 x))
+  (echo "Expected: unsat")
+  (echo "Actual:")
+  (check-sat)
+  (get-value (fc-1 fc-2 fc-3 fc-4 fc-5 x b))
+(pop 1)
+
+(pop 1)
+
+; vim: set syntax=scheme:
diff --git a/nullability/formal_methods/optional_code_synthesis.smt2 b/nullability/formal_methods/optional_code_synthesis.smt2
new file mode 100644
index 0000000..93d7a7d
--- /dev/null
+++ b/nullability/formal_methods/optional_code_synthesis.smt2
@@ -0,0 +1,149 @@
+;; 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
+
+;; Run: cvc5 --lang smt --incremental --fmf-bound optional_code_synthesis.smt2
+
+(set-option :produce-models true)
+(set-option :produce-assertions true)
+(set-option :produce-assignments true)
+(set-option :produce-unsat-cores true)
+(set-logic HO_ALL)
+
+(declare-datatype FlowConditions
+  ((make-flow-conditions
+    (get-fc-1 Bool)
+    (get-fc-2 Bool)
+    (get-fc-3 Bool)
+    (get-fc-4 Bool)
+    (get-fc-5 Bool))))
+
+(define-fun join-fc ((c Bool) (fc-then Bool) (fc-else Bool)) Bool
+  (or
+    (and c fc-then)
+    (and (not c) fc-else)))
+
+(declare-datatype OptionalValue
+  ((make-optional-value (get-x0 Bool))))
+
+(declare-fun get-has-value (OptionalValue) Bool)
+
+;; Args: flow-condition, opt.
+(declare-fun is-unsafe-to-unwrap (Bool OptionalValue) Bool)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example RegularCheck
+;;
+;; ```
+;; void foo(optional<int> x) {
+;;   // (1)
+;;   x.value();
+;;   if (x.has_value()) {
+;;     // (2)
+;;     x.value();
+;;   }
+;; }
+;; ```
+
+(define-fun run-RegularCheck
+  ((x OptionalValue) (body (-> FlowConditions Bool))) Bool
+  (let ((fc-1 true))
+    (let ((fc-2 (and fc-1 (get-has-value x))))
+      (body (make-flow-conditions fc-1 fc-2 false false false)))))
+
+;; Every program point is reachable.
+(assert
+  (exists ((x OptionalValue))
+    (run-RegularCheck x (lambda ((fcs FlowConditions)) (get-fc-1 fcs)))))
+
+(assert
+  (exists ((x OptionalValue))
+    (run-RegularCheck x (lambda ((fcs FlowConditions)) (get-fc-2 fcs)))))
+
+;; Unwrap at (1) is unsafe.
+(assert
+  (exists ((x OptionalValue))
+    (run-RegularCheck
+      x
+      (lambda ((fcs FlowConditions))
+        (is-unsafe-to-unwrap (get-fc-1 fcs) x)))))
+
+;; Unwrap at (2) is safe.
+(assert
+  (forall ((x OptionalValue))
+    (run-RegularCheck
+      x
+      (lambda ((fcs FlowConditions))
+      (not (is-unsafe-to-unwrap (get-fc-2 fcs) x))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Example MixedValues
+;;
+;; ```
+;; void foo(optional<int> x, bool b) {
+;;   // (1)
+;;   if (b) {
+;;     // (2)
+;;     x = 42;
+;;     // (3)
+;;   }
+;;   // (4)
+;;   x.value();
+;;   if (b) {
+;;     // (5)
+;;     x.value();
+;;   }
+;; }
+;; ```
+
+(define-fun run-MixedValues
+  ((x OptionalValue) (b Bool) (body (-> FlowConditions Bool))) Bool
+  (let ((fc-1 true))
+    (let ((fc-2 (and fc-1 b)))
+      (let ((fc-3 (and fc-2 (get-has-value x))))
+        (let ((fc-4 (join-fc b fc-3 fc-1)))
+          (let ((fc-5 (and fc-4 b)))
+            (body (make-flow-conditions fc-1 fc-2 fc-3 fc-4 fc-5))))))))
+
+;; Every program point is reachable.
+(assert
+  (exists ((x OptionalValue) (b Bool))
+    (run-MixedValues x b (lambda ((fcs FlowConditions)) (get-fc-1 fcs)))))
+
+(assert
+  (exists ((x OptionalValue) (b Bool))
+    (run-MixedValues x b (lambda ((fcs FlowConditions)) (get-fc-2 fcs)))))
+
+(assert
+  (exists ((x OptionalValue) (b Bool))
+    (run-MixedValues x b (lambda ((fcs FlowConditions)) (get-fc-3 fcs)))))
+
+(assert
+  (exists ((x OptionalValue) (b Bool))
+    (run-MixedValues x b (lambda ((fcs FlowConditions)) (get-fc-4 fcs)))))
+
+(assert
+  (exists ((x OptionalValue) (b Bool))
+    (run-MixedValues x b (lambda ((fcs FlowConditions)) (get-fc-5 fcs)))))
+
+;; Unwrap at (4) is unsafe.
+(assert
+  (exists ((x OptionalValue) (b Bool))
+    (run-MixedValues
+      x b
+      (lambda ((fcs FlowConditions))
+        (is-unsafe-to-unwrap (get-fc-4 fcs) x)))))
+
+;; Unwrap at (5) is safe.
+(assert
+  (forall ((x OptionalValue) (b Bool))
+    (run-MixedValues
+      x b
+      (lambda ((fcs FlowConditions))
+        (not (is-unsafe-to-unwrap (get-fc-5 fcs) x))))))
+
+(check-sat)
+(get-value (get-has-value))
+(get-value (is-unsafe-to-unwrap))
+
+; vim: set syntax=scheme:
diff --git a/nullability/pointer_nullability.cc b/nullability/pointer_nullability.cc
new file mode 100644
index 0000000..34e16ff
--- /dev/null
+++ b/nullability/pointer_nullability.cc
@@ -0,0 +1,316 @@
+// 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.h"
+
+#include "absl/log/check.h"
+#include "nullability/pointer_nullability_lattice.h"
+#include "clang/AST/ASTDumper.h"
+#include "clang/AST/TypeVisitor.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/SaveAndRestore.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using dataflow::AtomicBoolValue;
+using dataflow::BoolValue;
+using dataflow::Environment;
+using dataflow::PointerValue;
+using dataflow::SkipPast;
+using dataflow::TransferState;
+
+/// 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 `Null`.
+constexpr llvm::StringLiteral kKnown = "is_known";
+constexpr llvm::StringLiteral kNull = "is_null";
+
+NullabilityKind getNullabilityKind(QualType Type, ASTContext& Ctx) {
+  return Type->getNullability().value_or(NullabilityKind::Unspecified);
+}
+
+PointerValue* getPointerValueFromExpr(const Expr* PointerExpr,
+                                      const Environment& Env) {
+  return cast_or_null<PointerValue>(
+      Env.getValue(*PointerExpr, SkipPast::Reference));
+}
+
+std::pair<AtomicBoolValue&, AtomicBoolValue&> getPointerNullState(
+    const PointerValue& PointerVal, const Environment& Env) {
+  auto& PointerKnown = *cast<AtomicBoolValue>(PointerVal.getProperty(kKnown));
+  auto& PointerNull = *cast<AtomicBoolValue>(PointerVal.getProperty(kNull));
+  return {PointerKnown, PointerNull};
+}
+
+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(PointerValue& PointerVal, Environment& Env,
+                          BoolValue* KnownConstraint,
+                          BoolValue* NullConstraint) {
+  initPointerBoolProperty(PointerVal, kKnown, KnownConstraint, Env);
+  initPointerBoolProperty(PointerVal, kNull, NullConstraint, Env);
+}
+
+bool isNullable(const PointerValue& PointerVal, const Environment& Env) {
+  auto [PointerKnown, PointerNull] = getPointerNullState(PointerVal, Env);
+  auto& PointerNotKnownNull =
+      Env.makeNot(Env.makeAnd(PointerKnown, PointerNull));
+  return !Env.flowConditionImplies(PointerNotKnownNull);
+}
+
+std::string nullabilityToString(ArrayRef<NullabilityKind> Nullability) {
+  std::string Result = "[";
+  llvm::interleave(
+      Nullability,
+      [&](const NullabilityKind n) {
+        Result += getNullabilitySpelling(n).str();
+      },
+      [&] { Result += ", "; });
+  Result += "]";
+  return Result;
+}
+
+namespace {
+// Traverses a Type to find the points where it might be nullable.
+// This will visit the contained PointerType in the correct order to produce
+// the TypeNullability vector.
+//
+// Subclasses must provide `void report(const PointerType*, NullabilityKind)`,
+// and may override TypeVisitor Visit*Type methods to customize the traversal.
+//
+// Canonically-equivalent Types produce equivalent sequences of report() calls:
+//  - corresponding PointerTypes are canonically-equivalent
+//  - the NullabilityKind may be different, as it derives from type sugar
+template <class Impl>
+class NullabilityWalker : public TypeVisitor<Impl> {
+  using Base = TypeVisitor<Impl>;
+  Impl& derived() { return *static_cast<Impl*>(this); }
+
+  // A nullability attribute we've seen, waiting to attach to a pointer type.
+  // There may be sugar in between: Attributed -> Typedef -> Typedef -> Pointer.
+  // All non-sugar types must consume nullability, most will ignore it.
+  std::optional<NullabilityKind> PendingNullability;
+
+  void ignoreUnexpectedNullability() {
+    // TODO: Can we upgrade this to an assert?
+    // clang is pretty thorough about ensuring we can't put _Nullable on
+    // non-pointers, even failing template instantiation on this basis.
+    PendingNullability.reset();
+  }
+
+  // While walking the underlying type of alias TemplateSpecializationTypes,
+  // we see SubstTemplateTypeParmTypes where type parameters were referenced.
+  // The directly-available underlying types lack sugar, but we can retrieve the
+  // sugar from the arguments of the original TemplateSpecializationType.
+  //
+  // It is only possible to reference params of the immediately enclosing alias,
+  // so we keep details of the alias specialization we're currently processing.
+  struct AliasArgs {
+    const Decl* AssociatedDecl;
+    ArrayRef<TemplateArgument> Args;
+    // The alias context in which the alias specialization itself appeared.
+    // (The alias's args may reference params from this context.)
+    const AliasArgs* Parent;
+  };
+  const AliasArgs* CurrentAliasTemplate = nullptr;
+
+ public:
+  void Visit(QualType T) { Base::Visit(T.getTypePtr()); }
+  void Visit(const TemplateArgument& TA) {
+    if (TA.getKind() == TemplateArgument::Type) Visit(TA.getAsType());
+    if (TA.getKind() == TemplateArgument::Pack)
+      for (const auto& PackElt : TA.getPackAsArray()) Visit(PackElt);
+  }
+  void Visit(const DeclContext* DC) {
+    // For now, only consider enclosing classes.
+    // TODO: The nullability of template functions can affect local classes too,
+    // this can be relevant e.g. when instantiating templates with such types.
+    if (auto* CRD = llvm::dyn_cast<CXXRecordDecl>(DC))
+      Visit(DC->getParentASTContext().getRecordType(CRD));
+  }
+
+  void VisitType(const Type* T) {
+    // For sugar not explicitly handled below, desugar and continue.
+    // (We need to walk the full structure of the canonical type.)
+    if (auto* Desugar =
+            T->getLocallyUnqualifiedSingleStepDesugaredType().getTypePtr();
+        Desugar != T)
+      return Base::Visit(Desugar);
+
+    // We don't expect to see any nullable non-sugar types except PointerType.
+    ignoreUnexpectedNullability();
+    Base::VisitType(T);
+  }
+
+  void VisitFunctionProtoType(const FunctionProtoType* FPT) {
+    ignoreUnexpectedNullability();
+    Visit(FPT->getReturnType());
+    for (auto ParamType : FPT->getParamTypes()) Visit(ParamType);
+  }
+
+  void VisitTemplateSpecializationType(const TemplateSpecializationType* TST) {
+    if (TST->isTypeAlias()) {
+      // Aliases are sugar, visit the underlying type.
+      // Record template args so we can resugar substituted params.
+      const AliasArgs Args{TST->getTemplateName().getAsTemplateDecl(),
+                           TST->template_arguments(), CurrentAliasTemplate};
+      llvm::SaveAndRestore UseAlias(CurrentAliasTemplate, &Args);
+      VisitType(TST);
+      return;
+    }
+
+    auto* CRD = TST->getAsCXXRecordDecl();
+    CHECK(CRD) << "Expected an alias or class specialization in concrete code";
+    ignoreUnexpectedNullability();
+    Visit(CRD->getDeclContext());
+    for (auto TA : TST->template_arguments()) Visit(TA);
+  }
+
+  void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType* T) {
+    if (isa<TypeAliasTemplateDecl>(T->getAssociatedDecl())) {
+      if (CurrentAliasTemplate != nullptr) {
+        CHECK(T->getAssociatedDecl() == CurrentAliasTemplate->AssociatedDecl);
+        unsigned Index = T->getIndex();
+        // Valid because pack must be the last param in alias templates.
+        if (auto PackIndex = T->getPackIndex())
+          Index = CurrentAliasTemplate->Args.size() - 1 - *PackIndex;
+        const TemplateArgument& Arg = CurrentAliasTemplate->Args[Index];
+
+        llvm::SaveAndRestore OriginalContext(CurrentAliasTemplate,
+                                             CurrentAliasTemplate->Parent);
+        return Visit(Arg);
+      } else {
+        // Our top-level type references an unbound type alias param.
+        // Presumably our original input was the underlying type of an alias
+        // instantiation, we now lack the context needed to resugar it.
+        // TODO: maybe this could be an assert? We would need to trust all
+        // callers are obtaining types appropriately, and that clang never
+        // partially-desugars in a problematic way.
+      }
+    }
+    VisitType(T);
+  }
+
+  void VisitRecordType(const RecordType* RT) {
+    ignoreUnexpectedNullability();
+    Visit(RT->getDecl()->getDeclContext());
+    if (auto* CTSD = dyn_cast<ClassTemplateSpecializationDecl>(RT->getDecl())) {
+      // TODO: if this is an instantiation, these args lack sugar.
+      // We can try to retrieve it from the current template context.
+      for (auto& TA : CTSD->getTemplateArgs().asArray()) Visit(TA);
+    }
+  }
+
+  void VisitAttributedType(const AttributedType* AT) {
+    if (auto NK = AT->getImmediateNullability()) {
+      // If we see nullability applied twice, the outer one wins.
+      if (!PendingNullability.has_value()) PendingNullability = *NK;
+    }
+    Visit(AT->getModifiedType());
+    CHECK(!PendingNullability.has_value())
+        << "Should have been consumed by modified type! "
+        << AT->getModifiedType().getAsString();
+  }
+
+  void VisitPointerType(const PointerType* PT) {
+    derived().report(PT,
+                     PendingNullability.value_or(NullabilityKind::Unspecified));
+    PendingNullability.reset();
+    Visit(PT->getPointeeType());
+  }
+};
+
+template <typename T>
+unsigned countPointers(const T& Object) {
+  struct Walker : public NullabilityWalker<Walker> {
+    unsigned Count = 0;
+    void report(const PointerType*, NullabilityKind) { ++Count; }
+  } PointerCountWalker;
+  PointerCountWalker.Visit(Object);
+  return PointerCountWalker.Count;
+}
+
+}  // namespace
+
+unsigned countPointersInType(QualType T) { return countPointers(T); }
+
+unsigned countPointersInType(const DeclContext* DC) {
+  return countPointers(DC);
+}
+unsigned countPointersInType(TemplateArgument TA) { return countPointers(TA); }
+
+QualType exprType(const Expr* E) {
+  if (E->hasPlaceholderType(BuiltinType::BoundMember))
+    return Expr::findBoundMemberType(E);
+  return E->getType();
+}
+
+unsigned countPointersInType(const Expr* E) {
+  return countPointersInType(exprType(E));
+}
+
+std::vector<NullabilityKind> getNullabilityAnnotationsFromType(
+    QualType T,
+    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam) {
+  struct Walker : NullabilityWalker<Walker> {
+    std::vector<NullabilityKind> Annotations;
+    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam;
+
+    void report(const PointerType*, NullabilityKind NK) {
+      Annotations.push_back(NK);
+    }
+
+    void VisitSubstTemplateTypeParmType(const SubstTemplateTypeParmType* ST) {
+      if (SubstituteTypeParam) {
+        if (auto Subst = SubstituteTypeParam(ST)) {
+          DCHECK_EQ(Subst->size(),
+                    countPointersInType(ST->getCanonicalTypeInternal()))
+              << "Substituted nullability has the wrong structure: "
+              << QualType(ST, 0).getAsString();
+          llvm::append_range(Annotations, *Subst);
+          return;
+        }
+      }
+      NullabilityWalker::VisitSubstTemplateTypeParmType(ST);
+    }
+  } AnnotationVisitor;
+  AnnotationVisitor.SubstituteTypeParam = SubstituteTypeParam;
+  AnnotationVisitor.Visit(T);
+  return std::move(AnnotationVisitor.Annotations);
+}
+
+std::vector<NullabilityKind> unspecifiedNullability(const Expr* E) {
+  return std::vector<NullabilityKind>(countPointersInType(E),
+                                      NullabilityKind::Unspecified);
+}
+
+ArrayRef<NullabilityKind> getNullabilityForChild(
+    const Expr* E, TransferState<PointerNullabilityLattice>& State) {
+  return State.Lattice.insertExprNullabilityIfAbsent(E, [&] {
+    // Since we process child nodes before parents, we should already have
+    // computed the child nullability. However, this is not true in all test
+    // cases. So, we return unspecified nullability annotations.
+    // TODO: fix this issue, and CHECK() instead.
+    llvm::dbgs() << "=== Missing child nullability: ===\n";
+    dump(E, llvm::dbgs());
+    llvm::dbgs() << "==================================\n";
+
+    return unspecifiedNullability(E);
+  });
+}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/pointer_nullability.h b/nullability/pointer_nullability.h
new file mode 100644
index 0000000..b88a889
--- /dev/null
+++ b/nullability/pointer_nullability.h
@@ -0,0 +1,151 @@
+// 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_POINTER_NULLABILITY_H_
+#define CRUBIT_NULLABILITY_POINTER_NULLABILITY_H_
+
+#include <utility>
+
+#include "nullability/pointer_nullability_lattice.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/ASTDumper.h"
+#include "clang/AST/Expr.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
+#include "clang/Basic/Specifiers.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using dataflow::TransferState;
+
+/// Returns the `NullabilityKind` corresponding to the nullability annotation on
+/// `Type` if present. Otherwise, returns `NullabilityKind::Unspecified`.
+NullabilityKind getNullabilityKind(QualType Type, ASTContext& Ctx);
+
+/// Returns the `PointerValue` allocated to `PointerExpr` if available.
+/// Otherwise, returns nullptr.
+dataflow::PointerValue* getPointerValueFromExpr(
+    const Expr* PointerExpr, const dataflow::Environment& Env);
+
+/// 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 null.
+std::pair<dataflow::AtomicBoolValue&, dataflow::AtomicBoolValue&>
+getPointerNullState(const dataflow::PointerValue& PointerVal,
+                    const dataflow::Environment& Env);
+
+/// Sets the nullness properties on `PointerVal` if not already initialised.
+///
+/// The boolean properties may be constrained by specifying `KnownConstraint`
+/// and `NullConstraint`. Otherwise, the properties are set to freshly
+/// created atomic booleans.
+void initPointerNullState(dataflow::PointerValue& PointerVal,
+                          dataflow::Environment& Env,
+                          dataflow::BoolValue* KnownConstraint = nullptr,
+                          dataflow::BoolValue* NullConstraint = nullptr);
+
+/// Sets the nullness properties on `PointerVal` representing a nullptr if not
+/// already initialised.
+///
+/// `Known` is constrained to true, `Null` is constrained to true.
+inline void initNullPointer(dataflow::PointerValue& PointerVal,
+                            dataflow::Environment& Env) {
+  initPointerNullState(PointerVal, Env,
+                       /*KnownConstraint=*/&Env.getBoolLiteralValue(true),
+                       /*NullConstraint=*/&Env.getBoolLiteralValue(true));
+}
+
+/// Sets the nullness properties on `PointerVal` representing a pointer that is
+/// not null if not already initialised.
+///
+/// `Known` is constrained to true, `Null` is constrained to false.
+inline void initNotNullPointer(dataflow::PointerValue& PointerVal,
+                               dataflow::Environment& Env) {
+  initPointerNullState(PointerVal, Env,
+                       /*KnownConstraint=*/&Env.getBoolLiteralValue(true),
+                       /*NullConstraint=*/&Env.getBoolLiteralValue(false));
+}
+
+/// Sets the nullness properties on `PointerVal` representing a pointer that is
+/// nullable if not already initialised.
+///
+/// `Known` is constrained to true, `Null` is unconstrained.
+inline void initNullablePointer(dataflow::PointerValue& PointerVal,
+                                dataflow::Environment& Env) {
+  initPointerNullState(PointerVal, Env,
+                       /*KnownConstraint=*/&Env.getBoolLiteralValue(true));
+}
+
+/// Sets the nullness properties on `PointerVal` representing a pointer with
+/// unknown nullability if not already initialised.
+///
+/// `Known` is constrained to false, `Null` is unconstrained.
+inline void initUnknownPointer(dataflow::PointerValue& PointerVal,
+                               dataflow::Environment& Env) {
+  initPointerNullState(PointerVal, Env,
+                       /*KnownConstraint=*/&Env.getBoolLiteralValue(false));
+}
+
+/// Returns true if there is evidence that `PointerVal` may hold a nullptr.
+bool isNullable(const dataflow::PointerValue& PointerVal,
+                const dataflow::Environment& Env);
+
+/// Returns a human-readable debug representation of a nullability vector.
+std::string nullabilityToString(ArrayRef<NullabilityKind> Nullability);
+
+/// A function that may provide enhanced nullability information for a
+/// substituted template parameter (which has no sugar of its own).
+using GetTypeParamNullability = std::optional<std::vector<NullabilityKind>>(
+    const SubstTemplateTypeParmType* ST);
+/// Traverse over a type to get its nullability. For example, if T is the type
+/// Struct3Arg<int * _Nonnull, int, pair<int * _Nullable, int *>> * _Nonnull,
+/// the resulting nullability annotations will be {_Nonnull, _Nonnull,
+/// _Nullable, _Unknown}. Note that non-pointer elements (e.g., the second
+/// argument of Struct3Arg) do not get a nullability annotation.
+std::vector<NullabilityKind> getNullabilityAnnotationsFromType(
+    QualType T,
+    llvm::function_ref<GetTypeParamNullability> SubstituteTypeParam = nullptr);
+
+/// Prints QualType's underlying canonical type, annotated with nullability.
+/// See rebuildWithNullability().
+std::string printWithNullability(QualType, ArrayRef<NullabilityKind>,
+                                 ASTContext&);
+/// Returns an equivalent type annotated with the provided nullability.
+/// Any existing sugar (including nullability) is discarded.
+/// rebuildWithNullability(int *, {Nullable}) ==> int * _Nullable.
+QualType rebuildWithNullability(QualType, ArrayRef<NullabilityKind>,
+                                ASTContext&);
+
+/// Computes the number of pointer slots within a type.
+/// Each of these could conceptually be nullable, so this is the length of
+/// the nullability vector computed by getNullabilityAnnotationsFromType().
+unsigned countPointersInType(QualType T);
+unsigned countPointersInType(const Expr* E);
+unsigned countPointersInType(TemplateArgument TA);
+unsigned countPointersInType(const DeclContext* DC);
+
+QualType exprType(const Expr* E);
+
+std::vector<NullabilityKind> unspecifiedNullability(const Expr* E);
+
+// Work around the lack of Expr.dump() etc with an ostream but no ASTContext.
+template <typename T>
+void dump(const T& Node, llvm::raw_ostream& OS) {
+  clang::ASTDumper(OS, /*ShowColors=*/false).Visit(Node);
+}
+
+// Returns the computed nullability for a subexpr of the current expression.
+// This is always available as we compute bottom-up.
+ArrayRef<NullabilityKind> getNullabilityForChild(
+    const Expr* E, TransferState<PointerNullabilityLattice>& State);
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // CRUBIT_NULLABILITY_POINTER_NULLABILITY_H_
diff --git a/nullability/pointer_nullability_analysis.cc b/nullability/pointer_nullability_analysis.cc
new file mode 100644
index 0000000..7ab5c82
--- /dev/null
+++ b/nullability/pointer_nullability_analysis.cc
@@ -0,0 +1,697 @@
+// 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_analysis.h"
+
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "absl/log/check.h"
+#include "nullability/pointer_nullability.h"
+#include "nullability/pointer_nullability_lattice.h"
+#include "nullability/pointer_nullability_matchers.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/ASTDumper.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/OperationKinds.h"
+#include "clang/AST/Stmt.h"
+#include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
+#include "clang/Basic/LLVM.h"
+#include "clang/Basic/Specifiers.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using ast_matchers::MatchFinder;
+using dataflow::BoolValue;
+using dataflow::CFGMatchSwitchBuilder;
+using dataflow::Environment;
+using dataflow::PointerValue;
+using dataflow::SkipPast;
+using dataflow::TransferState;
+using dataflow::Value;
+
+namespace {
+
+std::vector<NullabilityKind> prepend(NullabilityKind Head,
+                                     ArrayRef<NullabilityKind> Tail) {
+  std::vector<NullabilityKind> Result = {Head};
+  Result.insert(Result.end(), Tail.begin(), Tail.end());
+  return Result;
+}
+
+void computeNullability(const Expr* E,
+                        TransferState<PointerNullabilityLattice>& State,
+                        std::function<std::vector<NullabilityKind>()> Compute) {
+  (void)State.Lattice.insertExprNullabilityIfAbsent(E, [&] {
+    auto Nullability = Compute();
+    if (unsigned ExpectedSize = countPointersInType(E);
+        ExpectedSize != Nullability.size()) {
+      // A nullability vector must have one entry per pointer in the type.
+      // If this is violated, we probably failed to handle some AST node.
+      llvm::dbgs()
+          << "=== Nullability vector has wrong number of entries: ===\n";
+      llvm::dbgs() << "Expression: \n";
+      dump(E, llvm::dbgs());
+      llvm::dbgs() << "\nNullability (" << Nullability.size()
+                   << " pointers): " << nullabilityToString(Nullability)
+                   << "\n";
+      llvm::dbgs() << "\nType (" << ExpectedSize << " pointers): \n";
+      dump(exprType(E), llvm::dbgs());
+      llvm::dbgs() << "=================================\n";
+
+      // We can't meaningfully interpret the vector, so discard it.
+      // TODO: fix all broken cases and upgrade to CHECK or DCHECK or so.
+      Nullability.assign(ExpectedSize, NullabilityKind::Unspecified);
+    }
+    return Nullability;
+  });
+}
+
+/// Compute the nullability annotation of type `T`, which contains types
+/// originally written as a class template type parameter.
+///
+/// Example:
+///
+/// \code
+///   template <typename F, typename S>
+///   struct pair {
+///     S *_Nullable getNullablePtrToSecond();
+///   };
+/// \endcode
+///
+/// Consider the following member call:
+///
+/// \code
+///   pair<int *, int *_Nonnull> x;
+///   x.getNullablePtrToSecond();
+/// \endcode
+///
+/// The class template specialization `x` has the following substitutions:
+///
+///   F=int *, whose nullability is [_Unspecified]
+///   S=int * _Nonnull, whose nullability is [_Nonnull]
+///
+/// The return type of the member call `x.getNullablePtrToSecond()` is
+/// S * _Nullable.
+///
+/// When we call `substituteNullabilityAnnotationsInClassTemplate` with the type
+/// `S * _Nullable` and the `base` node of the member call (in this case, a
+/// `DeclRefExpr`), it returns the nullability of the given type after applying
+/// substitutions, which in this case is [_Nullable, _Nonnull].
+std::vector<NullabilityKind> substituteNullabilityAnnotationsInClassTemplate(
+    QualType T, ArrayRef<NullabilityKind> BaseNullabilityAnnotations,
+    QualType BaseType) {
+  return getNullabilityAnnotationsFromType(
+      T,
+      [&](const SubstTemplateTypeParmType* ST)
+          -> std::optional<std::vector<NullabilityKind>> {
+        // The class specialization that is BaseType and owns ST.
+        const ClassTemplateSpecializationDecl* Specialization = nullptr;
+        if (auto RT = BaseType->getAs<RecordType>())
+          Specialization =
+              dyn_cast<ClassTemplateSpecializationDecl>(RT->getDecl());
+        // TODO: handle nested templates, where associated decl != base type
+        // (e.g. PointerNullabilityTest.MemberFunctionTemplateOfTemplateStruct)
+        if (!Specialization || Specialization != ST->getAssociatedDecl())
+          return std::nullopt;
+
+        unsigned ArgIndex = ST->getIndex();
+        auto TemplateArgs = Specialization->getTemplateArgs().asArray();
+
+        unsigned PointerCount =
+            countPointersInType(Specialization->getDeclContext());
+        for (auto TA : TemplateArgs.take_front(ArgIndex)) {
+          PointerCount += countPointersInType(TA);
+        }
+        unsigned SliceSize = countPointersInType(TemplateArgs[ArgIndex]);
+        return BaseNullabilityAnnotations.slice(PointerCount, SliceSize).vec();
+      });
+}
+
+/// Compute nullability annotations of `T`, which might contain template type
+/// variable substitutions bound by the call `CE`.
+///
+/// Example:
+///
+/// \code
+///   template<typename F, typename S>
+///   std::pair<S, F> flip(std::pair<F, S> p);
+/// \endcode
+///
+/// Consider the following CallExpr:
+///
+/// \code
+///   flip<int * _Nonnull, int * _Nullable>(std::make_pair(&x, &y));
+/// \endcode
+///
+/// This CallExpr has the following substitutions:
+///   F=int * _Nonnull, whose nullability is [_Nonnull]
+///   S=int * _Nullable, whose nullability is [_Nullable]
+///
+/// The return type of this CallExpr is `std::pair<S, F>`.
+///
+/// When we call `substituteNullabilityAnnotationsInFunctionTemplate` with the
+/// type `std::pair<S, F>` and the above CallExpr, it returns the nullability
+/// the given type after applying substitutions, which in this case is
+/// [_Nullable, _Nonnull].
+std::vector<NullabilityKind> substituteNullabilityAnnotationsInFunctionTemplate(
+    QualType T, const CallExpr* CE) {
+  return getNullabilityAnnotationsFromType(
+      T,
+      [&](const SubstTemplateTypeParmType* ST)
+          -> std::optional<std::vector<NullabilityKind>> {
+        // TODO: Handle calls that use template argument deduction.
+        // TODO: Handle nested templates (...->getDepth() > 0).
+        if (auto* DRE =
+                dyn_cast<DeclRefExpr>(CE->getCallee()->IgnoreImpCasts());
+            DRE != nullptr && ST->getReplacedParameter()->getDepth() == 0 &&
+            // Some or all of the template arguments may be deduced, and we
+            // won't see those on the `DeclRefExpr`. If the template argument
+            // was deduced, we don't have any sugar for it.
+            // TODO(b/268348533): Can we somehow obtain it from the function
+            // param it was deduced from?
+            // TODO(b/268345783): This check, as well as the index into
+            // `template_arguments` below, may be incorrect in the presence of
+            // parameters packs.  In function templates, parameter packs may
+            // appear anywhere in the parameter list. The index may therefore
+            // refer to one of the pack arguments, but we might incorrectly
+            // interpret it as referring to an argument that follows the pack.
+            ST->getIndex() < DRE->template_arguments().size()) {
+          TypeSourceInfo* TSI =
+              DRE->template_arguments()[ST->getIndex()].getTypeSourceInfo();
+          if (TSI == nullptr) return std::nullopt;
+          return getNullabilityAnnotationsFromType(TSI->getType());
+        }
+        return std::nullopt;
+      });
+}
+
+NullabilityKind getPointerNullability(const Expr* E,
+                                      PointerNullabilityAnalysis::Lattice& L) {
+  QualType ExprType = E->getType();
+  std::optional<NullabilityKind> Nullability = ExprType->getNullability();
+
+  // If the expression's type does not contain nullability information, it may
+  // be a template instantiation. Look up the nullability in the
+  // `ExprToNullability` map.
+  if (Nullability.value_or(NullabilityKind::Unspecified) ==
+      NullabilityKind::Unspecified) {
+    if (auto MaybeNullability = L.getExprNullability(E)) {
+      if (!MaybeNullability->empty()) {
+        // Return the nullability of the topmost pointer in the type.
+        Nullability = (*MaybeNullability)[0];
+      }
+    }
+  }
+  return Nullability.value_or(NullabilityKind::Unspecified);
+}
+
+void initPointerFromAnnotations(
+    PointerValue& PointerVal, const Expr* E,
+    TransferState<PointerNullabilityLattice>& State) {
+  NullabilityKind Nullability = getPointerNullability(E, State.Lattice);
+  switch (Nullability) {
+    case NullabilityKind::NonNull:
+      initNotNullPointer(PointerVal, State.Env);
+      break;
+    case NullabilityKind::Nullable:
+      initNullablePointer(PointerVal, State.Env);
+      break;
+    default:
+      initUnknownPointer(PointerVal, State.Env);
+  }
+}
+
+void transferFlowSensitiveNullPointer(
+    const Expr* NullPointer, const MatchFinder::MatchResult&,
+    TransferState<PointerNullabilityLattice>& State) {
+  if (auto* PointerVal = getPointerValueFromExpr(NullPointer, State.Env)) {
+    initNullPointer(*PointerVal, State.Env);
+  }
+}
+
+void transferFlowSensitiveNotNullPointer(
+    const Expr* NotNullPointer, const MatchFinder::MatchResult&,
+    TransferState<PointerNullabilityLattice>& State) {
+  if (auto* PointerVal = getPointerValueFromExpr(NotNullPointer, State.Env)) {
+    initNotNullPointer(*PointerVal, State.Env);
+  }
+}
+
+void transferFlowSensitivePointer(
+    const Expr* PointerExpr, const MatchFinder::MatchResult& Result,
+    TransferState<PointerNullabilityLattice>& State) {
+  if (auto* PointerVal = getPointerValueFromExpr(PointerExpr, State.Env)) {
+    initPointerFromAnnotations(*PointerVal, PointerExpr, State);
+  }
+}
+
+// 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 transferFlowSensitiveNullCheckComparison(
+    const BinaryOperator* BinaryOp, const MatchFinder::MatchResult& result,
+    TransferState<PointerNullabilityLattice>& State) {
+  // Boolean representing the comparison between the two pointer values,
+  // automatically created by the dataflow framework.
+  auto& PointerComparison =
+      *cast<BoolValue>(State.Env.getValue(*BinaryOp, SkipPast::None));
+
+  CHECK(BinaryOp->getOpcode() == BO_EQ || BinaryOp->getOpcode() == BO_NE);
+  auto& PointerEQ = BinaryOp->getOpcode() == BO_EQ
+                        ? PointerComparison
+                        : State.Env.makeNot(PointerComparison);
+  auto& PointerNE = BinaryOp->getOpcode() == BO_EQ
+                        ? State.Env.makeNot(PointerComparison)
+                        : PointerComparison;
+
+  auto* LHS = getPointerValueFromExpr(BinaryOp->getLHS(), State.Env);
+  auto* RHS = getPointerValueFromExpr(BinaryOp->getRHS(), State.Env);
+
+  if (!LHS || !RHS) return;
+
+  auto [LHSKnown, LHSNull] = getPointerNullState(*LHS, State.Env);
+  auto [RHSKnown, RHSNull] = getPointerNullState(*RHS, State.Env);
+  auto& LHSKnownNotNull =
+      State.Env.makeAnd(LHSKnown, State.Env.makeNot(LHSNull));
+  auto& RHSKnownNotNull =
+      State.Env.makeAnd(RHSKnown, State.Env.makeNot(RHSNull));
+  auto& LHSKnownNull = State.Env.makeAnd(LHSKnown, LHSNull);
+  auto& RHSKnownNull = State.Env.makeAnd(RHSKnown, RHSNull);
+
+  // nullptr == nullptr
+  State.Env.addToFlowCondition(State.Env.makeImplication(
+      State.Env.makeAnd(LHSKnownNull, RHSKnownNull), PointerEQ));
+  // nullptr != notnull
+  State.Env.addToFlowCondition(State.Env.makeImplication(
+      State.Env.makeAnd(LHSKnownNull, RHSKnownNotNull), PointerNE));
+  // notnull != nullptr
+  State.Env.addToFlowCondition(State.Env.makeImplication(
+      State.Env.makeAnd(LHSKnownNotNull, RHSKnownNull), PointerNE));
+}
+
+void transferFlowSensitiveNullCheckImplicitCastPtrToBool(
+    const Expr* CastExpr, const MatchFinder::MatchResult&,
+    TransferState<PointerNullabilityLattice>& State) {
+  auto* PointerVal =
+      getPointerValueFromExpr(CastExpr->IgnoreImplicit(), State.Env);
+  if (!PointerVal) return;
+
+  auto [PointerKnown, PointerNull] =
+      getPointerNullState(*PointerVal, State.Env);
+  auto& CastExprLoc = State.Env.createStorageLocation(*CastExpr);
+  State.Env.setValue(CastExprLoc, State.Env.makeNot(PointerNull));
+  State.Env.setStorageLocation(*CastExpr, CastExprLoc);
+}
+
+void transferFlowSensitiveCallExpr(
+    const CallExpr* CallExpr, const MatchFinder::MatchResult& Result,
+    TransferState<PointerNullabilityLattice>& State) {
+  // The dataflow framework itself does not create values for `CallExpr`s.
+  // However, we need these in some cases, so we produce them ourselves.
+
+  if (CallExpr->getType()->isAnyPointerType()) {
+    // Create a pointer so that we can attach nullability to it and have the
+    // nullability propagate with the pointer.
+    auto* PointerVal = getPointerValueFromExpr(CallExpr, State.Env);
+    if (!PointerVal) {
+      PointerVal =
+          cast<PointerValue>(State.Env.createValue(CallExpr->getType()));
+      auto& CallExprLoc = State.Env.createStorageLocation(*CallExpr);
+      State.Env.setValue(CallExprLoc, *PointerVal);
+      State.Env.setStorageLocation(*CallExpr, CallExprLoc);
+    }
+    initPointerFromAnnotations(*PointerVal, CallExpr, State);
+  } else if (CallExpr->isGLValue()) {
+    // The function returned a reference. Create a storage location for the
+    // expression so that if code creates a pointer from the reference, we will
+    // produce a `PointerValue`.
+    auto* Loc = State.Env.getStorageLocation(*CallExpr, SkipPast::None);
+    if (!Loc) {
+      // This is subtle: We call `createStorageLocation(QualType)`, not
+      // `createStorageLocation(const Expr &)`, so that we create a new
+      // storage location every time.
+      auto& NewLoc = State.Env.createStorageLocation(CallExpr->getType());
+      State.Env.setStorageLocation(*CallExpr, NewLoc);
+    }
+  }
+}
+
+void transferNonFlowSensitiveDeclRefExpr(
+    const DeclRefExpr* DRE, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(DRE, State, [&] {
+    return getNullabilityAnnotationsFromType(DRE->getType());
+  });
+}
+
+void transferNonFlowSensitiveMemberExpr(
+    const MemberExpr* ME, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(ME, State, [&]() {
+    auto BaseNullability = getNullabilityForChild(ME->getBase(), State);
+    QualType MemberType = ME->getType();
+    // When a MemberExpr is a part of a member function call
+    // (a child of CXXMemberCallExpr), the MemberExpr models a
+    // partially-applied member function, which isn't a real C++ construct.
+    // The AST does not provide rich type information for such MemberExprs.
+    // Instead, the AST specifies a placeholder type, specifically
+    // BuiltinType::BoundMember. So we have to look at the type of the member
+    // function declaration.
+    if (ME->hasPlaceholderType(BuiltinType::BoundMember)) {
+      MemberType = ME->getMemberDecl()->getType();
+    }
+    return substituteNullabilityAnnotationsInClassTemplate(
+        MemberType, BaseNullability, ME->getBase()->getType());
+  });
+}
+
+void transferNonFlowSensitiveMemberCallExpr(
+    const CXXMemberCallExpr* MCE, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(MCE, State, [&]() {
+    return getNullabilityForChild(MCE->getCallee(), State)
+        .take_front(countPointersInType(MCE))
+        .vec();
+  });
+}
+
+void transferNonFlowSensitiveCastExpr(
+    const CastExpr* CE, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(CE, State, [&]() -> std::vector<NullabilityKind> {
+    // Most casts that can convert ~unrelated types drop nullability in general.
+    // As a special case, preserve nullability of outer pointer types.
+    // For example, int* p; (void*)p; is a BitCast, but preserves nullability.
+    auto PreserveTopLevelPointers = [&](std::vector<NullabilityKind> V) {
+      auto ArgNullability = getNullabilityForChild(CE->getSubExpr(), State);
+      const PointerType* ArgType = dyn_cast<PointerType>(
+          CE->getSubExpr()->getType().getCanonicalType().getTypePtr());
+      const PointerType* CastType =
+          dyn_cast<PointerType>(CE->getType().getCanonicalType().getTypePtr());
+      for (int I = 0; ArgType && CastType; ++I) {
+        V[I] = ArgNullability[I];
+        ArgType = dyn_cast<PointerType>(ArgType->getPointeeType().getTypePtr());
+        CastType =
+            dyn_cast<PointerType>(CastType->getPointeeType().getTypePtr());
+      }
+      return V;
+    };
+
+    switch (CE->getCastKind()) {
+      // Casts between unrelated types: we can't say anything about nullability.
+      case CK_LValueBitCast:
+      case CK_BitCast:
+      case CK_LValueToRValueBitCast:
+        return PreserveTopLevelPointers(unspecifiedNullability(CE));
+
+      // Casts between equivalent types.
+      case CK_LValueToRValue:
+      case CK_NoOp:
+      case CK_AtomicToNonAtomic:
+      case CK_NonAtomicToAtomic:
+      case CK_AddressSpaceConversion:
+        return getNullabilityForChild(CE->getSubExpr(), State).vec();
+
+      // Controlled conversions between types
+      // TODO: these should be doable somehow
+      case CK_BaseToDerived:
+      case CK_DerivedToBase:
+      case CK_UncheckedDerivedToBase:
+        return PreserveTopLevelPointers(unspecifiedNullability(CE));
+      case CK_UserDefinedConversion:
+      case CK_ConstructorConversion:
+        return unspecifiedNullability(CE);
+
+      case CK_Dynamic: {
+        auto Result = unspecifiedNullability(CE);
+        // A dynamic_cast to pointer is null if the runtime check fails.
+        if (isa<PointerType>(CE->getType().getCanonicalType()))
+          Result.front() = NullabilityKind::Nullable;
+        return Result;
+      }
+
+      // Primitive values have no nullability.
+      case CK_ToVoid:
+      case CK_MemberPointerToBoolean:
+      case CK_PointerToBoolean:
+      case CK_PointerToIntegral:
+      case CK_IntegralCast:
+      case CK_IntegralToBoolean:
+      case CK_IntegralToFloating:
+      case CK_FloatingToFixedPoint:
+      case CK_FixedPointToFloating:
+      case CK_FixedPointCast:
+      case CK_FixedPointToIntegral:
+      case CK_IntegralToFixedPoint:
+      case CK_FixedPointToBoolean:
+      case CK_FloatingToIntegral:
+      case CK_FloatingToBoolean:
+      case CK_BooleanToSignedIntegral:
+      case CK_FloatingCast:
+      case CK_FloatingRealToComplex:
+      case CK_FloatingComplexToReal:
+      case CK_FloatingComplexToBoolean:
+      case CK_FloatingComplexCast:
+      case CK_FloatingComplexToIntegralComplex:
+      case CK_IntegralRealToComplex:
+      case CK_IntegralComplexToReal:
+      case CK_IntegralComplexToBoolean:
+      case CK_IntegralComplexCast:
+      case CK_IntegralComplexToFloatingComplex:
+        return {};
+
+      // This can definitely be null!
+      case CK_NullToPointer: {
+        auto Nullability = getNullabilityAnnotationsFromType(CE->getType());
+        Nullability.front() = NullabilityKind::Nullable;
+        return Nullability;
+      }
+
+      // Pointers out of thin air, who knows?
+      case CK_IntegralToPointer:
+        return unspecifiedNullability(CE);
+
+      // Decayed objects are never null.
+      case CK_ArrayToPointerDecay:
+      case CK_FunctionToPointerDecay:
+      case CK_BuiltinFnToFnPtr:
+        return prepend(NullabilityKind::NonNull,
+                       getNullabilityForChild(CE->getSubExpr(), State));
+
+      // TODO: what is our model of member pointers?
+      case CK_BaseToDerivedMemberPointer:
+      case CK_DerivedToBaseMemberPointer:
+      case CK_NullToMemberPointer:
+      case CK_ReinterpretMemberPointer:
+      case CK_ToUnion:  // and unions?
+        return unspecifiedNullability(CE);
+
+      // TODO: Non-C/C++ constructs, do we care about these?
+      case CK_CPointerToObjCPointerCast:
+      case CK_ObjCObjectLValueCast:
+      case CK_MatrixCast:
+      case CK_VectorSplat:
+      case CK_BlockPointerToObjCPointerCast:
+      case CK_AnyPointerToBlockPointerCast:
+      case CK_ARCProduceObject:
+      case CK_ARCConsumeObject:
+      case CK_ARCReclaimReturnedObject:
+      case CK_ARCExtendBlockObject:
+      case CK_CopyAndAutoreleaseBlockObject:
+      case CK_ZeroToOCLOpaqueType:
+      case CK_IntToOCLSampler:
+        return unspecifiedNullability(CE);
+
+      case CK_Dependent:
+        CHECK(false) << "Shouldn't see dependent casts here?";
+    }
+  });
+}
+
+void transferNonFlowSensitiveMaterializeTemporaryExpr(
+    const MaterializeTemporaryExpr* MTE, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(MTE, State, [&]() {
+    return getNullabilityForChild(MTE->getSubExpr(), State).vec();
+  });
+}
+
+void transferNonFlowSensitiveCallExpr(
+    const CallExpr* CE, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  // TODO: Check CallExpr arguments in the diagnoser against the nullability of
+  // parameters.
+  computeNullability(CE, State, [&]() {
+    // TODO(mboehme): Instead of relying on Clang to propagate nullability sugar
+    // to the `CallExpr`'s type, we should extract nullability directly from the
+    // callee `Expr .
+    return substituteNullabilityAnnotationsInFunctionTemplate(CE->getType(),
+                                                              CE);
+  });
+}
+
+void transferNonFlowSensitiveUnaryOperator(
+    const UnaryOperator* UO, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(UO, State, [&]() -> std::vector<NullabilityKind> {
+    switch (UO->getOpcode()) {
+      case UO_AddrOf:
+        return prepend(NullabilityKind::NonNull,
+                       getNullabilityForChild(UO->getSubExpr(), State));
+      case UO_Deref:
+        return getNullabilityForChild(UO->getSubExpr(), State)
+            .drop_front()
+            .vec();
+
+      case UO_PostInc:
+      case UO_PostDec:
+      case UO_PreInc:
+      case UO_PreDec:
+      case UO_Plus:
+      case UO_Minus:
+      case UO_Not:
+      case UO_LNot:
+      case UO_Real:
+      case UO_Imag:
+      case UO_Extension:
+        return getNullabilityForChild(UO->getSubExpr(), State);
+
+      case UO_Coawait:
+        // TODO: work out what to do here!
+        return unspecifiedNullability(UO);
+    }
+  });
+}
+
+void transferNonFlowSensitiveNewExpr(
+    const CXXNewExpr* NE, const MatchFinder::MatchResult& MR,
+    TransferState<PointerNullabilityLattice>& State) {
+  computeNullability(NE, State, [&]() {
+    std::vector<NullabilityKind> result =
+        getNullabilityAnnotationsFromType(NE->getType());
+    result.front() = NE->shouldNullCheckAllocation() ? NullabilityKind::Nullable
+                                                     : NullabilityKind::NonNull;
+    return result;
+  });
+}
+
+auto buildNonFlowSensitiveTransferer() {
+  return CFGMatchSwitchBuilder<TransferState<PointerNullabilityLattice>>()
+      .CaseOfCFGStmt<DeclRefExpr>(ast_matchers::declRefExpr(),
+                                  transferNonFlowSensitiveDeclRefExpr)
+      .CaseOfCFGStmt<MemberExpr>(ast_matchers::memberExpr(),
+                                 transferNonFlowSensitiveMemberExpr)
+      .CaseOfCFGStmt<CXXMemberCallExpr>(ast_matchers::cxxMemberCallExpr(),
+                                        transferNonFlowSensitiveMemberCallExpr)
+      .CaseOfCFGStmt<CastExpr>(ast_matchers::castExpr(),
+                               transferNonFlowSensitiveCastExpr)
+      .CaseOfCFGStmt<MaterializeTemporaryExpr>(
+          ast_matchers::materializeTemporaryExpr(),
+          transferNonFlowSensitiveMaterializeTemporaryExpr)
+      .CaseOfCFGStmt<CallExpr>(ast_matchers::callExpr(),
+                               transferNonFlowSensitiveCallExpr)
+      .CaseOfCFGStmt<UnaryOperator>(ast_matchers::unaryOperator(),
+                                    transferNonFlowSensitiveUnaryOperator)
+      .CaseOfCFGStmt<CXXNewExpr>(ast_matchers::cxxNewExpr(),
+                                 transferNonFlowSensitiveNewExpr)
+      .Build();
+}
+
+auto buildFlowSensitiveTransferer() {
+  return CFGMatchSwitchBuilder<TransferState<PointerNullabilityLattice>>()
+      // Handles initialization of the null states of pointers.
+      .CaseOfCFGStmt<Expr>(isCXXThisExpr(), transferFlowSensitiveNotNullPointer)
+      .CaseOfCFGStmt<Expr>(isAddrOf(), transferFlowSensitiveNotNullPointer)
+      .CaseOfCFGStmt<Expr>(isNullPointerLiteral(),
+                           transferFlowSensitiveNullPointer)
+      .CaseOfCFGStmt<CallExpr>(isCallExpr(), transferFlowSensitiveCallExpr)
+      .CaseOfCFGStmt<Expr>(isPointerExpr(), transferFlowSensitivePointer)
+      // Handles comparison between 2 pointers.
+      .CaseOfCFGStmt<BinaryOperator>(isPointerCheckBinOp(),
+                                     transferFlowSensitiveNullCheckComparison)
+      // Handles checking of pointer as boolean.
+      .CaseOfCFGStmt<Expr>(isImplicitCastPointerToBool(),
+                           transferFlowSensitiveNullCheckImplicitCastPtrToBool)
+      .Build();
+}
+}  // namespace
+
+PointerNullabilityAnalysis::PointerNullabilityAnalysis(ASTContext& Context)
+    : DataflowAnalysis<PointerNullabilityAnalysis, PointerNullabilityLattice>(
+          Context),
+      NonFlowSensitiveTransferer(buildNonFlowSensitiveTransferer()),
+      FlowSensitiveTransferer(buildFlowSensitiveTransferer()) {}
+
+void PointerNullabilityAnalysis::transfer(const CFGElement& Elt,
+                                          PointerNullabilityLattice& Lattice,
+                                          Environment& Env) {
+  TransferState<PointerNullabilityLattice> State(Lattice, Env);
+  NonFlowSensitiveTransferer(Elt, getASTContext(), State);
+  FlowSensitiveTransferer(Elt, getASTContext(), State);
+}
+
+BoolValue& mergeBoolValues(BoolValue& Bool1, const Environment& Env1,
+                           BoolValue& Bool2, const Environment& Env2,
+                           Environment& MergedEnv) {
+  if (&Bool1 == &Bool2) {
+    return Bool1;
+  }
+
+  auto& MergedBool = MergedEnv.makeAtomicBoolValue();
+
+  // If `Bool1` and `Bool2` is constrained to the same true / false value,
+  // `MergedBool` can be constrained similarly without needing to consider the
+  // path taken - this simplifies the flow condition tracked in `MergedEnv`.
+  // Otherwise, information about which path was taken is used to associate
+  // `MergedBool` with `Bool1` and `Bool2`.
+  if (Env1.flowConditionImplies(Bool1) && Env2.flowConditionImplies(Bool2)) {
+    MergedEnv.addToFlowCondition(MergedBool);
+  } else if (Env1.flowConditionImplies(Env1.makeNot(Bool1)) &&
+             Env2.flowConditionImplies(Env2.makeNot(Bool2))) {
+    MergedEnv.addToFlowCondition(MergedEnv.makeNot(MergedBool));
+  } else {
+    // TODO(b/233582219): Flow conditions are not necessarily mutually
+    // exclusive, a fix is in order: https://reviews.llvm.org/D130270. Update
+    // this section when the patch is commited.
+    auto& FC1 = Env1.getFlowConditionToken();
+    auto& FC2 = Env2.getFlowConditionToken();
+    MergedEnv.addToFlowCondition(MergedEnv.makeOr(
+        MergedEnv.makeAnd(FC1, MergedEnv.makeIff(MergedBool, Bool1)),
+        MergedEnv.makeAnd(FC2, MergedEnv.makeIff(MergedBool, Bool2))));
+  }
+  return MergedBool;
+}
+
+bool PointerNullabilityAnalysis::merge(QualType Type, const Value& Val1,
+                                       const Environment& Env1,
+                                       const Value& Val2,
+                                       const Environment& Env2,
+                                       Value& MergedVal,
+                                       Environment& MergedEnv) {
+  if (!Type->isAnyPointerType()) {
+    return false;
+  }
+
+  auto [Known1, Null1] = getPointerNullState(cast<PointerValue>(Val1), Env1);
+  auto [Known2, Null2] = getPointerNullState(cast<PointerValue>(Val2), Env2);
+
+  auto& Known = mergeBoolValues(Known1, Env1, Known2, Env2, MergedEnv);
+  auto& Null = mergeBoolValues(Null1, Env1, Null2, Env2, MergedEnv);
+
+  initPointerNullState(cast<PointerValue>(MergedVal), MergedEnv, &Known, &Null);
+
+  return true;
+}
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/pointer_nullability_analysis.h b/nullability/pointer_nullability_analysis.h
new file mode 100644
index 0000000..0b88497
--- /dev/null
+++ b/nullability/pointer_nullability_analysis.h
@@ -0,0 +1,57 @@
+// 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_POINTER_NULLABILITY_ANALYSIS_H_
+#define CRUBIT_NULLABILITY_POINTER_NULLABILITY_ANALYSIS_H_
+
+#include "nullability/pointer_nullability_lattice.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Type.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/Value.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+/// Analyses constructs in the source code to collect nullability information
+/// about pointers at each program point.
+class PointerNullabilityAnalysis
+    : public dataflow::DataflowAnalysis<PointerNullabilityAnalysis,
+                                        PointerNullabilityLattice> {
+ private:
+  absl::flat_hash_map<const Expr*, std::vector<NullabilityKind>>
+      ExprToNullability;
+
+ public:
+  explicit PointerNullabilityAnalysis(ASTContext& context);
+
+  PointerNullabilityLattice initialElement() {
+    return PointerNullabilityLattice(ExprToNullability);
+  }
+
+  void transfer(const CFGElement& Elt, PointerNullabilityLattice& Lattice,
+                dataflow::Environment& Env);
+
+  bool merge(QualType Type, const dataflow::Value& Val1,
+             const dataflow::Environment& Env1, const dataflow::Value& Val2,
+             const dataflow::Environment& Env2, dataflow::Value& MergedVal,
+             dataflow::Environment& MergedEnv) override;
+
+ private:
+  // Applies non-flow-sensitive transfer functions on statements
+  dataflow::CFGMatchSwitch<dataflow::TransferState<PointerNullabilityLattice>>
+      NonFlowSensitiveTransferer;
+
+  // Applies flow-sensitive transfer functions on statements
+  dataflow::CFGMatchSwitch<dataflow::TransferState<PointerNullabilityLattice>>
+      FlowSensitiveTransferer;
+};
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // CRUBIT_NULLABILITY_POINTER_NULLABILITY_ANALYSIS_H_
diff --git a/nullability/pointer_nullability_diagnosis.cc b/nullability/pointer_nullability_diagnosis.cc
new file mode 100644
index 0000000..8289142
--- /dev/null
+++ b/nullability/pointer_nullability_diagnosis.cc
@@ -0,0 +1,312 @@
+// 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_diagnosis.h"
+
+#include <optional>
+#include <string>
+
+#include "nullability/pointer_nullability.h"
+#include "nullability/pointer_nullability_matchers.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Stmt.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Basic/Specifiers.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using ast_matchers::MatchFinder;
+using dataflow::CFGMatchSwitchBuilder;
+using dataflow::Environment;
+using dataflow::TransferStateForDiagnostics;
+
+namespace {
+
+// Returns true if `Expr` is uninterpreted or known to be nullable.
+bool isNullableOrUntracked(const Expr* E, const Environment& Env) {
+  auto* ActualVal = getPointerValueFromExpr(E, Env);
+  if (ActualVal == nullptr) {
+    llvm::dbgs()
+        << "The dataflow analysis framework does not model a PointerValue for "
+           "the following Expr, and thus its dereference is marked as "
+           "unsafe:\n";
+    E->dump();
+  }
+  return !ActualVal || isNullable(*ActualVal, Env);
+}
+
+// Returns true if an uninterpreted or nullable `Expr` was assigned to a
+// construct with a non-null `DeclaredType`.
+bool isIncompatibleAssignment(QualType DeclaredType, const Expr* E,
+                              const Environment& Env, ASTContext& Ctx) {
+  CHECK(DeclaredType->isAnyPointerType());
+  return getNullabilityKind(DeclaredType, Ctx) == NullabilityKind::NonNull &&
+         isNullableOrUntracked(E, Env);
+}
+
+std::optional<CFGElement> diagnoseDereference(
+    const UnaryOperator* UnaryOp, const MatchFinder::MatchResult&,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State) {
+  if (isNullableOrUntracked(UnaryOp->getSubExpr(), State.Env)) {
+    return std::optional<CFGElement>(CFGStmt(UnaryOp));
+  }
+  return std::nullopt;
+}
+
+std::optional<CFGElement> diagnoseArrow(
+    const MemberExpr* MemberExpr, const MatchFinder::MatchResult& Result,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State) {
+  if (isNullableOrUntracked(MemberExpr->getBase(), State.Env)) {
+    return std::optional<CFGElement>(CFGStmt(MemberExpr));
+  }
+  return std::nullopt;
+}
+
+bool isIncompatibleArgumentList(ArrayRef<QualType> ParamTypes,
+                                ArrayRef<const Expr*> Args,
+                                const Environment& Env, ASTContext& Ctx) {
+  CHECK_EQ(ParamTypes.size(), Args.size());
+  for (unsigned int I = 0; I < Args.size(); ++I) {
+    auto ParamType = ParamTypes[I].getNonReferenceType();
+    if (!ParamType->isAnyPointerType()) {
+      continue;
+    }
+    if (isIncompatibleAssignment(ParamType, Args[I], Env, Ctx)) {
+      return true;
+    }
+  }
+  return false;
+}
+
+NullabilityKind parseNullabilityKind(StringRef EnumName) {
+  return llvm::StringSwitch<NullabilityKind>(EnumName)
+      .Case("NK_nonnull", NullabilityKind::NonNull)
+      .Case("NK_nullable", NullabilityKind::Nullable)
+      .Case("NK_unspecified", NullabilityKind::Unspecified)
+      .Default(NullabilityKind::Unspecified);
+}
+
+/// Evaluates the `__assert_nullability` call by comparing the expected
+/// nullability to the nullability computed by the dataflow analysis.
+///
+/// If the function being diagnosed is called `__assert_nullability`, we assume
+/// it is a call of the shape __assert_nullability<a, b, c, ...>(p), where `p`
+/// is an expression that contains pointers and a, b, c ... represent each of
+/// the NullabilityKinds in `p`'s expected nullability. An expression's
+/// nullability can be expressed as a vector of NullabilityKinds, where each
+/// vector element corresponds to one of the pointers contained in the
+/// expression.
+///
+/// For example:
+/// \code
+///    enum NullabilityKind {
+///      NK_nonnull,
+///      NK_nullable,
+///      NK_unspecified,
+///    };
+///
+///    template<NullabilityKind ...NK, typename T>
+///    void __assert_nullability(T&);
+///
+///    template<typename T0, typename T1>
+///    struct Struct2Arg {
+///      T0 arg0;
+///      T1 arg1;
+///    };
+///
+///    void target(Struct2Arg<int *, int * _Nullable> p) {
+///      __assert_nullability<NK_unspecified, NK_nullable>(p);
+///    }
+/// \endcode
+bool diagnoseAssertNullabilityCall(
+    const CallExpr* CE,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State,
+    ASTContext& Ctx) {
+  auto* DRE = cast<DeclRefExpr>(CE->getCallee()->IgnoreImpCasts());
+
+  // Extract the expected nullability from the template parameter pack.
+  std::vector<NullabilityKind> Expected;
+  for (auto P : DRE->template_arguments()) {
+    if (P.getArgument().getKind() == TemplateArgument::Expression) {
+      if (auto* EnumDRE = dyn_cast<DeclRefExpr>(P.getSourceExpression())) {
+        Expected.push_back(parseNullabilityKind(EnumDRE->getDecl()->getName()));
+      }
+    }
+  }
+
+  // Compare the nullability computed by nullability analysis with the
+  // expected one.
+  const Expr* GivenExpr = CE->getArg(0);
+  std::optional<ArrayRef<NullabilityKind>> MaybeComputed =
+      State.Lattice.getExprNullability(GivenExpr);
+  if (!MaybeComputed.has_value()) {
+    llvm::dbgs()
+        << "Could not evaluate __assert_nullability. Could not find the "
+           "nullability of the argument expression: ";
+    CE->dump();
+    return false;
+  }
+  if (MaybeComputed->vec() == Expected) return true;
+  // The computed and expected nullabilities differ. Print both to aid
+  // debugging.
+  llvm::dbgs() << "__assert_nullability failed at location: ";
+  CE->getExprLoc().print(llvm::dbgs(), Ctx.getSourceManager());
+  llvm::dbgs() << "\nExpression:\n";
+  GivenExpr->dump();
+  llvm::dbgs() << "Expected nullability: ";
+  llvm::dbgs() << nullabilityToString(Expected) << "\n";
+  llvm::dbgs() << "Computed nullability: ";
+  llvm::dbgs() << nullabilityToString(*MaybeComputed) << "\n";
+  return false;
+}
+
+std::optional<CFGElement> diagnoseCallExpr(
+    const CallExpr* CE, const MatchFinder::MatchResult& Result,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State) {
+  // Emit a warning for a nullable callee. We don't do this for member functions
+  // because in this case the callee can't be null. If we're calling a
+  // pointer-to-member-function, the callee is a `.*` or `->*` `BinaryOperator`,
+  // which itself can never be null. A nullable pointer-to-member-function will
+  // manifest as a nullable RHS of this `BinaryOperator` and should be diagnosed
+  // there.
+  if (!isa<CXXMemberCallExpr>(CE) &&
+      isNullableOrUntracked(CE->getCallee(), State.Env)) {
+    return std::optional<CFGElement>(CFGStmt(CE->getCallee()));
+  }
+
+  if (auto* FD = CE->getDirectCallee()) {
+    if (FD->getDeclName().isIdentifier() &&
+        FD->getName() == "__assert_nullability" &&
+        !diagnoseAssertNullabilityCall(CE, State, *Result.Context)) {
+      // TODO: Handle __assert_nullability failures differently from regular
+      // diagnostic ([[unsafe]]) failures.
+      return std::optional<CFGElement>(CFGStmt(CE));
+    }
+  }
+
+  auto* Callee = CE->getCalleeDecl();
+  // TODO(mboehme): Retrieve the nullability directly from the callee using
+  // `getNullabilityForChild(CE->getCallee())`, as what we have here now
+  // doesn't work for callees that don't have a decl.
+  if (!Callee) return std::nullopt;
+
+  auto* CalleeType = Callee->getFunctionType();
+  if (!CalleeType) return std::nullopt;
+
+  // TODO(mboehme): We're only looking at the nullability spelled on the
+  // `FunctionProtoType`, but there could be extra information in the callee.
+  // An example (due to sammccall@):
+  //
+  // template <typename T> struct Sink {
+  //   static void eat(T) { ... }
+  // }
+  // void target(Sink<Nonnull<int*>> &S) {
+  //   S<Nonnull<int*>>::eat(nullptr); // no warning
+  //   // callee is instantiated Sink<int*>::eat(int*)
+  //   // however nullability vector of DRE S::eat should be [Nonnull]
+  //   // (not sure if it is today)
+  // }
+  auto* CalleeFPT = CalleeType->getAs<FunctionProtoType>();
+  if (!CalleeFPT) return std::nullopt;
+
+  auto ParamTypes = CalleeFPT->getParamTypes();
+  ArrayRef<const Expr*> Args(CE->getArgs(), CE->getNumArgs());
+  // The first argument of an member operator call expression is the implicit
+  // object argument, which does not appear in the list of parameter types.
+  // Note that operator calls always have a direct callee.
+  if (isa<CXXOperatorCallExpr>(CE) &&
+      isa<CXXMethodDecl>(CE->getDirectCallee())) {
+    Args = Args.drop_front();
+  }
+  if (CalleeFPT->isVariadic()) {
+    CHECK_GE(Args.size(), ParamTypes.size());
+    Args = Args.take_front(ParamTypes.size());
+  }
+
+  return isIncompatibleArgumentList(ParamTypes, Args, State.Env,
+                                    *Result.Context)
+             ? std::optional<CFGElement>(CFGStmt(CE))
+             : std::nullopt;
+}
+
+std::optional<CFGElement> diagnoseConstructExpr(
+    const CXXConstructExpr* CE, const MatchFinder::MatchResult& Result,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State) {
+  auto ConstructorParamTypes = CE->getConstructor()
+                                   ->getType()
+                                   ->getAs<FunctionProtoType>()
+                                   ->getParamTypes();
+  ArrayRef<const Expr*> ConstructorArgs(CE->getArgs(), CE->getNumArgs());
+  return isIncompatibleArgumentList(ConstructorParamTypes, ConstructorArgs,
+                                    State.Env, *Result.Context)
+             ? std::optional<CFGElement>(CFGStmt(CE))
+             : std::nullopt;
+}
+
+std::optional<CFGElement> diagnoseReturn(
+    const ReturnStmt* RS, const MatchFinder::MatchResult& Result,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State) {
+  auto ReturnType = cast<FunctionDecl>(State.Env.getDeclCtx())->getReturnType();
+
+  // TODO: Handle non-pointer return types.
+  if (!ReturnType->isPointerType()) {
+    return std::nullopt;
+  }
+
+  auto* ReturnExpr = RS->getRetValue();
+  CHECK(ReturnExpr->getType()->isPointerType());
+
+  return isIncompatibleAssignment(ReturnType, ReturnExpr, State.Env,
+                                  *Result.Context)
+             ? std::optional<CFGElement>(CFGStmt(RS))
+             : std::nullopt;
+}
+
+std::optional<CFGElement> diagnoseMemberInitializer(
+    const CXXCtorInitializer* CI, const MatchFinder::MatchResult& Result,
+    const TransferStateForDiagnostics<PointerNullabilityLattice>& State) {
+  CHECK(CI->isAnyMemberInitializer());
+  auto MemberType = CI->getAnyMember()->getType();
+  if (!MemberType->isAnyPointerType()) {
+    return std::nullopt;
+  }
+  auto MemberInitExpr = CI->getInit();
+  return isIncompatibleAssignment(MemberType, MemberInitExpr, State.Env,
+                                  *Result.Context)
+             ? std::optional<CFGElement>(CFGInitializer(CI))
+             : std::nullopt;
+}
+
+auto buildDiagnoser() {
+  return CFGMatchSwitchBuilder<const dataflow::TransferStateForDiagnostics<
+                                   PointerNullabilityLattice>,
+                               std::optional<CFGElement>>()
+      // (*)
+      .CaseOfCFGStmt<UnaryOperator>(isPointerDereference(), diagnoseDereference)
+      // (->)
+      .CaseOfCFGStmt<MemberExpr>(isPointerArrow(), diagnoseArrow)
+      // Check compatibility of parameter assignments
+      .CaseOfCFGStmt<CallExpr>(isCallExpr(), diagnoseCallExpr)
+      .CaseOfCFGStmt<ReturnStmt>(isPointerReturn(), diagnoseReturn)
+      .CaseOfCFGStmt<CXXConstructExpr>(isConstructExpr(), diagnoseConstructExpr)
+      .CaseOfCFGInit<CXXCtorInitializer>(isCtorMemberInitializer(),
+                                         diagnoseMemberInitializer)
+      .Build();
+}
+
+}  // namespace
+
+PointerNullabilityDiagnoser::PointerNullabilityDiagnoser()
+    : Diagnoser(buildDiagnoser()) {}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/pointer_nullability_diagnosis.h b/nullability/pointer_nullability_diagnosis.h
new file mode 100644
index 0000000..57cf6a1
--- /dev/null
+++ b/nullability/pointer_nullability_diagnosis.h
@@ -0,0 +1,53 @@
+// 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_POINTER_NULLABILITY_DIAGNOSIS_H_
+#define CRUBIT_NULLABILITY_POINTER_NULLABILITY_DIAGNOSIS_H_
+
+#include <optional>
+
+#include "nullability/pointer_nullability_lattice.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Stmt.h"
+#include "clang/Analysis/FlowSensitive/CFGMatchSwitch.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.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.
+  std::optional<CFGElement> diagnose(
+      const CFGElement* Elt, ASTContext& Ctx,
+      const dataflow::TransferStateForDiagnostics<PointerNullabilityLattice>&
+          State) {
+    return Diagnoser(*Elt, Ctx, State);
+  }
+
+ private:
+  dataflow::CFGMatchSwitch<
+      const dataflow::TransferStateForDiagnostics<PointerNullabilityLattice>,
+      std::optional<CFGElement>>
+      Diagnoser;
+};
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // CRUBIT_NULLABILITY_POINTER_NULLABILITY_DIAGNOSIS_H_
diff --git a/nullability/pointer_nullability_lattice.h b/nullability/pointer_nullability_lattice.h
new file mode 100644
index 0000000..66222c5
--- /dev/null
+++ b/nullability/pointer_nullability_lattice.h
@@ -0,0 +1,75 @@
+// 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 THIRD_PARTY_CRUBIT_NULLABILITY_POINTER_NULLABILITY_LATTICE_H_
+#define THIRD_PARTY_CRUBIT_NULLABILITY_POINTER_NULLABILITY_LATTICE_H_
+
+#include <optional>
+#include <ostream>
+
+#include "absl/container/flat_hash_map.h"
+#include "absl/log/check.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysisContext.h"
+#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+class PointerNullabilityLattice {
+ private:
+  // Owned by the PointerNullabilityAnalysis object, shared by all lattice
+  // elements within one analysis run.
+  absl::flat_hash_map<const Expr *, std::vector<NullabilityKind>>
+      &ExprToNullability;
+
+ public:
+  PointerNullabilityLattice(
+      absl::flat_hash_map<const Expr *, std::vector<NullabilityKind>>
+          &ExprToNullability)
+      : ExprToNullability(ExprToNullability) {}
+
+  std::optional<ArrayRef<NullabilityKind>> getExprNullability(
+      const Expr *E) const {
+    auto I = ExprToNullability.find(&dataflow::ignoreCFGOmittedNodes(*E));
+    return I == ExprToNullability.end()
+               ? std::nullopt
+               : std::optional<ArrayRef<NullabilityKind>>(I->second);
+  }
+
+  // If the `ExprToNullability` map already contains an entry for `E`, does
+  // nothing. Otherwise, inserts a new entry with key `E` and value computed by
+  // the provided GetNullability.
+  // Returns the (cached or computed) nullability.
+  ArrayRef<NullabilityKind> insertExprNullabilityIfAbsent(
+      const Expr *E,
+      const std::function<std::vector<NullabilityKind>()> &GetNullability) {
+    E = &dataflow::ignoreCFGOmittedNodes(*E);
+    if (auto It = ExprToNullability.find(E); It != ExprToNullability.end())
+      return It->second;
+    // Deliberately perform a separate lookup after calling GetNullability.
+    // It may invalidate iterators, e.g. inserting missing vectors for children.
+    auto [Iterator, Inserted] = ExprToNullability.insert({E, GetNullability()});
+    CHECK(Inserted) << "GetNullability inserted same " << E->getStmtClassName();
+    return Iterator->second;
+  }
+
+  bool operator==(const PointerNullabilityLattice &Other) const { return true; }
+
+  dataflow::LatticeJoinEffect join(const PointerNullabilityLattice &Other) {
+    return dataflow::LatticeJoinEffect::Unchanged;
+  }
+};
+
+inline std::ostream &operator<<(std::ostream &OS,
+                                const PointerNullabilityLattice &) {
+  return OS << "noop";
+}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // THIRD_PARTY_CRUBIT_NULLABILITY_POINTER_NULLABILITY_LATTICE_H_
diff --git a/nullability/pointer_nullability_matchers.cc b/nullability/pointer_nullability_matchers.cc
new file mode 100644
index 0000000..e2416c5
--- /dev/null
+++ b/nullability/pointer_nullability_matchers.cc
@@ -0,0 +1,70 @@
+// 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/AST/OperationKinds.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+using ast_matchers::anyOf;
+using ast_matchers::binaryOperator;
+using ast_matchers::callExpr;
+using ast_matchers::cxxConstructExpr;
+using ast_matchers::cxxCtorInitializer;
+using ast_matchers::cxxThisExpr;
+using ast_matchers::declRefExpr;
+using ast_matchers::expr;
+using ast_matchers::hasAnyOperatorName;
+using ast_matchers::hasCastKind;
+using ast_matchers::hasOperands;
+using ast_matchers::hasOperatorName;
+using ast_matchers::hasReturnValue;
+using ast_matchers::hasType;
+using ast_matchers::hasUnaryOperand;
+using ast_matchers::implicitCastExpr;
+using ast_matchers::isAnyPointer;
+using ast_matchers::isArrow;
+using ast_matchers::isMemberInitializer;
+using ast_matchers::memberExpr;
+using ast_matchers::returnStmt;
+using ast_matchers::unaryOperator;
+using ast_matchers::internal::Matcher;
+
+Matcher<Stmt> isPointerExpr() { return expr(hasType(isAnyPointer())); }
+Matcher<Stmt> isNullPointerLiteral() {
+  return implicitCastExpr(anyOf(hasCastKind(CK_NullToPointer),
+                                hasCastKind(CK_NullToMemberPointer)));
+}
+Matcher<Stmt> isAddrOf() { return unaryOperator(hasOperatorName("&")); }
+Matcher<Stmt> isPointerDereference() {
+  return unaryOperator(hasOperatorName("*"), hasUnaryOperand(isPointerExpr()));
+}
+Matcher<Stmt> isPointerCheckBinOp() {
+  return binaryOperator(hasAnyOperatorName("!=", "=="),
+                        hasOperands(isPointerExpr(), isPointerExpr()));
+}
+Matcher<Stmt> isImplicitCastPointerToBool() {
+  return implicitCastExpr(hasCastKind(CK_PointerToBoolean));
+}
+Matcher<Stmt> isMemberOfPointerType() {
+  return memberExpr(hasType(isAnyPointer()));
+}
+Matcher<Stmt> isPointerArrow() { return memberExpr(isArrow()); }
+Matcher<Stmt> isCXXThisExpr() { return cxxThisExpr(); }
+Matcher<Stmt> isCallExpr() { return callExpr(); }
+Matcher<Stmt> isPointerReturn() {
+  return returnStmt(hasReturnValue(hasType(isAnyPointer())));
+}
+Matcher<Stmt> isConstructExpr() { return cxxConstructExpr(); }
+Matcher<CXXCtorInitializer> isCtorMemberInitializer() {
+  return cxxCtorInitializer(isMemberInitializer());
+}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/pointer_nullability_matchers.h b/nullability/pointer_nullability_matchers.h
new file mode 100644
index 0000000..353bbe8
--- /dev/null
+++ b/nullability/pointer_nullability_matchers.h
@@ -0,0 +1,32 @@
+// 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_POINTER_NULLABILITY_MATCHERS_H_
+#define CRUBIT_NULLABILITY_POINTER_NULLABILITY_MATCHERS_H_
+
+#include "clang/ASTMatchers/ASTMatchersInternal.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+ast_matchers::internal::Matcher<Stmt> isPointerExpr();
+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();
+ast_matchers::internal::Matcher<Stmt> isPointerDereference();
+ast_matchers::internal::Matcher<Stmt> isPointerCheckBinOp();
+ast_matchers::internal::Matcher<Stmt> isImplicitCastPointerToBool();
+ast_matchers::internal::Matcher<Stmt> isCallExpr();
+ast_matchers::internal::Matcher<Stmt> isPointerReturn();
+ast_matchers::internal::Matcher<Stmt> isConstructExpr();
+ast_matchers::internal::Matcher<CXXCtorInitializer> isCtorMemberInitializer();
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // CRUBIT_NULLABILITY_POINTER_NULLABILITY_MATCHERS_H_
diff --git a/nullability/pointer_nullability_test.cc b/nullability/pointer_nullability_test.cc
new file mode 100644
index 0000000..89a6bbc
--- /dev/null
+++ b/nullability/pointer_nullability_test.cc
@@ -0,0 +1,288 @@
+// 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.h"
+
+#include "clang/Testing/TestAST.h"
+#include "llvm/ADT/StringRef.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googlemock/include/gmock/gmock.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang::tidy::nullability {
+namespace {
+using testing::ElementsAre;
+
+class GetNullabilityAnnotationsFromTypeTest : public ::testing::Test {
+ protected:
+  // C++ declarations prepended before parsing type in nullVec().
+  std::string Preamble;
+
+  // Parses `Type` and returns getNullabilityAnnotationsFromType().
+  std::vector<NullabilityKind> nullVec(llvm::StringRef Type) {
+    clang::TestAST AST((Preamble + "\nusing Target = " + Type + ";").str());
+    auto Target = AST.context().getTranslationUnitDecl()->lookup(
+        &AST.context().Idents.get("Target"));
+    CHECK(Target.isSingleResult());
+    QualType TargetType =
+        AST.context().getTypedefType(Target.find_first<TypeAliasDecl>());
+    return getNullabilityAnnotationsFromType(TargetType);
+  }
+};
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, Pointers) {
+  EXPECT_THAT(nullVec("int"), ElementsAre());
+  EXPECT_THAT(nullVec("int *"), ElementsAre(NullabilityKind::Unspecified));
+  EXPECT_THAT(nullVec("int **"), ElementsAre(NullabilityKind::Unspecified,
+                                             NullabilityKind::Unspecified));
+  EXPECT_THAT(nullVec("int *_Nullable*_Nonnull"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, Sugar) {
+  Preamble = "using X = int* _Nonnull;";
+
+  EXPECT_THAT(nullVec("X"), ElementsAre(NullabilityKind::NonNull));
+  EXPECT_THAT(nullVec("X*"), ElementsAre(NullabilityKind::Unspecified,
+                                         NullabilityKind::NonNull));
+
+  EXPECT_THAT(nullVec("X(*)"), ElementsAre(NullabilityKind::Unspecified,
+                                           NullabilityKind::NonNull));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, AliasTemplates) {
+  Preamble = R"cpp(
+    template <typename T>
+    using Nullable = T _Nullable;
+    template <typename T>
+    using Nonnull = T _Nonnull;
+  )cpp";
+  EXPECT_THAT(nullVec("Nullable<int*>"),
+              ElementsAre(NullabilityKind::Nullable));
+
+  EXPECT_THAT(
+      nullVec("Nullable<Nullable<int*>*>"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable));
+
+  EXPECT_THAT(nullVec("Nullable<Nullable<Nonnull<int*>*>*>"),
+              ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable,
+                          NullabilityKind::NonNull));
+
+  Preamble = R"cpp(
+    template <typename T, typename U>
+    struct Pair;
+    template <typename T>
+    using Two = Pair<T, T>;
+  )cpp";
+  EXPECT_THAT(
+      nullVec("Two<int* _Nullable>"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Nullable));
+
+  Preamble = R"cpp(
+    template <typename T1>
+    using A = T1* _Nullable;
+    template <typename T2>
+    using B = A<T2>* _Nonnull;
+  )cpp";
+  EXPECT_THAT(nullVec("B<int>"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
+
+  Preamble = R"cpp(
+    template <typename T, typename U, typename V>
+    struct Triple;
+    template <typename A, typename... Rest>
+    using TripleAlias = Triple<A _Nonnull, Rest...>;
+  )cpp";
+  EXPECT_THAT(nullVec("TripleAlias<int *, int *_Nullable, int*>"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable,
+                          NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, DependentAlias) {
+  // Simple dependent type-aliases.
+  Preamble = R"cpp(
+    template <class T>
+    struct Nullable {
+      using type = T _Nullable;
+    };
+  )cpp";
+  // TODO: should be [Nullable, Nonnull]
+  EXPECT_THAT(
+      nullVec("Nullable<int* _Nonnull *>::type"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, NestedClassTemplate) {
+  // Simple struct inside template.
+  Preamble = R"cpp(
+    template <class T>
+    struct Outer {
+      struct Inner;
+    };
+    using OuterNullableInner = Outer<int* _Nonnull>::Inner;
+  )cpp";
+  // TODO: should be [NonNull]
+  EXPECT_THAT(nullVec("Outer<int* _Nonnull>::Inner"),
+              ElementsAre(NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, ReferenceOuterTemplateParam) {
+  // Referencing type-params from indirectly-enclosing template.
+  Preamble = R"cpp(
+    template <class A, class B>
+    struct Pair;
+
+    template <class T>
+    struct Outer {
+      template <class U>
+      struct Inner {
+        using type = Pair<U, T>;
+      };
+    };
+  )cpp";
+  // TODO: should be [Nonnull, Nullable]
+  EXPECT_THAT(
+      nullVec("Outer<int *_Nullable>::Inner<int *_Nonnull>::type"),
+      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, DependentlyNamedTemplate) {
+  // Instantiation of dependent-named template
+  Preamble = R"cpp(
+    struct Wrapper {
+      template <class T>
+      using Nullable = T _Nullable;
+    };
+
+    template <class U, class WrapT>
+    struct S {
+      using type = typename WrapT::template Nullable<U>* _Nonnull;
+    };
+  )cpp";
+  EXPECT_THAT(nullVec("S<int *, Wrapper>::type"),
+              ElementsAre(NullabilityKind::NonNull, NullabilityKind::Nullable));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, TemplateTemplateParams) {
+  // Template template params
+  Preamble = R"cpp(
+    template <class X>
+    struct Nullable {
+      using type = X _Nullable;
+    };
+    template <class X>
+    struct Nonnull {
+      using type = X _Nonnull;
+    };
+
+    template <template <class> class Nullability, class T>
+    struct Pointer {
+      using type = typename Nullability<T*>::type;
+    };
+  )cpp";
+  EXPECT_THAT(nullVec("Pointer<Nullable, int>::type"),
+              ElementsAre(NullabilityKind::Nullable));
+  // TODO: should be [Nullable, Nonnull]
+  EXPECT_THAT(
+      nullVec("Pointer<Nullable, Pointer<Nonnull, int>::type>::type"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
+  // Same thing, but with alias templates.
+  Preamble = R"cpp(
+    template <class X>
+    using Nullable = X _Nullable;
+    template <class X>
+    using Nonnull = X _Nonnull;
+
+    template <template <class> class Nullability, class T>
+    struct Pointer {
+      using type = Nullability<T*>;
+    };
+  )cpp";
+  EXPECT_THAT(nullVec("Pointer<Nullable, int>::type"),
+              ElementsAre(NullabilityKind::Nullable));
+  // TODO: should be [Nullable, Nonnull]
+  EXPECT_THAT(
+      nullVec("Pointer<Nullable, Pointer<Nonnull, int>::type>::type"),
+      ElementsAre(NullabilityKind::Nullable, NullabilityKind::Unspecified));
+}
+
+TEST_F(GetNullabilityAnnotationsFromTypeTest, ClassTemplateParamPack) {
+  // Parameter packs
+  Preamble = R"cpp(
+    template <class... X>
+    struct TupleWrapper {
+      class Tuple;
+    };
+
+    template <class... X>
+    struct NullableTuple {
+      using type = TupleWrapper<X _Nullable...>::Tuple;
+    };
+  )cpp";
+  // TODO: should be [Unspecified, Nonnull]
+  EXPECT_THAT(
+      nullVec("TupleWrapper<int*, int* _Nonnull>::Tuple"),
+      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
+  // TODO: should be [Nullable, Nullable]
+  EXPECT_THAT(
+      nullVec("NullableTuple<int*, int* _Nonnull>::type"),
+      ElementsAre(NullabilityKind::Unspecified, NullabilityKind::Unspecified));
+}
+
+class PrintWithNullabilityTest : public ::testing::Test {
+ protected:
+  // C++ declarations prepended before parsing type in nullVec().
+  std::string Preamble;
+
+  // Parses `Type`, augments it with Nulls, and prints the result.
+  std::string print(llvm::StringRef Type, ArrayRef<NullabilityKind> Nulls) {
+    clang::TestAST AST((Preamble + "\n using Target = " + Type + ";").str());
+    auto Target = AST.context().getTranslationUnitDecl()->lookup(
+        &AST.context().Idents.get("Target"));
+    CHECK(Target.isSingleResult());
+    QualType TargetType =
+        AST.context().getTypedefType(Target.find_first<TypeAliasDecl>());
+    return printWithNullability(TargetType, Nulls, AST.context());
+  }
+};
+
+TEST_F(PrintWithNullabilityTest, Pointers) {
+  EXPECT_EQ(print("int*", {NullabilityKind::Nullable}), "int * _Nullable");
+  EXPECT_EQ(
+      print("int***", {NullabilityKind::Nullable, NullabilityKind::NonNull,
+                       NullabilityKind::Unspecified}),
+      "int ** _Nonnull * _Nullable");
+}
+
+TEST_F(PrintWithNullabilityTest, Sugar) {
+  Preamble = R"cpp(
+    template <class T>
+    using Ptr = T*;
+    using Int = int;
+    using IntPtr = Ptr<Int>;
+  )cpp";
+  EXPECT_EQ(print("IntPtr", {NullabilityKind::Nullable}), "int * _Nullable");
+}
+
+TEST_F(PrintWithNullabilityTest, Templates) {
+  Preamble = R"cpp(
+    template <class>
+    struct vector;
+    template <class, class>
+    struct pair;
+  )cpp";
+  EXPECT_EQ(print("vector<pair<int*, int*>*>",
+                  {NullabilityKind::Nullable, NullabilityKind::NonNull,
+                   NullabilityKind::Unspecified}),
+            "vector<pair<int * _Nonnull, int *> * _Nullable>");
+}
+
+TEST_F(PrintWithNullabilityTest, Functions) {
+  EXPECT_EQ(print("float*(*)(double*, double*)",
+                  {NullabilityKind::Nullable, NullabilityKind::NonNull,
+                   NullabilityKind::NonNull, NullabilityKind::Unspecified}),
+            "float * _Nonnull (* _Nullable)(double * _Nonnull, double *)");
+}
+
+}  // namespace
+}  // namespace clang::tidy::nullability
diff --git a/nullability/test/BUILD b/nullability/test/BUILD
new file mode 100644
index 0000000..cf99fe9
--- /dev/null
+++ b/nullability/test/BUILD
@@ -0,0 +1,219 @@
+# Integration tests for nullability verification.
+
+package(default_applicable_licenses = ["//:license"])
+
+cc_library(
+    name = "check_diagnostics",
+    testonly = 1,
+    srcs = ["check_diagnostics.cc"],
+    hdrs = ["check_diagnostics.h"],
+    deps = [
+        "//nullability:pointer_nullability_analysis",
+        "//nullability:pointer_nullability_diagnosis",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang/unittests:dataflow_testing_support",
+        "@llvm-project//llvm:Support",
+        "@llvm-project//llvm:TestingSupport",
+        "@llvm-project//third-party/unittest:gtest",
+    ],
+)
+
+cc_test(
+    name = "assert_nullability_test",
+    srcs = ["assert_nullability_test.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "aliases",
+    srcs = ["aliases.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "basic",
+    srcs = ["basic.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "binary_ops",
+    srcs = ["binary_ops.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "casts",
+    srcs = ["casts.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "comparisons",
+    srcs = ["comparisons.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "constructors",
+    srcs = ["constructors.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "fields",
+    srcs = ["fields.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "function_calls",
+    srcs = ["function_calls.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "function_pointers",
+    srcs = ["function_pointers.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "initialization",
+    srcs = ["initialization.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "merge",
+    srcs = ["merge.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "operator_new",
+    srcs = ["operator_new.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "parens",
+    srcs = ["parens.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "pointer_arithmetic",
+    srcs = ["pointer_arithmetic.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "return_statements",
+    srcs = ["return_statements.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "templates",
+    srcs = ["templates.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "temporary_materialization",
+    srcs = ["temporary_materialization.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "this_pointer",
+    srcs = ["this_pointer.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "variance",
+    srcs = ["variance.cc"],
+    deps = [
+        ":check_diagnostics",
+        "@llvm-project//third-party/unittest:gtest",
+        "@llvm-project//third-party/unittest:gtest_main",
+    ],
+)
diff --git a/nullability/test/aliases.cc b/nullability/test/aliases.cc
new file mode 100644
index 0000000..852a482
--- /dev/null
+++ b/nullability/test/aliases.cc
@@ -0,0 +1,30 @@
+// 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
+
+// Tests for nullability information hidden behind aliases.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang::tidy::nullability {
+namespace {
+
+TEST(PointerNullabilityTest, Aliases) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T>
+    struct Factory {
+      T get();
+    };
+    using NeverNull = int* _Nonnull;
+    using MaybeNull = int* _Nullable;
+
+    void target(Factory<NeverNull> never, Factory<MaybeNull> maybe) {
+      *never.get();
+      *maybe.get();  // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace clang::tidy::nullability
\ No newline at end of file
diff --git a/nullability/test/assert_nullability_test.cc b/nullability/test/assert_nullability_test.cc
new file mode 100644
index 0000000..2bdc10f
--- /dev/null
+++ b/nullability/test/assert_nullability_test.cc
@@ -0,0 +1,211 @@
+// 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
+
+// Tests that check that `__assert_nullability` works correctly.
+//
+// Note that this file should not contain all tests that use
+// `__assert_nullability`, but only tests to verify that `__assert_nullability`
+// itself works.
+//
+// TODO(mboehme): Because this test is doing something different than the
+// other tests, we would ideally want to place it in a different directory.
+// For the time being, the `_test` suffix at the end of the filename is
+// intended as a weak way of indicating this.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, AssertNullability) {
+  // Concrete struct.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct StructNonnullNullable {
+      int* _Nonnull nonnull;
+      int* _Nullable nullable;
+    };
+
+    void target(StructNonnullNullable p) {
+      __assert_nullability<>(p);
+      __assert_nullability<NK_nonnull>(p);                   // [[unsafe]]
+      __assert_nullability<NK_nullable>(p);                  // [[unsafe]]
+      __assert_nullability<NK_nonnull, NK_nullable>(p);      // [[unsafe]]
+      __assert_nullability<NK_nonnull, NK_unspecified>(p);   // [[unsafe]]
+      __assert_nullability<NK_nonnull, NK_nonnull>(p);       // [[unsafe]]
+      __assert_nullability<NK_nullable, NK_nullable>(p);     // [[unsafe]]
+      __assert_nullability<NK_unspecified, NK_nullable>(p);  // [[unsafe]]
+    }
+  )cc"));
+
+  // Struct with two template type parameters.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {};
+
+    void target(Struct2Arg<int *, int *_Nullable> p) {
+      __assert_nullability<NK_unspecified>(p);  // [[unsafe]]
+      __assert_nullability<NK_nullable>(p);     // [[unsafe]]
+
+      __assert_nullability<NK_unspecified, NK_nonnull>(p);  // [[unsafe]]
+      __assert_nullability<NK_unspecified, NK_nullable>(p);
+      __assert_nullability<NK_unspecified, NK_unspecified>(p);  // [[unsafe]]
+      __assert_nullability<NK_nonnull, NK_nullable>(p);         // [[unsafe]]
+      __assert_nullability<NK_nullable, NK_nullable>(p);        // [[unsafe]]
+
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_unspecified>(p);
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nullable>(p);
+    }
+  )cc"));
+
+  // Struct with one type and non-type template parameters.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, typename T2>
+    struct Struct3ArgWithInt {};
+
+    void target(Struct3ArgWithInt<2147483647, int* _Nullable, int* _Nonnull> p) {
+      __assert_nullability<>(p);             // [[unsafe]]
+      __assert_nullability<NK_nonnull>(p);   // [[unsafe]]
+      __assert_nullability<NK_nullable>(p);  // [[unsafe]]
+
+      __assert_nullability<NK_unspecified, NK_nonnull>(p);  // [[unsafe]]
+      __assert_nullability<NK_nonnull, NK_nonnull>(p);      // [[unsafe]]
+      __assert_nullability<NK_nonnull, NK_nullable>(p);     // [[unsafe]]
+      __assert_nullability<NK_nullable, NK_nonnull>(p);
+      __assert_nullability<NK_nullable, NK_nullable>(p);     // [[unsafe]]
+      __assert_nullability<NK_nullable, NK_unspecified>(p);  // [[unsafe]]
+
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nonnull>(p);
+    }
+  )cc"));
+
+  // Nested template arguments.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {};
+
+    void target(
+        Struct2Arg<Struct2Arg<int *, int *_Nullable>,
+                   Struct2Arg<Struct2Arg<int *_Nullable, int *_Nonnull>,
+                              Struct2Arg<int *_Nullable, int *_Nullable>>>
+            p) {
+      __assert_nullability<>(p);  // [[unsafe]]
+
+      __assert_nullability<NK_unspecified, NK_nullable, NK_nullable, NK_nonnull,
+                           NK_nullable, NK_nullable>(p);
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nullable, NK_nonnull, NK_nullable,
+           NK_nullable, NK_nullable>(p);
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nullable, NK_nonnull, NK_nullable>(
+              p);
+    }
+  )cc"));
+
+  // Struct with two template parameters substituted with concrete structs.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct StructUnknownNullable {
+      int* unknown;
+      int* _Nullable nullable;
+    };
+
+    struct StructNullableNonnull {
+      int* _Nullable nullable;
+      int* _Nonnull nonnull;
+    };
+
+    template <typename T1, typename T2>
+    struct Struct2Arg {};
+
+    void target(Struct2Arg<StructUnknownNullable, StructNullableNonnull> p) {
+      __assert_nullability<>(p);
+
+      __assert_nullability<NK_unspecified, NK_nullable>(p);  // [[unsafe]]
+
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nullable>(p);
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nullable, NK_nonnull>(p);
+      __assert_nullability  // [[unsafe]]
+          <NK_nonnull, NK_nullable, NK_nullable, NK_nonnull>(p);
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nullable, NK_nonnull,
+           NK_unspecified>(p);
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 arg0;
+      T1 arg1;
+
+      T0 getT0();
+      T1 getT1();
+    };
+
+    void target(
+        Struct2Arg<Struct2Arg<int *, int *_Nullable>,
+                   Struct2Arg<Struct2Arg<int *_Nullable, int *_Nonnull>,
+                              Struct2Arg<int *_Nullable, int *_Nullable>>>
+            p) {
+      __assert_nullability<NK_unspecified, NK_nullable, NK_nullable, NK_nonnull,
+                           NK_nullable, NK_nullable>(p);
+      __assert_nullability<NK_unspecified, NK_nullable>(p.arg0);
+      __assert_nullability<NK_unspecified>(p.arg0.arg0);
+      __assert_nullability<NK_nullable>(p.arg0.arg1);
+      __assert_nullability<NK_nullable, NK_nonnull, NK_nullable, NK_nullable>(
+          p.arg1);
+      __assert_nullability<NK_nullable, NK_nonnull>(p.arg1.arg0);
+      __assert_nullability<NK_nullable>(p.arg1.arg0.arg0);
+      __assert_nullability<NK_nonnull>(p.arg1.arg0.arg1);
+      __assert_nullability<NK_nullable, NK_nullable>(p.arg1.arg1);
+      __assert_nullability<NK_nullable>(p.arg1.arg1.arg0);
+      __assert_nullability<NK_nullable>(p.arg1.arg1.arg1);
+
+      __assert_nullability<>(p.arg0.arg0);           // [[unsafe]]
+      __assert_nullability<NK_unspecified>(p.arg0);  // [[unsafe]]
+      __assert_nullability                           // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_nonnull, NK_nullable, NK_nullable>(
+              p.arg1);
+
+      __assert_nullability<NK_unspecified, NK_nullable>(p.getT0());
+      __assert_nullability<NK_nonnull>(p.getT1().getT0().getT1());
+
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified, NK_nullable, NK_unspecified>(p.getT0());
+      __assert_nullability  // [[unsafe]]
+          <NK_unspecified>(p.getT0());
+
+      __assert_nullability<NK_nonnull>(p.getT1().arg0.getT1());
+      __assert_nullability<NK_nonnull>(p.arg1.getT0().arg1);
+      __assert_nullability<NK_nonnull>(p.arg1.arg0.arg1);
+
+      __assert_nullability  // [[unsafe]]
+          <>(p.getT1().getT0().getT1());
+      __assert_nullability  // [[unsafe]]
+          <NK_nonnull, NK_nonnull>(p.arg1.getT0().arg1);
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable p, int* _Nonnull q, int* r) {
+      __assert_nullability<NK_nonnull, NK_nullable>(&p);
+      __assert_nullability<NK_nonnull, NK_nonnull>(&q);
+      __assert_nullability<NK_nonnull>(&*p);  // [[unsafe]]
+      __assert_nullability<NK_nonnull>(&*q);
+      __assert_nullability<NK_nonnull>(&*r);
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/basic.cc b/nullability/test/basic.cc
new file mode 100644
index 0000000..9e8804e
--- /dev/null
+++ b/nullability/test/basic.cc
@@ -0,0 +1,241 @@
+// 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
+
+// Tests for basic functionality (simple dereferences without control flow).
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, NoPointerOperations) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() { 1 + 2; }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DerefNullPtr) {
+  // nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      int *x = nullptr;
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  // 0
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      int *x = 0;
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DerefAddrOf) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      int i;
+      int *x = &i;
+      *x;
+    }
+  )cc"));
+
+  // transitive
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      int i;
+      int *x = &i;
+      int *y = x;
+      *y;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DerefPtrAnnotatedNonNullWithoutACheck) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) { *x; }
+  )cc"));
+
+  // transitive
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nonnull x) {
+      int *y = x;
+      *y;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DerefPtrAnnotatedNullableWithoutACheck) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  // transitive
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x) {
+      int *y = x;
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DerefUnknownPtrWithoutACheck) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) { *x; }
+  )cc"));
+
+  // transitive
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      int *y = x;
+      *y;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DoubleDereference) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int** p) {
+      *p;
+      **p;
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int** _Nonnull p) {
+      *p;
+      **p;
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull* p) {
+      *p;
+      **p;
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull* _Nonnull p) {
+      *p;
+      **p;
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int** _Nullable p) {
+      *p;   // [[unsafe]]
+      **p;  // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable* p) {
+      *p;
+      **p;  // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable* _Nullable p) {
+      *p;   // [[unsafe]]
+      **p;  // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable* _Nonnull p) {
+      *p;
+      **p;  // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull* _Nullable p) {
+      *p;   // [[unsafe]]
+      **p;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, ArrowOperatorOnNonNullPtr) {
+  // (->) member field
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *foo;
+    };
+    void target(Foo *_Nonnull foo) { foo->foo; }
+  )cc"));
+
+  // (->) member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *foo();
+    };
+    void target(Foo *_Nonnull foo) { foo->foo(); }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, ArrowOperatorOnNullablePtr) {
+  // (->) member field
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *foo;
+    };
+    void target(Foo *_Nullable foo) {
+      foo->foo;  // [[unsafe]]
+      if (foo) {
+        foo->foo;
+      } else {
+        foo->foo;  // [[unsafe]]
+      }
+      foo->foo;  // [[unsafe]]
+    }
+  )cc"));
+
+  // (->) member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *foo();
+    };
+    void target(Foo *_Nullable foo) {
+      foo->foo();  // [[unsafe]]
+      if (foo) {
+        foo->foo();
+      } else {
+        foo->foo();  // [[unsafe]]
+      }
+      foo->foo();  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, ArrowOperatorOnUnknownPtr) {
+  // (->) member field
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *foo;
+    };
+    void target(Foo *foo) { foo->foo; }
+  )cc"));
+
+  // (->) member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *foo();
+    };
+    void target(Foo *foo) { foo->foo(); }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/binary_ops.cc b/nullability/test/binary_ops.cc
new file mode 100644
index 0000000..ecb1443
--- /dev/null
+++ b/nullability/test/binary_ops.cc
@@ -0,0 +1,88 @@
+// 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
+
+// Tests for binary operators.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, BinaryExpressions) {
+  // x && y
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nullable y) {
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+      if (x && y) {
+        *x;
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+
+  // x || y
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nullable y) {
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+      if (x || y) {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+
+  // !x && !y
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nullable y) {
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+      if (!x && !y) {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+
+  // !x || !y
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nullable y) {
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+      if (!x || !y) {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      } else {
+        *x;
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/casts.cc b/nullability/test/casts.cc
new file mode 100644
index 0000000..e719d3c
--- /dev/null
+++ b/nullability/test/casts.cc
@@ -0,0 +1,308 @@
+// 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
+
+// Tests for casts of types containing nullability annotations.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+// TODO(b/233582219): Implement diagnosis of unreachable program points
+TEST(PointerNullabilityTest, NonNullPtrImplicitCastToBool) {
+  // x
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) {
+      *x;
+      if (x) {
+        *x;
+      } else {
+        *x;  // unreachable
+      }
+      *x;
+    }
+  )cc"));
+
+  // !x
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) {
+      *x;
+      if (!x) {
+        *x;  // unreachable
+      } else {
+        *x;
+      }
+      *x;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NullablePtrImplicitCastToBool) {
+  // x
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+      if (x) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  // !x
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+      if (!x) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+// TODO(b/233582219): Fix false negatives. Casting the pointer to boolean is
+// evidence of the author considering null a possibility, hence the unnannotated
+// pointer should be considered nullable and emit warnings where it fails or is
+// not null checked.
+TEST(PointerNullabilityTest, UnknownPtrImplicitCastToBool) {
+  // x
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      *x;  // false-negative
+      if (x) {
+        *x;
+      } else {
+        *x;  // false-negative
+      }
+      *x;  // false-negative
+    }
+  )cc"));
+
+  // !x
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      *x;  // false-negative
+      if (!x) {
+        *x;  // false-negative
+      } else {
+        *x;
+      }
+      *x;  // false-negative
+    }
+  )cc"));
+}
+
+// CK_Bitcast: Bitcasts preserve outer nullability
+TEST(PointerNullabilityTest, Bitcast) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <class X>
+    struct vector {};
+
+    void target() {
+      // Bitcasts preserve nullability.
+      __assert_nullability<NK_nullable>((void*)value<int* _Nullable>());
+      __assert_nullability<NK_nonnull>((void*)value<int* _Nonnull>());
+      __assert_nullability<NK_unspecified>((void*)value<int*>());
+      // Nullability of further outer pointer types is preserved in bitcasts.
+      __assert_nullability<NK_nullable, NK_nullable>(
+          (void**)value<int* _Nullable* _Nullable>());
+      __assert_nullability<NK_nonnull, NK_nonnull>(
+          (void**)value<int* _Nonnull* _Nonnull>());
+      __assert_nullability<NK_unspecified, NK_unspecified>((void**)value<int**>());
+      // But nullability of other inner types is dropped.
+      __assert_nullability<NK_nullable, NK_unspecified>(
+          (void**)value<vector<int* _Nullable>* _Nullable>());
+      __assert_nullability<NK_nonnull, NK_unspecified>(
+          (void**)value<vector<int* _Nonnull>* _Nonnull>());
+
+      __assert_nullability<NK_nonnull, NK_unspecified>(
+          (void**)value<int* _Nonnull>);
+      __assert_nullability<NK_nonnull>((void*)value<int* _Nonnull* _Nonnull>());
+    }
+  )cc"));
+}
+
+// CK_NoOp: No-op casts preserve deep nullability
+// TODO: fix false-positives from treating untracked values as unsafe.
+TEST(PointerNullabilityTest, NoOp) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <class X>
+    struct vector {};
+
+    void target() {
+      // No-op casts preserve deep nullability.
+      __assert_nullability  // [[unsafe]] TODO: fix false positive
+          <NK_nullable, NK_nullable>(const_cast<vector<int>*>(
+              (vector<int>* const)value<vector<int* _Nullable>* _Nullable>()));
+    }
+  )cc"));
+}
+
+// Casts between types with inheritance - only simple cases handled.
+// TODO: fix false-positives from treating untracked values as unsafe.
+TEST(PointerNullabilityTest, Inheritance) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <class X>
+    struct base {
+      virtual void ensure_polymorphic();
+    };
+    template <class X>
+    struct derived : base<X> {};
+
+    void target() {
+      // CK_BaseToDerived: preserves outer nullability only.
+      // TODO: determine that derived's type param is actually nullable here.
+      __assert_nullability<NK_nullable, NK_unspecified>(
+          (derived<int *> *)value<base<int *_Nullable> *_Nullable>());
+      // CK_Dynamic: dynamic_cast returns a nullable pointer.
+      auto b = value<base<int *_Nonnull> *_Nonnull>();
+      __assert_nullability  // [[unsafe]] TODO: fix false positive
+          <NK_nullable, NK_unspecified>(dynamic_cast<derived<int> *>(b));
+      // ... only if casting to a pointer!
+      auto c = value<base<int *>>();
+      __assert_nullability<NK_unspecified>(dynamic_cast<derived<int *> &>(c));
+    }
+  )cc"));
+}
+
+// User-defined conversions could do anything, use declared type.
+TEST(PointerNullabilityTest, UserDefinedConversions) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <class X>
+    struct BuildFromPointer {
+      BuildFromPointer(int*);
+    };
+
+    void target() {
+      // User-defined conversions could do anything.
+      // CK_ConstructorConversion
+      __assert_nullability<NK_unspecified>(
+          (BuildFromPointer<double*>)value<int* _Nonnull>());
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CastToNonPointer) {
+  // Casting to non-pointer types destroyes nullability.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    using I = __INTPTR_TYPE__;
+
+    // TODO: fix false-positives from treating untracked values as unsafe.
+    void target() {
+      // Casting away pointerness destroys nullability.
+      // CK_PointerToIntegral
+      __assert_nullability<>((I)value<int* _Nonnull>());
+      // CK_PointerToBoolean
+      __assert_nullability<>((bool)value<int* _Nonnull>());
+      // Casting them back does not recover it.
+      // CK_IntegralToPointer
+      __assert_nullability  // [[unsafe]] TODO: fix false positive
+          <>((int*)(I)value<int* _Nonnull>());
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, TrivialNullability) {
+  // Casts with trivial nullability
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      // Null is nullable!
+      __assert_nullability<NK_nullable>((int*)nullptr);
+
+      // Decayed objects are non-null.
+      int array[2];
+      __assert_nullability<NK_nonnull>((int*)array);
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CastNullToAlias) {
+  // This used to crash!
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    using P = int *;
+    P target() { return nullptr; }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CastExpression) {
+  // TODO: We currently do not warn on local variables
+  // whose annotations conflict with the initializer. Decide whether to do so,
+  // and then treat static casts in an equivalent manner.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable p) {
+      static_cast<int *_Nonnull>(p);  // TODO: To warn, or not to warn, that is
+                                      // the question.
+      static_cast<int *>(p);
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, typename T2>
+    struct Struct3Arg {
+      T1 arg1;
+      T2 arg2;
+    };
+
+    void target(Struct3Arg<1, int *_Nullable, int *> &p) {
+      *static_cast<const Struct3Arg<1, int *, int *> &>(p).arg1;  // [[unsafe]]
+      *static_cast<const Struct3Arg<1, int *, int *> &>(p).arg2;
+      *static_cast<int *>(p.arg1);  // [[unsafe]]
+      *static_cast<int *>(p.arg2);
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Base {};
+    struct Derived : public Base {};
+
+    void target(Derived *_Nullable x, Derived *_Nonnull y) {
+      *static_cast<Base *>(x);  // [[unsafe]]
+      *static_cast<Base *>(y);  // [[unsafe]] TODO: Fix false positive.
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, typename T2>
+    struct Struct3Arg {
+      T1 arg1;
+      T2 arg2;
+    };
+
+    void target(Struct3Arg<1, int *_Nullable, int *> &p) {
+      *((const Struct3Arg<1, int *, int *> &)p).arg1;  // [[unsafe]]
+      *((const Struct3Arg<1, int *, int *> &)p)        // [[unsafe]]
+           .arg2;        // TODO: Fix false positive.
+      *(int *)p.arg1;    // [[unsafe]]
+      *(int *)p.arg2;    // [[unsafe]] TODO: Fix false positive.
+      *(float *)p.arg1;  // [[unsafe]]
+      *(char *)p.arg2;   // [[unsafe]] TODO: Fix false positive.
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 arg0;
+      T1 arg1;
+    };
+
+    void target(Struct2Arg<const int *, const int *_Nullable> &p) {
+      *const_cast<int *>(p.arg0);  // [[unsafe]] TODO: Fix false positive.
+      *const_cast<int *>(p.arg1);  // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/check_diagnostics.cc b/nullability/test/check_diagnostics.cc
new file mode 100644
index 0000000..a66d444
--- /dev/null
+++ b/nullability/test/check_diagnostics.cc
@@ -0,0 +1,101 @@
+// 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/test/check_diagnostics.h"
+
+#include "nullability/pointer_nullability_analysis.h"
+#include "nullability/pointer_nullability_diagnosis.h"
+#include "clang/Analysis/CFG.h"
+#include "third_party/llvm/llvm-project/clang/unittests/Analysis/FlowSensitive/TestingSupport.h"
+#include "llvm/Testing/Support/Error.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+constexpr char kPreamble[] = R"cc(
+  enum NullabilityKind {
+    NK_nonnull,
+    NK_nullable,
+    NK_unspecified,
+  };
+
+  template <NullabilityKind... NK, typename T>
+  void __assert_nullability(const T&);
+
+  template <typename T>
+  T value();
+)cc";
+
+constexpr char kNewHeader[] = R"cc(
+  namespace std {
+  struct nothrow_t {
+    explicit nothrow_t() = default;
+  };
+  extern const nothrow_t nothrow;
+  using size_t = decltype(sizeof(int));
+  }  // namespace std
+  void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
+)cc";
+
+bool checkDiagnostics(llvm::StringRef SourceCode) {
+  std::vector<CFGElement> Diagnostics;
+  PointerNullabilityDiagnoser Diagnoser;
+  bool Failed = false;
+  EXPECT_THAT_ERROR(
+      dataflow::test::checkDataflow<PointerNullabilityAnalysis>(
+          dataflow::test::AnalysisInputs<PointerNullabilityAnalysis>(
+              SourceCode, ast_matchers::hasName("target"),
+              [](ASTContext &ASTCtx, dataflow::Environment &) {
+                return PointerNullabilityAnalysis(ASTCtx);
+              })
+              .withPostVisitCFG([&Diagnostics, &Diagnoser](
+                                    ASTContext &Ctx, const CFGElement &Elt,
+                                    const dataflow::TransferStateForDiagnostics<
+                                        PointerNullabilityLattice> &State) {
+                auto EltDiagnostics = Diagnoser.diagnose(&Elt, Ctx, State);
+                if (EltDiagnostics.has_value()) {
+                  Diagnostics.push_back(EltDiagnostics.value());
+                }
+              })
+              .withASTBuildVirtualMappedFiles(
+                  {{"preamble.h", kPreamble}, {"new", kNewHeader}})
+              .withASTBuildArgs({"-fsyntax-only", "-std=c++17",
+                                 "-Wno-unused-value", "-Wno-nonnull",
+                                 "-include", "preamble.h", "-I."}),
+          [&Diagnostics, &Failed](
+              const llvm::DenseMap<unsigned, std::string> &Annotations,
+              const dataflow::test::AnalysisOutputs &AnalysisData) {
+            // Note: use sorted sets for expected and actual lines to improve
+            // readability of the error output in case the test fails.
+            std::set<unsigned> ExpectedLines, ActualLines;
+            for (const auto &[Line, _] : Annotations) {
+              ExpectedLines.insert(Line);
+            }
+            auto &SrcMgr = AnalysisData.ASTCtx.getSourceManager();
+            for (auto Element : Diagnostics) {
+              if (std::optional<CFGStmt> stmt = Element.getAs<CFGStmt>()) {
+                ActualLines.insert(SrcMgr.getPresumedLineNumber(
+                    stmt->getStmt()->getBeginLoc()));
+              } else if (std::optional<CFGInitializer> init =
+                             Element.getAs<CFGInitializer>()) {
+                ActualLines.insert(SrcMgr.getPresumedLineNumber(
+                    init->getInitializer()->getSourceLocation()));
+              } else {
+                ADD_FAILURE() << "this code should not be reached";
+              }
+            }
+            EXPECT_THAT(ActualLines, testing::ContainerEq(ExpectedLines));
+            if (ActualLines != ExpectedLines) {
+              Failed = true;
+            }
+          }),
+      llvm::Succeeded());
+  return !Failed;
+}
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/check_diagnostics.h b/nullability/test/check_diagnostics.h
new file mode 100644
index 0000000..9d05b76
--- /dev/null
+++ b/nullability/test/check_diagnostics.h
@@ -0,0 +1,25 @@
+// 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 THIRD_PARTY_CRUBIT_NULLABILITY_TEST_CHECK_DIAGNOSTICS_H_
+#define THIRD_PARTY_CRUBIT_NULLABILITY_TEST_CHECK_DIAGNOSTICS_H_
+
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+
+/// Runs nullability verification on `SourceCode` and returns whether
+/// diagnostics are produced on those lines marked in the source code with
+/// `llvm::Annotations` style annotations (and no other lines).
+/// TODO(mboehme): So far, we only check the locations of the diagnostics; it
+/// would be desirable to check their actual content too.
+bool checkDiagnostics(llvm::StringRef SourceCode);
+
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // THIRD_PARTY_CRUBIT_NULLABILITY_TEST_CHECK_DIAGNOSTICS_H_
diff --git a/nullability/test/comparisons.cc b/nullability/test/comparisons.cc
new file mode 100644
index 0000000..62d1163
--- /dev/null
+++ b/nullability/test/comparisons.cc
@@ -0,0 +1,502 @@
+// 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
+
+// Tests for comparisons of types containing nullability annotations.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, CompareNonNullPtrAndNonNullPtr) {
+  // nonnull == nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x, int* _Nonnull y) {
+      *x;
+      *y;
+      if (x == y) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+
+  // nonnull != nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x, int* _Nonnull y) {
+      *x;
+      *y;
+      if (x != y) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CompareNullablePtrAndNullablePtr) {
+  // nullable == nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nullable y) {
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+      if (x == y) {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+
+  // nullable != nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nullable y) {
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+      if (x != y) {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+        *y;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CompareUnknownPtrAndUnknownPtr) {
+  // unknown == unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x, int *y) {
+      *x;
+      *y;
+      if (x == y) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+
+  // unknown != unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x, int *y) {
+      *x;
+      *y;
+      if (x != y) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+}
+
+// TODO(b/233582219): Implement diagnosis of unreachable program points
+TEST(PointerNullabilityTest, CompareNonNullPtrAndNullPtr) {
+  // nonnull == nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) {
+      *x;
+      if (x == nullptr) {
+        *x;  // unreachable
+      } else {
+        *x;
+      }
+      *x;
+    }
+  )cc"));
+
+  // nullptr == nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) {
+      *x;
+      if (nullptr == x) {
+        *x;  // unreachable
+      } else {
+        *x;
+      }
+      *x;
+    }
+  )cc"));
+
+  // nonnull != nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) {
+      *x;
+      if (x != nullptr) {
+        *x;
+      } else {
+        *x;  // unreachable
+      }
+      *x;
+    }
+  )cc"));
+
+  // nullptr != nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull x) {
+      *x;
+      if (nullptr != x) {
+        *x;
+      } else {
+        *x;  // unreachable
+      }
+      *x;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CompareNullablePtrAndNullPtr) {
+  // nullable == nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+      if (x == nullptr) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  // nullptr == nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+      if (nullptr == x) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  // nullable != nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+      if (x != nullptr) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  // nullptr != nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x) {
+      *x;  // [[unsafe]]
+      if (nullptr != x) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CompareNullablePtrAndNonNullPtr) {
+  // nullable == nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nonnull y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (x == y) {
+        *x;
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+
+  // nonnull == nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nonnull y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (y == x) {
+        *x;
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+
+  // nullable != nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nonnull y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (x != y) {
+        *x;  // [[unsafe]]
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+
+  // nonnull != nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nullable x, int* _Nonnull y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (y != x) {
+        *x;  // [[unsafe]]
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CompareNullablePtrAndUnknownPtr) {
+  // nullable == unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x, int *y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (x == y) {
+        *x;  // [[unsafe]]
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+
+  // unknown == nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x, int *y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (y == x) {
+        *x;  // [[unsafe]]
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+
+  // nullable != unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x, int *y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (x != y) {
+        *x;  // [[unsafe]]
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+
+  // unknown != nullable
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x, int *y) {
+      *x;  // [[unsafe]]
+      *y;
+      if (y != x) {
+        *x;  // [[unsafe]]
+        *y;
+      } else {
+        *x;  // [[unsafe]]
+        *y;
+      }
+      *x;  // [[unsafe]]
+      *y;
+    }
+  )cc"));
+}
+
+// TODO(b/233582219): Fix false negatives. The pointer is compared to nullptr,
+// hence the unnannotated pointer should be considered nullable and emit
+// warnings where it fails or is not null checked.
+TEST(PointerNullabilityTest, CompareUnknownPtrAndNullPtr) {
+  // unknown == nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      *x;  // false-negative
+      if (x == nullptr) {
+        *x;  // false-negative
+      } else {
+        *x;
+      }
+      *x;  // false-negative
+    }
+  )cc"));
+
+  // nullptr == unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      *x;  // false-negative
+      if (nullptr == x) {
+        *x;  // false-negative
+      } else {
+        *x;
+      }
+      *x;  // false-negative
+    }
+  )cc"));
+
+  // unknown != nullptr
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      *x;  // false-negative
+      if (x != nullptr) {
+        *x;
+      } else {
+        *x;  // false-negative
+      }
+      *x;  // false-negative
+    }
+  )cc"));
+
+  // nullptr != unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x) {
+      *x;  // false-negative
+      if (nullptr != x) {
+        *x;
+      } else {
+        *x;  // false-negative
+      }
+      *x;  // false-negative
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CompareUnknownPtrAndNonNullPtr) {
+  // unknown == nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x, int *_Nonnull y) {
+      *x;
+      *y;
+      if (x == y) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+
+  // nonnull == unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x, int *_Nonnull y) {
+      *x;
+      *y;
+      if (y == x) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+
+  // unknown != nonnull
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x, int *_Nonnull y) {
+      *x;
+      *y;
+      if (x != y) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+
+  // nonnull != unknown
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *x, int *_Nonnull y) {
+      *x;
+      *y;
+      if (y != x) {
+        *x;
+        *y;
+      } else {
+        *x;
+        *y;
+      }
+      *x;
+      *y;
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/constructors.cc b/nullability/test/constructors.cc
new file mode 100644
index 0000000..51a2cb0
--- /dev/null
+++ b/nullability/test/constructors.cc
@@ -0,0 +1,113 @@
+// 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
+
+// Tests for constructors.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, ConstructExpr) {
+  // Constructor call assigned to local variable.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct TakeNonnull {
+      explicit TakeNonnull(int *_Nonnull) {}
+    };
+    struct TakeNullable {
+      explicit TakeNullable(int *_Nullable) {}
+    };
+    struct TakeUnannotated {
+      explicit TakeUnannotated(int *) {}
+    };
+    int *_Nonnull makeNonnull();
+    int *_Nullable makeNullable();
+    int *makeUnannotated();
+    void target() {
+      auto NN1 = TakeNonnull(makeNonnull());
+      auto NN2 = TakeNonnull(makeNullable());  // [[unsafe]]
+      auto NN3 = TakeNonnull(makeUnannotated());
+
+      auto NB1 = TakeNullable(makeNonnull());
+      auto NB2 = TakeNullable(makeNullable());
+      auto NB3 = TakeNullable(makeUnannotated());
+
+      auto UN1 = TakeUnannotated(makeNonnull());
+      auto UN2 = TakeUnannotated(makeNullable());
+      auto UN3 = TakeUnannotated(makeUnannotated());
+    }
+  )cc"));
+
+  // Constructor call in a base initializer.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct TakeNonnull {
+      explicit TakeNonnull(int* _Nonnull);
+    };
+    struct target : TakeNonnull {
+      target(int* _Nullable ptr_nullable)  // Forced line break.
+          : TakeNonnull(ptr_nullable)      // [[unsafe]]
+      {}
+    };
+  )cc"));
+
+  // Call to a delegating constructor.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nullable makeNullable();
+    struct target {
+      target(int* _Nonnull);
+      target()                      // Forced line break.
+          : target(makeNullable())  // [[unsafe]]
+      {}
+    };
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, ConstructorMemberInitializer) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nullable makeNullable();
+    struct target {
+      int* _Nonnull ptr_nonnull;
+      int* _Nullable ptr_nullable;
+      int* ptr_unannotated;
+      target()
+          : ptr_nonnull(makeNullable()),  // [[unsafe]]
+            ptr_nullable(makeNullable()),
+            ptr_unannotated(makeNullable()) {}
+    };
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull makeNonnull();
+    struct target {
+      int* _Nonnull ptr_nonnull;
+      int* _Nullable ptr_nullable;
+      int* ptr_unannotated;
+      target()
+          : ptr_nonnull(makeNonnull()),
+            ptr_nullable(makeNonnull()),
+            ptr_unannotated(makeNonnull()) {}
+    };
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *makeUnannotated();
+    struct target {
+      int *_Nonnull ptr_nonnull;
+      int *_Nullable ptr_nullable;
+      int *ptr_unannotated;
+      target()
+          : ptr_nonnull(makeUnannotated()),
+            ptr_nullable(makeUnannotated()),
+            ptr_unannotated(makeUnannotated()) {}
+    };
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/fields.cc b/nullability/test/fields.cc
new file mode 100644
index 0000000..e1f3f15
--- /dev/null
+++ b/nullability/test/fields.cc
@@ -0,0 +1,111 @@
+// 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
+
+// Tests for nullability annotations on fields.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, NonNullFieldsOfPointerType) {
+  // dereference field of pointer type
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo* _Nonnull ptr;
+    };
+    void target(Foo foo) { *foo.ptr; }
+  )cc"));
+
+  // dereference field of pointer type in member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo* _Nonnull ptr;
+      void target() { *ptr; }
+    };
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NullableFieldsOfPointerType) {
+  // dereference field of pointer type
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo* _Nullable ptr;
+    };
+    void target(Foo foo) {
+      *foo.ptr;  // [[unsafe]]
+      if (foo.ptr) {
+        *foo.ptr;
+      } else {
+        *foo.ptr;  // [[unsafe]]
+      }
+      *foo.ptr;  // [[unsafe]]
+    }
+  )cc"));
+
+  // dereference field of pointer type in member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo* _Nullable ptr;
+      void target() {
+        *ptr;  // [[unsafe]]
+        if (ptr) {
+          *ptr;
+        } else {
+          *ptr;  // [[unsafe]]
+        }
+        *ptr;  // [[unsafe]]
+      }
+    };
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, UnknownFieldsOfPointerType) {
+  // dereference field of pointer type
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *ptr;
+    };
+    void target(Foo foo) { *foo.ptr; }
+  )cc"));
+
+  // dereference field of pointer type in member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      Foo *ptr;
+      void target() { *ptr; }
+    };
+  )cc"));
+}
+
+// TODO: fix false positives due to unsupported PointerValues in the framework.
+TEST(PointerNullabilityTest, ChainedFieldDeref) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct S {
+      S* _Nonnull nonnull;
+      S* _Nullable nullable;
+      S* unknown;
+    };
+    void target(S& s) {
+      *(*s.nonnull).nonnull;   // [[unsafe]] TODO: fix false positive
+      *(*s.nonnull).nullable;  // [[unsafe]]
+      *(*s.nonnull).unknown;   // [[unsafe]] TODO: fix false positive
+
+      s.nonnull->nonnull->nonnull;   // [[unsafe]] TODO: fix false positive
+      s.nonnull->nonnull->nullable;  // [[unsafe]] TODO: fix false positive
+      s.nonnull->nullable->nonnull;  // [[unsafe]]
+      s.nonnull->unknown->nonnull;   // [[unsafe]] TODO: fix false positive
+
+      *&s;
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/function_calls.cc b/nullability/test/function_calls.cc
new file mode 100644
index 0000000..12f1310
--- /dev/null
+++ b/nullability/test/function_calls.cc
@@ -0,0 +1,449 @@
+// 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
+
+// Tests for function calls.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, CallExprWithPointerReturnType) {
+  // free function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nonnull makeNonnull();
+    int *_Nullable makeNullable();
+    int *makeUnannotated();
+    void target() {
+      *makeNonnull();
+      *makeNullable();  // [[unsafe]]
+      *makeUnannotated();
+    }
+  )cc"));
+
+  // member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      int *_Nonnull makeNonnull();
+      int *_Nullable makeNullable();
+      int *makeUnannotated();
+    };
+    void target(Foo foo) {
+      *foo.makeNonnull();
+      *foo.makeNullable();  // [[unsafe]]
+      *foo.makeUnannotated();
+    }
+  )cc"));
+
+  // function pointer
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull (*makeNonnull)(),
+                int* _Nullable (*makeNullable)(), int* (*makeUnannotated)()) {
+      *makeNonnull();
+      *makeNullable();  // [[unsafe]]
+      *makeUnannotated();
+    }
+  )cc"));
+
+  // pointer to function pointer
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int* _Nonnull (**makeNonnull)(),
+                int* _Nullable (**makeNullable)(), int* (**makeUnannotated)()) {
+      *(*makeNonnull)();
+      *(*makeNullable)();  // [[unsafe]]
+      *(*makeUnannotated)();
+    }
+  )cc"));
+
+  // function returning a function pointer which returns a pointer
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    typedef int* _Nonnull (*MakeNonnullT)();
+    typedef int* _Nullable (*MakeNullableT)();
+    typedef int* (*MakeUnannotatedT)();
+    void target(MakeNonnullT (*makeNonnull)(), MakeNullableT (*makeNullable)(),
+                MakeUnannotatedT (*makeUnannotated)()) {
+      *(*makeNonnull)()();
+      *(*makeNullable)()();  // [[unsafe]]
+      *(*makeUnannotated)()();
+    }
+  )cc"));
+
+  // free function returns reference to pointer
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nonnull &makeNonnull();
+    int *_Nullable &makeNullable();
+    int *&makeUnannotated();
+    void target() {
+      *makeNonnull();
+      *makeNullable();  // [[unsafe]]
+      *makeUnannotated();
+    }
+  )cc"));
+
+  // function called in loop
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nullable makeNullable();
+    bool makeBool();
+    void target() {
+      bool first = true;
+      while (true) {
+        int *x = makeNullable();
+        if (first && x == nullptr) return;
+        first = false;
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallExprParamAssignment) {
+  // free function with single param
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void takeNonnull(int *_Nonnull);
+    void takeNullable(int *_Nullable);
+    void takeUnannotated(int *);
+    void target(int *_Nonnull ptr_nonnull, int *_Nullable ptr_nullable,
+                int *ptr_unannotated) {
+      takeNonnull(nullptr);  // [[unsafe]]
+      takeNonnull(ptr_nonnull);
+      takeNonnull(ptr_nullable);  // [[unsafe]]
+      takeNonnull(ptr_unannotated);
+
+      takeNullable(nullptr);
+      takeNullable(ptr_nonnull);
+      takeNullable(ptr_nullable);
+      takeNullable(ptr_unannotated);
+
+      takeUnannotated(nullptr);
+      takeUnannotated(ptr_nonnull);
+      takeUnannotated(ptr_nullable);
+      takeUnannotated(ptr_unannotated);
+    }
+  )cc"));
+
+  // free function with multiple params of mixed nullability
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void takeMixed(int *, int *_Nullable, int *_Nonnull);
+    void target() {
+      takeMixed(nullptr, nullptr, nullptr);  // [[unsafe]]
+    }
+  )cc"));
+
+  // member function
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      void takeNonnull(int *_Nonnull);
+      void takeNullable(int *_Nullable);
+      void takeUnannotated(int *);
+    };
+    void target(Foo foo) {
+      foo.takeNonnull(nullptr);  // [[unsafe]]
+      foo.takeNullable(nullptr);
+      foo.takeUnannotated(nullptr);
+    }
+  )cc"));
+
+  // function pointer
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(void (*takeNonnull)(int *_Nonnull),
+                void (*takeNullable)(int *_Nullable),
+                void (*takeUnannotated)(int *)) {
+      takeNonnull(nullptr);  // [[unsafe]]
+      takeNullable(nullptr);
+      takeUnannotated(nullptr);
+    }
+  )cc"));
+
+  // pointer to function pointer
+  //
+  // TODO(b/233582219): Fix false negative. Implement support for retrieving
+  // parameter types from a pointer to function pointer.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(void (**takeNonnull)(int *_Nonnull),
+                void (**takeNullable)(int *_Nullable),
+                void (**takeUnannotated)(int *)) {
+      (*takeNonnull)(nullptr);  // false-negative
+      (*takeNullable)(nullptr);
+      (*takeUnannotated)(nullptr);
+    }
+  )cc"));
+
+  // function returned from function
+  //
+  // TODO(b/233582219): Fix false negative. Implement support for retrieving
+  // parameter types for functions returned by another function.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    typedef void (*takeNonnullF)(int *_Nonnull);
+    typedef void (*takeNullableF)(int *_Nullable);
+    typedef void (*takeUnannotatedF)(int *);
+    void target(takeNonnullF (*takeNonnull)(), takeNullableF (*takeNullable)(),
+                takeUnannotatedF (*takeUnannotated)()) {
+      (*takeNonnull)()(nullptr);  // false-negative
+      (*takeNullable)()(nullptr);
+      (*takeUnannotated)()(nullptr);
+    }
+  )cc"));
+
+  // passing a reference to a nonnull pointer
+  //
+  // TODO(b/233582219): Fix false negative. When the nonnull pointer is passed
+  // by reference into the callee which takes a nullable parameter, its value
+  // may be changed to null, making it unsafe to dereference when we return from
+  // the function call. Some possible approaches for handling this case:
+  // (1) Disallow passing a nonnull pointer as a nullable reference - and warn
+  // at the function call.
+  // (2) Assume in worst case the nonnull pointer becomes nullable after the
+  // call - and warn at the dereference.
+  // (3) Sacrifice soundness for reduction in noise, and skip the warning.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void takeNonnullRef(int *_Nonnull &);
+    void takeNullableRef(int *_Nullable &);
+    void takeUnannotatedRef(int *&);
+    void target(int *_Nonnull ptr_nonnull) {
+      takeNonnullRef(ptr_nonnull);
+      *ptr_nonnull;
+
+      // false-negative
+      takeNullableRef(ptr_nonnull);
+      *ptr_nonnull;
+
+      takeUnannotatedRef(ptr_nonnull);
+      *ptr_nonnull;
+    }
+  )cc"));
+
+  // passing a reference to a nullable pointer
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void takeNonnullRef(int *_Nonnull &);
+    void takeNullableRef(int *_Nullable &);
+    void takeUnannotatedRef(int *&);
+    void target(int *_Nullable ptr_nullable) {
+      takeNonnullRef(ptr_nullable);  // [[unsafe]]
+      *ptr_nullable;                 // [[unsafe]]
+
+      takeNullableRef(ptr_nullable);
+      *ptr_nullable;  // [[unsafe]]
+
+      takeUnannotatedRef(ptr_nullable);
+      *ptr_nullable;  // [[unsafe]]
+    }
+  )cc"));
+
+  // passing a reference to an unannotated pointer
+  //
+  // TODO(b/233582219): Fix false negative. The unannotated pointer should be
+  // considered nullable if it has been used as a nullable pointer.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void takeNonnullRef(int *_Nonnull &);
+    void takeNullableRef(int *_Nullable &);
+    void takeUnannotatedRef(int *&);
+    void target(int *ptr_unannotated) {
+      takeNonnullRef(ptr_unannotated);
+      *ptr_unannotated;
+
+      takeNullableRef(ptr_unannotated);
+      *ptr_unannotated;  // false-negative
+
+      takeUnannotatedRef(ptr_unannotated);
+      *ptr_unannotated;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CanOverwritePtrWithPtrCreatedFromRefReturnType) {
+  // Test that if we create a pointer from a function returning a reference, we
+  // can use that pointer to overwrite an existing nullable pointer and make it
+  // nonnull.
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int& get_int();
+
+    void target(int* _Nullable i) {
+      i = &get_int();
+      *i;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CanOverwritePtrWithPtrReturnedByFunction) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull get_int();
+
+    void target(int* _Nullable i) {
+      i = get_int();
+      *i;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallVariadicFunction) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void variadic(int* _Nonnull, ...);
+    void target() {
+      int i = 0;
+      variadic(&i, nullptr, &i);
+      variadic(nullptr, nullptr, &i);  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallMemberOperatorNoParams) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct MakeNonnull {
+      int *_Nonnull operator()();
+    };
+    struct MakeNullable {
+      int *_Nullable operator()();
+    };
+    struct MakeUnannotated {
+      int *operator()();
+    };
+    void target() {
+      MakeNonnull makeNonnull;
+      *makeNonnull();
+
+      MakeNullable makeNullable;
+      *makeNullable();  // [[unsafe]]
+
+      MakeUnannotated makeUnannotated;
+      *makeUnannotated();
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallMemberOperatorOneParam) {
+  // overloaded operator with single param
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    // map<int * _Nonnull, int>
+    struct MapWithNonnullKeys {
+      int &operator[](int *_Nonnull key);
+    };
+    // map<int * _Nullable, int>
+    struct MapWithNullableKeys {
+      int &operator[](int *_Nullable key);
+    };
+    // map<int *, int>
+    struct MapWithUnannotatedKeys {
+      int &operator[](int *key);
+    };
+    void target(int *_Nonnull ptr_nonnull, int *_Nullable ptr_nullable,
+                int *ptr_unannotated) {
+      MapWithNonnullKeys nonnull_keys;
+      nonnull_keys[nullptr] = 42;  // [[unsafe]]
+      nonnull_keys[ptr_nonnull] = 42;
+      nonnull_keys[ptr_nullable] = 42;  // [[unsafe]]
+      nonnull_keys[ptr_unannotated] = 42;
+
+      MapWithNullableKeys nullable_keys;
+      nullable_keys[nullptr] = 42;
+      nullable_keys[ptr_nonnull] = 42;
+      nullable_keys[ptr_nullable] = 42;
+      nullable_keys[ptr_unannotated] = 42;
+
+      MapWithUnannotatedKeys unannotated_keys;
+      unannotated_keys[nullptr] = 42;
+      unannotated_keys[ptr_nonnull] = 42;
+      unannotated_keys[ptr_nullable] = 42;
+      unannotated_keys[ptr_unannotated] = 42;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallMemberOperatorMultipleParams) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct TakeMixed {
+      void operator()(int *, int *_Nullable, int *_Nonnull);
+    };
+    void target() {
+      TakeMixed takeMixed;
+      takeMixed(nullptr, nullptr, nullptr);  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallFreeOperator) {
+  // No nullability involved. This is just a regression test to make sure we can
+  // process a call to a free overloaded operator.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct A {};
+    A operator+(A, A);
+    void target() {
+      A a;
+      a = a + a;
+    }
+  )cc"));
+}
+
+// Check that we distinguish between the nullability of the return type and
+// parameters.
+TEST(PointerNullabilityTest, DistinguishFunctionReturnTypeAndParams) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nullable callee(int *_Nonnull);
+
+    void target() {
+      int i = 0;
+      __assert_nullability<NK_nullable>(callee(&i));
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, DistinguishMethodReturnTypeAndParams) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct S {
+      int *_Nullable callee(int *_Nonnull);
+    };
+
+    void target(S s) {
+      int i = 0;
+      __assert_nullability<NK_nullable>(s.callee(&i));
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest,
+     ClassTemplate_DistinguishMethodReturnTypeAndParams) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct S {
+      T0 callee(T1);
+    };
+
+    void target(S<int *_Nullable, int *_Nonnull> s) {
+      int i = 0;
+      __assert_nullability<NK_nullable>(s.callee(&i));
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest,
+     CallFunctionTemplate_TemplateArgInReturnTypeHasNullTypeSourceInfo) {
+  // This test sets up a function call where we don't have a `TypeSourceInfo`
+  // for the argument to a template parameter used in the return type.
+  // This is a regression test for a crash that we observed on real-world code.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <class T>
+    struct A {
+      using Type = T;
+    };
+    template <int, class T>
+    typename A<T>::Type f(T);
+    void target() { f<0>(1); }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallFunctionTemplate_PartiallyDeduced) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int, class T>
+    T f(T);
+    void target() { f<0>(1); }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/function_pointers.cc b/nullability/test/function_pointers.cc
new file mode 100644
index 0000000..290ba15
--- /dev/null
+++ b/nullability/test/function_pointers.cc
@@ -0,0 +1,101 @@
+// 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
+
+// Tests for nullability of function pointers.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, FunctionToPointerDecayIsNonnull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      // Use `static_cast` to force function-to-pointer decay.
+      __assert_nullability<NK_nonnull>(static_cast<void (*)()>(target));
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, CallExplicitlyDereferencedDirectCallee) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void callee();
+    void target() { (*callee)(); }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, AnnotationsInReturnType) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nullable target() {
+      // Use `static_cast` to force function-to-pointer decay.
+      __assert_nullability<NK_nonnull, NK_nullable>(
+          static_cast<int* (*)()>(target));
+      return nullptr;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, AnnotationsInParameters) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable) {
+      // Use `static_cast` to force function-to-pointer decay.
+      __assert_nullability<NK_nonnull, NK_nullable>(
+          static_cast<void (*)(int *)>(target));
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NonnullCallback) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(void (*_Nonnull callback)()) {
+      // Both an explicit dereference and an implicit dereference done by a
+      // function call should be allowed.
+      (*callback)();
+      callback();
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NullableCallback) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(void (*_Nullable callback)()) {
+      // Both an explicit dereference and an implicit dereference done by a
+      // function call should be marked as unsafe.
+      (*callback)();  // [[unsafe]]
+      callback();     // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NonnullCallbackWithoutCalleeDecl) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    using NonnullCallbackType = void (*_Nonnull)();
+    NonnullCallbackType getCallback();
+    void target() {
+      __assert_nullability<NK_nonnull>(getCallback());
+      (*getCallback())();
+      getCallback()();
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NullableCallbackWithoutCalleeDecl) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    using NullableCallbackType = void (*_Nullable)();
+    NullableCallbackType getCallback();
+    void target(bool b) {
+      __assert_nullability<NK_nullable>(getCallback());
+      (*getCallback())();  // [[unsafe]]
+      getCallback()();     // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/initialization.cc b/nullability/test/initialization.cc
new file mode 100644
index 0000000..fd03f7e
--- /dev/null
+++ b/nullability/test/initialization.cc
@@ -0,0 +1,46 @@
+// 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
+
+// Tests that check nullability is transferred correctly across initializers.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, TransitiveNullCheck) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x) {
+      int *y = x;
+      *x;  // [[unsafe]]
+      if (y) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable x) {
+      int *y = x;
+      *y;  // [[unsafe]]
+      if (x) {
+        *y;
+      } else {
+        *y;  // [[unsafe]]
+      }
+      *y;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/merge.cc b/nullability/test/merge.cc
new file mode 100644
index 0000000..e9e2787
--- /dev/null
+++ b/nullability/test/merge.cc
@@ -0,0 +1,317 @@
+// 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
+
+// Tests for merging different nullability types.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, MergeNullAndNonNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nonnull y, bool b) {
+      int *x = nullptr;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = y;
+        *x;
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNullAndNullable) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable y, bool b) {
+      int *x = nullptr;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = y;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNullAndUnknown) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *y, bool b) {
+      int *x = nullptr;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = y;
+        *x;
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNonNullAndNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nonnull y, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = nullptr;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNonNullAndNonNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nonnull y, int *_Nonnull z, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = z;
+        *x;
+      }
+      *x;
+      if (b) {
+        *x;
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNonNullAndNullable) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nonnull y, int *_Nullable z, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = z;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNonNullAndUnknown) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nonnull y, int *z, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = z;
+        *x;
+      }
+      *x;
+      if (b) {
+        *x;
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNullableAndNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable y, bool b) {
+      int *x = y;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = nullptr;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNullableAndNonNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable y, int *_Nonnull z, bool b) {
+      int *x = y;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = z;
+        *x;
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNullableAndNullable) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable y, int *_Nullable z, bool b) {
+      int *x = y;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = z;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeNullableAndUnknown) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable y, int *z, bool b) {
+      int *x = y;
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+        x = z;
+        *x;
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;
+      } else {
+        *x;  // [[unsafe]]
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeUnknownAndNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *y, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = nullptr;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeUnknownAndNonNull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *y, int *_Nonnull z, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = z;
+        *x;
+      }
+      *x;
+      if (b) {
+        *x;
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeUnknownAndNullable) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *y, int *_Nullable z, bool b) {
+      int *x = y;
+      *x;
+      if (b) {
+        *x;
+        x = z;
+        *x;  // [[unsafe]]
+      }
+      *x;  // [[unsafe]]
+      if (b) {
+        *x;  // [[unsafe]]
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MergeUnknownAndUnknown) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *y, int *z, bool b) {
+      int *x = y;
+      if (b) {
+        *x;
+        x = z;
+        *x;
+      }
+      *x;
+      if (b) {
+        *x;
+      } else {
+        *x;
+      }
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/operator_new.cc b/nullability/test/operator_new.cc
new file mode 100644
index 0000000..585efc3
--- /dev/null
+++ b/nullability/test/operator_new.cc
@@ -0,0 +1,74 @@
+// 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
+
+// Tests for operator new.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, ThrowingNew) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      int *p = new int;
+      *p;
+      delete p;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, AssignFromNewMakesNullableNonnull) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target() {
+      int* _Nullable p = nullptr;
+      p = new int;
+      *p;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NoThrowNew) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+#include <new>
+    void target() {
+      int* p = new (std::nothrow) int;
+      *p;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, AssignFromNoThrowNewMakesNonnullNullable) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+#include <new>
+    void target() {
+      int i = 0;
+      int* _Nonnull p = &i;
+      p = new (std::nothrow) int;
+      *p;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NewPreservesNullabilityOnAllocatedType) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+#include <new>
+    void target() {
+      __assert_nullability<NK_nonnull, NK_nonnull>(new (int* _Nonnull));
+      __assert_nullability<NK_nonnull, NK_nullable>(new (int* _Nullable));
+      __assert_nullability<NK_nullable, NK_nonnull>(
+          new (std::nothrow)(int* _Nonnull));
+      __assert_nullability<NK_nullable, NK_nullable>(
+          new (std::nothrow)(int* _Nullable));
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/parens.cc b/nullability/test/parens.cc
new file mode 100644
index 0000000..926a148
--- /dev/null
+++ b/nullability/test/parens.cc
@@ -0,0 +1,53 @@
+// 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
+
+// Tests for parenthesized expressions.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, ParenthesizedExpressions) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+      T0 getT0();
+    };
+
+    void target(Struct1Arg<int *_Nullable> p) {
+      *(p).arg0;         // [[unsafe]]
+      *((p)).arg0;       // [[unsafe]]
+      *(p).getT0();      // [[unsafe]]
+      *(((p))).getT0();  // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, typename T2>
+    struct Struct3ArgWithInt {
+      T1 arg1;
+      T2 arg2;
+
+      T1 getT1();
+      T2 getT2();
+    };
+
+    void target(Struct3ArgWithInt<1, int *, int *_Nullable> p) {
+      *(((p)).arg1);
+      *(((p))).getT1();
+      (*((p)).arg2);         // [[unsafe]]
+      *(((((p)))).getT2());  // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/pointer_arithmetic.cc b/nullability/test/pointer_arithmetic.cc
new file mode 100644
index 0000000..8af4adf
--- /dev/null
+++ b/nullability/test/pointer_arithmetic.cc
@@ -0,0 +1,43 @@
+// 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
+
+// Tests for pointer arithmetic.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+// TODO: fix false positives due to unsupported PointerValues in the framework.
+TEST(PointerNullabilityTest, PointerArithmetic) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void target(int *_Nullable p, int *_Nonnull q, int *r) {
+      *++p;  // [[unsafe]]
+      *p++;  // [[unsafe]]
+      *--p;  // [[unsafe]]
+      *p--;  // [[unsafe]]
+      *+p;   // [[unsafe]]
+
+      *++q;  // [[unsafe]] TODO: fix false positive
+      *q++;  // [[unsafe]] TODO: fix false positive
+      *--q;  // [[unsafe]] TODO: fix false positive
+      *q--;  // [[unsafe]] TODO: fix false positive
+      *+q;   // [[unsafe]] TODO: fix false positive
+
+      *++r;  // [[unsafe]] TODO: fix false positive
+      *r++;  // [[unsafe]] TODO: fix false positive
+      *--r;  // [[unsafe]] TODO: fix false positive
+      *r--;  // [[unsafe]] TODO: fix false positive
+      *+r;   // [[unsafe]] TODO: fix false positive
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/return_statements.cc b/nullability/test/return_statements.cc
new file mode 100644
index 0000000..4672c1e
--- /dev/null
+++ b/nullability/test/return_statements.cc
@@ -0,0 +1,141 @@
+// 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
+
+// Tests for return statements.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, ReturnStatements) {
+  // nonnull return type
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull target() {
+      return nullptr;  // [[unsafe]]
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull target(int* _Nonnull ptr_nonnull) {
+      return ptr_nonnull;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull target(int* _Nullable ptr_nullable) {
+      return ptr_nullable;  // [[unsafe]]
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nonnull target(int *ptr_unannotated) {
+      return ptr_unannotated;
+    }
+  )cc"));
+
+  // nullable return type
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nullable target() { return nullptr; }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nullable target(int* _Nonnull ptr_nonnull) {
+      return ptr_nonnull;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nullable target(int* _Nullable ptr_nullable) {
+      return ptr_nullable;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nullable target(int *ptr_unannotated) {
+      return ptr_unannotated;
+    }
+  )cc"));
+
+  // unannotated return type
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* target() { return nullptr; }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* target(int* _Nonnull ptr_nonnull) {
+      return ptr_nonnull;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* target(int* _Nullable ptr_nullable) {
+      return ptr_nullable;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *target(int *ptr_unannotated) {
+      return ptr_unannotated;
+    }
+  )cc"));
+
+  // multiple return statements
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull target(bool b, int* _Nonnull ptr_nonnull) {
+      if (b) {
+        return nullptr;  // [[unsafe]]
+      }
+      return ptr_nonnull;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull target(int* _Nullable ptr_nullable,
+                         int* _Nonnull ptr_nonnull) {
+      if (ptr_nullable) {
+        return ptr_nullable;
+      }
+      return ptr_nonnull;
+    }
+  )cc"));
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int* _Nonnull target(int* _Nullable ptr_nullable_1,
+                         int* _Nullable ptr_nullable_2) {
+      if (ptr_nullable_1) {
+        return ptr_nullable_2;  // [[unsafe]]
+      }
+      return ptr_nullable_1;  // [[unsafe]]
+    }
+  )cc"));
+
+  // return result of merging 2 pointer values
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nonnull target(bool b, int i) {
+      int *ptr;
+      if (b) {
+        ptr = &i;
+      } else {
+        ptr = nullptr;
+      }
+      return ptr;  // [[unsafe]]
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, NonPointerReturnType) {
+  checkDiagnostics(R"cc(
+    struct S {
+      int* p;
+      int*& target() { return p; }
+    };
+  )cc");
+
+  checkDiagnostics(R"cc(
+    struct S {
+      int* _Nullable p;
+      int* _Nonnull& target() {
+        return p;  // TODO: Fix false negative.
+      }
+    };
+  )cc");
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/templates.cc b/nullability/test/templates.cc
new file mode 100644
index 0000000..2de241a
--- /dev/null
+++ b/nullability/test/templates.cc
@@ -0,0 +1,1170 @@
+// 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
+
+// Tests for nullability annotations in template arguments.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+// TODO: Fix false negatives.
+TEST(PointerNullabilityTest, ClassTemplateInstantiation) {
+  // Class template specialization with one argument initialised as _Nullable.
+  // We test types that contain both nullability that is substituted into the
+  // template argument and nullability that is spelt inside the template. That
+  // is, we should be able to accurately store nullabilities from different
+  // sources in a single nullability vector.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+      T0 *unknownTPtr;
+      T0 *_Nullable nullableTPtr;
+      T0 *_Nonnull nonnullTPtr;
+
+      T0 getT();
+      T0 *getUnknownTPtr();
+      T0 *_Nullable getNullableTPtr();
+      T0 *_Nonnull getNonnullTPtr();
+    };
+    void target(Struct1Arg<int *_Nullable> p) {
+      *p.arg0;  // [[unsafe]]
+      *p.unknownTPtr;
+      *p.nullableTPtr;  // [[unsafe]]
+      *p.nonnullTPtr;
+      **p.unknownTPtr;   // [[unsafe]]
+      **p.nullableTPtr;  // [[unsafe]]
+      **p.nonnullTPtr;   // [[unsafe]]
+
+      *p.getT();  // [[unsafe]]
+      *p.getUnknownTPtr();
+      *p.getNullableTPtr();  // [[unsafe]]
+      *p.getNonnullTPtr();
+      **p.getUnknownTPtr();   // [[unsafe]]
+      **p.getNullableTPtr();  // [[unsafe]]
+      **p.getNonnullTPtr();   // [[unsafe]]
+    }
+  )cc"));
+
+  // Class template specialization with one argument initialised as _Nonnull.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+      T0 *unknownTPtr;
+      T0 *_Nullable nullableTPtr;
+      T0 *_Nonnull nonnullTPtr;
+
+      T0 getT();
+      T0 *getUnknownTPtr();
+      T0 *_Nullable getNullableTPtr();
+      T0 *_Nonnull getNonnullTPtr();
+    };
+
+    void target(Struct1Arg<int *_Nonnull> p) {
+      *p.getT();
+      *p.getUnknownTPtr();
+      *p.getNullableTPtr();  // [[unsafe]]
+      *p.getNonnullTPtr();
+      **p.getUnknownTPtr();
+      **p.getNullableTPtr();  // [[unsafe]]
+      **p.getNonnullTPtr();
+
+      *p.arg0;
+      *p.unknownTPtr;
+      *p.nullableTPtr;  // [[unsafe]]
+      *p.nonnullTPtr;
+      **p.unknownTPtr;
+      **p.nullableTPtr;  // [[unsafe]]
+      **p.nonnullTPtr;
+    }
+  )cc"));
+
+  // Class template specialization with one argument initialised without
+  // nullability annotation.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+      T0 *unknownTPtr;
+      T0 *_Nullable nullableTPtr;
+      T0 *_Nonnull nonnullTPtr;
+      T0 getT();
+
+      T0 *getUnknownTPtr();
+      T0 *_Nullable getNullableTPtr();
+      T0 *_Nonnull getNonnullTPtr();
+    };
+
+    void target(Struct1Arg<int *> p) {
+      *p.getT();
+      *p.getUnknownTPtr();
+      *p.getNullableTPtr();  // [[unsafe]]
+      *p.getNonnullTPtr();
+      **p.getUnknownTPtr();
+      **p.getNullableTPtr();  // [[unasfe]]
+      **p.getNonnullTPtr();
+
+      *p.arg0;
+      *p.unknownTPtr;
+      *p.nullableTPtr;  // [[unsafe]]
+      *p.nonnullTPtr;
+      **p.unknownTPtr;
+      **p.nullableTPtr;  // [[unsafe]]
+      **p.nonnullTPtr;
+    }
+  )cc"));
+
+  // Class template specialization with two arguments, whose second argument is
+  // initialized as nullable.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 arg0;
+      T0 *unknownT0Ptr;
+      T0 *_Nullable nullableT0Ptr;
+      T0 *_Nonnull nonnullT0Ptr;
+
+      T1 arg1;
+      T1 *unknownT1Ptr;
+      T1 *_Nullable nullableT1Ptr;
+      T1 *_Nonnull nonnullT1Ptr;
+
+      T0 getT0();
+      T0 *getUnknownT0Ptr();
+      T0 *_Nullable getNullableT0Ptr();
+      T0 *_Nonnull getNonnullT0Ptr();
+
+      T1 getT1();
+      T1 *getUnknownT1Ptr();
+      T1 *_Nullable getNullableT1Ptr();
+      T1 *_Nonnull getNonnullT1Ptr();
+    };
+
+    void target(Struct2Arg<int *_Nonnull, double *_Nullable> p) {
+      *p.arg0;
+      *p.arg1;  // [[unsafe]]
+
+      *p.unknownT0Ptr;
+      *p.nullableT0Ptr;  // [[unsafe]]
+      *p.nonnullT0Ptr;
+
+      *p.unknownT1Ptr;
+      *p.nullableT1Ptr;  // [[unsafe]]
+      *p.nonnullT1Ptr;
+
+      *p.getUnknownT0Ptr();
+      *p.getNullableT0Ptr();  // [[unsafe]]
+      *p.getNonnullT0Ptr();
+
+      *p.getUnknownT1Ptr();
+      *p.getNullableT1Ptr();  // [[unsafe]]
+      *p.getNonnullT1Ptr();
+    }
+  )cc"));
+
+  // Class template specialization with 5 arguments with interleaved
+  // nullable/nonnull/unknown.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1, typename T2, typename T3, typename T4>
+    struct Struct5Arg {
+      T0 arg0;
+      T1 arg1;
+      T2 arg2;
+      T3 arg3;
+      T4 arg4;
+
+      T0 getT0();
+      T1 getT1();
+      T2 getT2();
+      T3 getT3();
+      T4 getT4();
+    };
+    void target(Struct5Arg<int* _Nullable, double* _Nonnull, float*,
+                           double* _Nullable, int* _Nonnull>
+                    p) {
+      *p.arg0;  // [[unsafe]]
+      *p.arg1;
+      *p.arg2;
+      *p.arg3;  // [[unsafe]]
+      *p.arg4;
+
+      *p.getT0();  // [[unsafe]]
+      *p.getT1();
+      *p.getT2();
+      *p.getT3();  // [[unsafe]]
+      *p.getT4();
+    }
+  )cc"));
+
+  // Class template specialization with 5 arguments with interleaved
+  // nullable/nonnull/unknown/const.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1, typename T2, typename T3, typename T4>
+    struct Struct5Arg {
+      T0 arg0;
+      T1 arg1;
+      T2 arg2;
+      T3 arg3;
+      T4 arg4;
+
+      T0 getT0();
+      T1 getT1();
+      T2 getT2();
+      T3 getT3();
+      T4 getT4();
+    };
+    void target(Struct5Arg<int* const _Nullable, double const* const _Nonnull,
+                           float*, double const* const _Nullable, int* _Nonnull>
+                    p) {
+      *p.arg0;  // [[unsafe]]
+      *p.arg1;
+      *p.arg2;
+      *p.arg3;  // [[unsafe]]
+      *p.arg4;
+
+      *p.getT0();  // [[unsafe]]
+      *p.getT1();
+      *p.getT2();
+      *p.getT3();  // [[unsafe]]
+      *p.getT4();
+    }
+  )cc"));
+
+  // Class template specialization with interleaved int and type template
+  // parameters.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, int I2, typename T3, int I4, typename T5>
+    struct Struct6ArgWithInt {
+      T1 arg1;
+      T3 arg3;
+      T5 arg5;
+
+      T1 getT1();
+      T3 getT3();
+      T5 getT5();
+    };
+    void target(
+        Struct6ArgWithInt<0, int *_Nullable, 1, int *_Nullable, 2, int *> &x) {
+      *x.arg1;  // [[unsafe]]
+      *x.arg3;  // [[unsafe]]
+      *x.arg5;
+
+      *x.getT1();  // [[unsafe]]
+      *x.getT3();  // [[unsafe]]
+      *x.getT5();
+    }
+  )cc"));
+}
+
+// TODO: Fix false positives and false negatives.
+TEST(PointerNullabilityTest,
+     ClassTemplateInstantiationWithStructsAsParameters) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct3IntPtrs {
+      int* unknown;
+      int* _Nullable nullable;
+      int* _Nonnull nonnull;
+
+      int* getUnknown();
+      int* _Nullable getNullable();
+      int* _Nonnull getNonnull();
+    };
+
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+      T0 getT0();
+    };
+
+    void target(Struct1Arg<Struct3IntPtrs> p) {
+      *p.arg0.unknown;
+      *p.arg0.nullable;  // [[unsafe]]
+      *p.arg0.nonnull;
+
+      *p.arg0.getUnknown();
+      *p.arg0.getNullable();  // [[unsafe]]
+      *p.arg0.getNonnull();
+
+      *p.getT0().unknown;   // [[unsafe]] TODO: fix false positive.
+      *p.getT0().nullable;  // [[unsafe]]
+      *p.getT0().nonnull;   // [[unsafe]] TODO: fix false positive.
+
+      *p.getT0().getUnknown();
+      *p.getT0().getNullable();  // [[unsafe]]
+      *p.getT0().getNonnull();
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct1UnknownArg {
+      char* unknownChar;
+
+      char* getUnknownChar();
+    };
+
+    struct Struct1NullableArg {
+      char* _Nullable nullableChar;
+
+      char* _Nullable getNullableChar();
+    };
+
+    struct Struct1NonnullArg {
+      char* _Nonnull nonnullChar;
+
+      char* _Nonnull getNonnullChar();
+    };
+
+    struct StructLotsOfArgs {
+      int num;
+      long long* unknownLongLong;
+      double* _Nullable nullableDouble;
+      float* _Nonnull nonnullFloat;
+      short* unknownShort;
+      unsigned int* _Nullable nullableUInt;
+      bool* _Nullable nullableBool;
+
+      long long* getUnknownLongLong();
+      double* _Nullable getNullableDouble();
+      float* _Nonnull getNonnullFloat();
+      short* getUnknownShort();
+      unsigned int* _Nullable getNullableUInt();
+      bool* _Nullable getNullableBool();
+    };
+
+    template <typename T0, typename T1, typename T2, typename T3>
+    struct Struct4Arg {
+      T0 arg0;
+      T1 arg1;
+      T2 arg2;
+      T3 arg3;
+
+      T0 getT0();
+      T1 getT1();
+      T2 getT2();
+      T3 getT3();
+    };
+
+    void target(Struct4Arg<Struct1UnknownArg, Struct1NullableArg,
+                           Struct1NonnullArg, StructLotsOfArgs>
+                    p) {
+      *p.arg0.unknownChar;
+      *p.arg1.nullableChar;  // [[unsafe]]
+      *p.arg2.nonnullChar;
+      *p.arg3.unknownLongLong;
+      *p.arg3.nullableDouble;  // [[unsafe]]
+      *p.arg3.nonnullFloat;
+      *p.arg3.unknownShort;
+      *p.arg3.nullableUInt;  // [[unsafe]]
+      *p.arg3.nullableBool;  // [[unsafe]]
+
+      *p.arg0.getUnknownChar();
+      *p.arg1.getNullableChar();  // [[unsafe]]
+      *p.arg2.getNonnullChar();
+      *p.arg3.getUnknownLongLong();
+      *p.arg3.getNullableDouble();  // [[unsafe]]
+      *p.arg3.getNonnullFloat();
+      *p.arg3.getUnknownShort();
+      *p.arg3.getNullableUInt();  // [[unsafe]]
+      *p.arg3.getNullableBool();  // [[unsafe]]
+
+      *p.getT0().unknownChar;      // [[unsafe]] TODO: fix false positive.
+      *p.getT1().nullableChar;     // [[unsafe]]
+      *p.getT2().nonnullChar;      // [[unsafe]] TODO: fix false positive.
+      *p.getT3().unknownLongLong;  // [[unsafe]] TODO: fix false positive.
+      *p.getT3().nullableDouble;   // [[unsafe]]
+      *p.getT3().nonnullFloat;     // [[unsafe]] TODO: fix false positive.
+      *p.getT3().unknownShort;     // [[unsafe]] TODO: fix false positive.
+      *p.getT3().nullableUInt;     // [[unsafe]]
+      *p.getT3().nullableBool;     // [[unsafe]]
+
+      *p.getT0().getUnknownChar();
+      *p.getT1().getNullableChar();  // [[unsafe]]
+      *p.getT2().getNonnullChar();
+      *p.getT3().getUnknownLongLong();
+      *p.getT3().getNullableDouble();  // [[unsafe]]
+      *p.getT3().getNonnullFloat();
+      *p.getT3().getUnknownShort();
+      *p.getT3().getNullableUInt();  // [[unsafe]]
+      *p.getT3().getNullableBool();  // [[unsafe]]
+    }
+  )cc"));
+
+  // With const arguments and int template parameter.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct1UnknownArg {
+      char* const constUnknownChar;
+      char const* unknownConstChar;
+      char const* const constUnknownConstChar;
+
+      char* const getConstUnknownChar();
+      char const* getUnknownConstChar();
+      char const* const getConstUnknownConstChar();
+    };
+
+    struct Struct1NullableArg {
+      char* const _Nullable constNullableChar;
+      char const* _Nullable nullableConstChar;
+      char const* const _Nullable constNullableConstChar;
+
+      char* const _Nullable getConstNullableChar();
+      char const* _Nullable getNullableConstChar();
+      char* const* _Nullable getConstNullableConstChar();
+    };
+
+    struct Struct1NonnullArg {
+      char* const _Nonnull constNonnullChar;
+      char const* _Nonnull nonnullConstChar;
+      char const* const _Nonnull constNonnullConstChar;
+
+      char* const _Nonnull getConstNonnullChar();
+      char const* _Nonnull getNonnullConstChar();
+      char const* const _Nonnull getConstNonnullConstChar();
+    };
+
+    template <int I0, typename T1, typename T2, typename T3>
+    struct Struct4Arg {
+      T1 arg1;
+      T2 arg2;
+      T3 arg3;
+
+      T1 getT1();
+      T2 getT2();
+      T3 getT3();
+    };
+
+    void target(
+        Struct4Arg<4, Struct1UnknownArg, Struct1NullableArg, Struct1NonnullArg>
+            p) {
+      *p.arg1.constUnknownChar;
+      *p.arg1.unknownConstChar;
+      *p.arg1.constUnknownConstChar;
+      *p.arg2.constNullableChar;       // [[unsafe]]
+      *p.arg2.nullableConstChar;       // [[unsafe]]
+      *p.arg2.constNullableConstChar;  // [[unsafe]]
+      *p.arg3.constNonnullChar;
+      *p.arg3.nonnullConstChar;
+      *p.arg3.constNonnullConstChar;
+
+      *p.arg1.getConstUnknownChar();
+      *p.arg1.getUnknownConstChar();
+      *p.arg1.getConstUnknownConstChar();
+      *p.arg2.getConstNullableChar();       // [[unsafe]]
+      *p.arg2.getNullableConstChar();       // [[unsafe]]
+      *p.arg2.getConstNullableConstChar();  // [[unsafe]]
+      *p.arg3.getConstNonnullChar();
+      *p.arg3.getNonnullConstChar();
+      *p.arg3.getConstNonnullConstChar();
+
+      *p.getT1().constUnknownChar;       // [[unsafe]] TODO: fix false positive.
+      *p.getT1().unknownConstChar;       // [[unsafe]] TODO: fix false positive.
+      *p.getT1().constUnknownConstChar;  // [[unsafe]] TODO: fix false positive.
+      *p.getT2().constNullableChar;      // [[unsafe]]
+      *p.getT2().nullableConstChar;      // [[unsafe]]
+      *p.getT2().constNullableConstChar;  // [[unsafe]]
+      *p.getT3().constNonnullChar;       // [[unsafe]] TODO: fix false positive.
+      *p.getT3().nonnullConstChar;       // [[unsafe]] TODO: fix false positive.
+      *p.getT3().constNonnullConstChar;  // [[unsafe]] TODO: fix false positive.
+
+      *p.getT1().getConstUnknownChar();
+      *p.getT1().getUnknownConstChar();
+      *p.getT1().getConstUnknownConstChar();
+      *p.getT2().getConstNullableChar();       // [[unsafe]]
+      *p.getT2().getNullableConstChar();       // [[unsafe]]
+      *p.getT2().getConstNullableConstChar();  // [[unsafe]]
+      *p.getT3().getConstNonnullChar();
+      *p.getT3().getNonnullConstChar();
+      *p.getT3().getConstNonnullConstChar();
+    }
+  )cc"));
+}
+
+// TODO: Fix false negatives.
+TEST(PointerNullabilityTest, MemberFunctionTemplateOfConcreteStruct) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct S {
+      template <typename T0>
+      T0 getT0();
+    };
+
+    void target(S p) {
+      *p.getT0<int *>();
+      *p.getT0<int *_Nonnull>();
+      *p.getT0<int *_Nullable>();  // TODO: fix false negative.
+
+      *p.getT0<int const *>();
+      *p.getT0<int *const>();
+      *p.getT0<int const *const>();
+      *p.getT0<int const *_Nonnull>();
+      *p.getT0<int *const _Nonnull>();
+      *p.getT0<int const *const _Nonnull>();
+      *p.getT0<int const *_Nullable>();        // TODO: fix false negative.
+      *p.getT0<int *const _Nullable>();        // TODO: fix false negative.
+      *p.getT0<int const *const _Nullable>();  // TODO: fix false negative.
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct S {
+      template <int I0, typename T1, int I2>
+      T1 getT1();
+    };
+
+    void target(S p) {
+      *p.getT1<0, int *, 1>();
+      *p.getT1<2147483647, int *_Nonnull, -2147483647>();
+      *p.getT1<4, int *_Nullable, 4>();  // TODO: fix false negative.
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, MemberFunctionTemplateOfTemplateStruct) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0>
+    struct S {
+      template <typename TN1>
+      TN1 getTN1();
+    };
+
+    void target(S<int> p) {
+      *p.getTN1<int *>();
+      *p.getTN1<int *_Nonnull>();
+      *p.getTN1<int *_Nullable>();  // TODO: fix false negative.
+
+      *p.getTN1<int const *>();
+      *p.getTN1<int *const>();
+      *p.getTN1<int const *const>();
+      *p.getTN1<int const *_Nonnull>();
+      *p.getTN1<int *const _Nonnull>();
+      *p.getTN1<int const *const _Nonnull>();
+      *p.getTN1<int const *_Nullable>();        // TODO: fix false negative.
+      *p.getTN1<int *const _Nullable>();        // TODO: fix false negative.
+      *p.getTN1<int const *const _Nullable>();  // TODO: fix false negative.
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0>
+    struct S {
+      template <int IN1, typename TN2, int IN3>
+      TN2 getTN2();
+    };
+
+    void target(S<int> p) {
+      *p.getTN2<0, int *, 1>();
+      *p.getTN2<2147483647, int *_Nonnull, -2147483647>();
+      *p.getTN2<4, int *_Nullable, 4>();  // TODO: fix false negative.
+    }
+  )cc"));
+}
+
+// TODO: Fix false positives.
+TEST(PointerNullabilityTest,
+     ClassTemplateInstantiationWithTemplateStructsAsParameters) {
+  // Class template with another class template as parameter
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 arg0;
+      T1 arg1;
+    };
+
+    template <typename TN0, typename TN1>
+    struct Struct2ArgNested {
+      Struct2Arg<TN1, Struct2Arg<TN0, TN1>>* arg0;
+      Struct2Arg<TN1, Struct2Arg<TN0, TN1>>* _Nullable arg1;
+    };
+
+    void target(Struct2ArgNested<int* _Nonnull, double* _Nullable> p) {
+      *p.arg0;
+      *p.arg1;  // [[unsafe]]
+
+      *p.arg0->arg0;
+      *p.arg0->arg1.arg0;
+      *p.arg0->arg1.arg1;
+    }
+  )cc"));
+
+  // Class template with itself as parameter
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 arg0;
+      T1 arg1;
+    };
+
+    void target(Struct2Arg<Struct2Arg<int*, int* _Nullable>, int* _Nonnull> p) {
+      *p.arg0.arg0;
+      *p.arg0.arg1;  // [[unsafe]]
+      *p.arg1;
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1, typename T2, typename T3, typename T4>
+    struct Struct5Arg {
+      T0 arg0;
+      T1 arg1;
+      T2 arg2;
+      T3 arg3;
+      T4 arg4;
+    };
+
+    void
+    target(Struct5Arg<
+           Struct5Arg<
+               Struct5Arg<Struct5Arg<int* _Nullable, int* _Nonnull,
+                                     float* _Nullable, int*, double* _Nullable>,
+                          int, int, int, int* _Nullable>,
+               int, int* _Nullable, int, int>,
+           int, int* _Nullable, int* _Nonnull, int>
+               p) {
+      *p.arg0.arg0.arg0.arg0;  // [[unsafe]]
+      *p.arg0.arg0.arg0.arg1;  // [[unsafe]] TODO: fix false positive.
+      *p.arg0.arg0.arg0.arg2;  // [[unsafe]]
+      *p.arg0.arg0.arg0.arg3;  // [[unsafe]] TODO: fix false positive.
+      *p.arg0.arg0.arg0.arg4;  // [[unsafe]]
+      *p.arg0.arg0.arg4;       // [[unsafe]]
+      *p.arg0.arg2;            // [[unsafe]]
+      *p.arg2;                 // [[unsafe]]
+      *p.arg3;
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, typename T2, typename T3, int I4,
+              typename T5, typename T6>
+    struct Struct7ArgWithInt {
+      T1 arg1;
+      T2 arg2;
+      T3 arg3;
+      T5 arg5;
+      T6 arg6;
+    };
+
+    void target(Struct7ArgWithInt<
+                0,
+                Struct7ArgWithInt<
+                    2147483647,
+                    Struct7ArgWithInt<
+                        0,
+                        Struct7ArgWithInt<-2147483647, int* _Nullable,
+                                          int* _Nonnull, float* _Nullable, 0,
+                                          int*, double* _Nullable>,
+                        int, int, 1, int, int* _Nullable>,
+                    int, int* _Nullable, 2147483647, int, int>,
+                int, int* _Nullable, 2, int* _Nonnull, int>
+                    p) {
+      *p.arg1.arg1.arg1.arg1;  // [[unsafe]]
+      *p.arg1.arg1.arg1.arg2;  // [[unsafe]] TODO: fix false positive.
+      *p.arg1.arg1.arg1.arg3;  // [[unsafe]]
+      *p.arg1.arg1.arg1.arg5;  // [[unsafe]] TODO: fix false positive.
+      *p.arg1.arg1.arg1.arg6;  // [[unsafe]]
+      *p.arg1.arg1.arg6;       // [[unsafe]]
+      *p.arg1.arg3;            // [[unsafe]]
+      *p.arg3;                 // [[unsafe]]
+      *p.arg5;
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest,
+     ClassTemplateInstantiationWithPointersToStructsAsParameters) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct3IntPtrs {
+      int* unknown;
+      int* _Nullable nullable;
+      int* _Nonnull nonnull;
+
+      int* getUnknown();
+      int* _Nullable getNullable();
+      int* _Nonnull getNonnull();
+    };
+
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+
+      T0 getT0();
+    };
+
+    void target(Struct1Arg<Struct3IntPtrs*> p) {
+      *p.arg0->unknown;
+      *p.arg0->nullable;  // [[unsafe]]
+      *p.arg0->nonnull;
+
+      *p.arg0->getUnknown();
+      *p.arg0->getNullable();  // [[unsafe]]
+      *p.arg0->getNonnull();
+
+      *p.getT0()->unknown;
+      *p.getT0()->nullable;  // [[unsafe]]
+      *p.getT0()->nonnull;
+
+      *p.getT0()->getUnknown();
+      *p.getT0()->getNullable();  // [[unsafe]]
+      *p.getT0()->getNonnull();
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct3IntPtrs {
+      int* unknown;
+      int* _Nullable nullable;
+      int* _Nonnull nonnull;
+
+      int* getUnknown();
+      int* _Nullable getNullable();
+      int* _Nonnull getNonnull();
+    };
+
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+
+      T0 getT0();
+    };
+
+    void target(Struct1Arg<Struct3IntPtrs* _Nullable> p) {
+      *p.arg0->unknown;   // [[unsafe]]
+      *p.arg0->nullable;  // [[unsafe]]
+      *p.arg0->nonnull;   // [[unsafe]]
+
+      *p.arg0->getUnknown();   // [[unsafe]]
+      *p.arg0->getNullable();  // [[unsafe]]
+      *p.arg0->getNonnull();   // [[unsafe]]
+
+      *p.getT0()->unknown;   // [[unsafe]]
+      *p.getT0()->nullable;  // [[unsafe]]
+      *p.getT0()->nonnull;   // [[unsafe]]
+
+      *p.getT0()->getUnknown();   // [[unsafe]]
+      *p.getT0()->getNullable();  // [[unsafe]]
+      *p.getT0()->getNonnull();   // [[unsafe]]
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct3IntPtrs {
+      int* unknown;
+      int* _Nullable nullable;
+      int* _Nonnull nonnull;
+
+      int* getUnknown();
+      int* _Nullable getNullable();
+      int* _Nonnull getNonnull();
+    };
+
+    template <typename T0>
+    struct Struct1Arg {
+      T0 arg0;
+
+      T0 getT0();
+    };
+
+    void target(Struct1Arg<Struct3IntPtrs* _Nonnull> p) {
+      *p.arg0->unknown;
+      *p.arg0->nullable;  // [[unsafe]]
+      *p.arg0->nonnull;
+
+      *p.arg0->getUnknown();
+      *p.arg0->getNullable();  // [[unsafe]]
+      *p.arg0->getNonnull();
+
+      *p.getT0()->unknown;
+      *p.getT0()->nullable;  // [[unsafe]]
+      *p.getT0()->nonnull;
+
+      *p.getT0()->getUnknown();
+      *p.getT0()->getNullable();  // [[unsafe]]
+      *p.getT0()->getNonnull();
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Struct3IntPtrs {
+      int* unknown;
+      int* _Nullable nullable;
+      int* _Nonnull nonnull;
+
+      int* getUnknown();
+      int* _Nullable getNullable();
+      int* _Nonnull getNonnull();
+    };
+
+    template <int I0, typename T1>
+    struct Struct2Arg {
+      T1 arg1;
+
+      T1 getT1();
+    };
+
+    void target(Struct2Arg<0, Struct3IntPtrs*> p) {
+      *p.arg1->unknown;
+      *p.arg1->nullable;  // [[unsafe]]
+      *p.arg1->nonnull;
+
+      *p.arg1->getUnknown();
+      *p.arg1->getNullable();  // [[unsafe]]
+      *p.arg1->getNonnull();
+
+      *p.getT1()->unknown;
+      *p.getT1()->nullable;  // [[unsafe]]
+      *p.getT1()->nonnull;
+      *p.getT1()->getUnknown();
+      *p.getT1()->getNullable();  // [[unsafe]]
+      *p.getT1()->getNonnull();
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest,
+     ClassTemplateInstantiationWithPointersToTemplateStructsAsParameters) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 arg0;
+      T1 arg1;
+
+      T0 getT0();
+      T1 getT1();
+    };
+
+    void target(Struct2Arg<Struct2Arg<int *, int *_Nullable> *_Nullable,
+                           Struct2Arg<int, int *> *_Nonnull>
+                    p) {
+      *p.arg0;        // [[unsafe]]
+      *p.arg0->arg0;  // [[unsafe]]
+      *p.arg0->arg1;  // [[unsafe]]
+      *p.arg1;
+      *p.arg1->arg1;
+
+      *p.arg0->getT0();  // [[unsafe]]
+      *p.arg0->getT1();  // [[unsafe]]
+      *p.arg1->getT1();
+
+      *p.getT0();        // [[unsafe]]
+      *p.getT0()->arg0;  // [[unsafe]]
+      *p.getT0()->arg1;  // [[unsafe]]
+      *p.getT1();
+      *p.getT1()->arg1;
+
+      *p.getT0()->getT0();  // [[unsafe]]
+      *p.getT0()->getT1();  // [[unsafe]]
+      *p.getT1()->getT1();
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct StructNonnullUnknown {
+      T0 nonnull;
+      T1 unknown;
+
+      T0 getNonnull();
+      T1 getUnknown();
+    };
+
+    template <typename T0, typename T1>
+    struct StructNonnullNullable {
+      T0 nonnull;
+      T1 nullable;
+
+      T0 getNonnull();
+      T1 getNullable();
+    };
+
+    template <typename T0, typename T1>
+    struct StructNullableNonnull {
+      T0 nullable;
+      T1 nonnull;
+
+      T0 getNullable();
+      T1 getNonnull();
+    };
+
+    template <typename T0, typename T1>
+    struct StructNullableNullable {
+      T0 nullable0;
+      T1 nullable1;
+
+      T0 getNullable0();
+      T1 getNullable1();
+    };
+
+    template <typename T0, typename T1>
+    struct StructNullableUnknown {
+      T0 nullable;
+      T1 unknown;
+
+      T0 getNullable();
+      T1 getUnknown();
+    };
+
+    template <typename T0, typename T1>
+    struct StructUnknownNullable {
+      T0 unknown;
+      T1 nullable;
+
+      T0 getUnknown();
+      T1 getNullable();
+    };
+
+    void
+    target(StructNonnullUnknown<
+           StructNonnullNullable<
+               StructNullableNullable<int* _Nullable, int* _Nullable>* _Nonnull,
+               StructUnknownNullable<int*,
+                                     int* _Nullable>* _Nullable>* _Nonnull,
+           StructUnknownNullable<
+               StructUnknownNullable<int*, int* _Nullable>*,
+               StructNullableNonnull<int* _Nullable,
+                                     int* _Nonnull>* _Nullable>*>
+               p) {
+      *p.nonnull;
+      *p.nonnull->nonnull;
+      *p.nonnull->nonnull->nullable0;  // TODO: fix false negative.
+      *p.nonnull->nonnull->nullable1;  // TODO: fix false negative.
+      *p.nonnull->nullable;            // TODO: fix false negative.
+      *p.nonnull->nullable->unknown;   // TODO: fix false negative.
+      *p.nonnull->nullable->nullable;  // TODO: fix false negative.
+      *p.unknown->unknown;
+      *p.unknown->unknown->unknown;
+      *p.unknown->unknown->nullable;  // TODO: fix false negative.
+      *p.unknown;
+      *p.unknown->nullable;            // TODO: fix false negative.
+      *p.unknown->nullable->nullable;  // TODO: fix false negative.
+      *p.unknown->nullable->nonnull;   // TODO: fix false negative.
+
+      *p.nonnull->getNonnull();
+      *p.nonnull->getNonnull()->nullable0;  // TODO: fix false negative.
+      *p.nonnull->getNonnull()->nullable1;  // TODO: fix false negative.
+      *p.nonnull->getNullable();
+      *p.nonnull->getNullable()->unknown;   // TODO: fix false negative.
+      *p.nonnull->getNullable()->nullable;  // TODO: fix false negative.
+      *p.unknown->getUnknown();
+      *p.unknown->getUnknown()->unknown;
+      *p.unknown->getUnknown()->nullable;   // TODO: fix false negative.
+      *p.unknown->getNullable();            // TODO: fix false negative.
+      *p.unknown->getNullable()->nullable;  // TODO: fix false negative.
+      *p.unknown->getNullable()->nonnull;   // TODO: fix false negative.
+
+      *p.nonnull->getNonnull()->getNullable0();  // TODO: fix false negative.
+      *p.nonnull->getNonnull()->getNullable1();  // TODO: fix false negative.
+      *p.nonnull->getNullable()->getUnknown();   // TODO: fix false negative.
+      *p.nonnull->getNullable()->getNullable();  // TODO: fix false negative.
+      *p.unknown->getUnknown()->getUnknown();
+      *p.unknown->getUnknown()->getNullable();   // TODO: fix false negative.
+      *p.unknown->getNullable()->getNullable();  // TODO: fix false negative.
+      *p.unknown->getNullable()->getNonnull();   // TODO: fix false negative.
+
+      *p.nonnull->nonnull->getNullable0();  // TODO: fix false negative.
+      *p.nonnull->nonnull->getNullable1();  // TODO: fix false negative.
+      *p.nonnull->nullable->getUnknown();   // TODO: fix false negative.
+      *p.nonnull->nullable->getNullable();  // TODO: fix false negative.
+      *p.unknown->unknown->getUnknown();
+      *p.unknown->unknown->getNullable();   // TODO: fix false negative.
+      *p.unknown->nullable->getNullable();  // TODO: fix false negative.
+      *p.unknown->nullable->getNonnull();   // TODO: fix false negative.
+
+      *p.getNonnull();
+      *p.getNonnull()->nonnull;
+      *p.getNonnull()->nonnull->nullable0;  // TODO: fix false negative.
+      *p.getNonnull()->nonnull->nullable1;  // TODO: fix false negative.
+      *p.getNonnull()->nullable;            // TODO: fix false negative.
+      *p.getNonnull()->nullable->unknown;   // TODO: fix false negative.
+      *p.getNonnull()->nullable->nullable;  // TODO: fix false negative.
+      *p.getUnknown()->unknown;
+      *p.getUnknown()->unknown->unknown;
+      *p.getUnknown()->unknown->nullable;  // TODO: fix false negative.
+      *p.getUnknown();
+      *p.getUnknown()->nullable;            // TODO: fix false negative.
+      *p.getUnknown()->nullable->nullable;  // TODO: fix false negative.
+      *p.getUnknown()->nullable->nonnull;   // TODO: fix false negative.
+
+      *p.getNonnull()->getNonnull();
+      *p.getNonnull()->getNonnull()->nullable0;  // TODO: fix false negative.
+      *p.getNonnull()->getNonnull()->nullable1;  // TODO: fix false negative.
+      *p.getNonnull()->getNullable();            // TODO: fix false negative.
+      *p.getNonnull()->getNullable()->unknown;   // TODO: fix false negative.
+      *p.getNonnull()->getNullable()->nullable;  // TODO: fix false negative.
+      *p.getUnknown()->getUnknown();
+      *p.getUnknown()->getUnknown()->unknown;
+      *p.getUnknown()->getUnknown()->nullable;   // TODO: fix false negative.
+      *p.getUnknown()->getNullable();            // TODO: fix false negative.
+      *p.getUnknown()->getNullable()->nullable;  // TODO: fix false negative.
+      *p.getUnknown()->getNullable()->nonnull;   // TODO: fix false negative.
+
+      *p.getNonnull()->nonnull->getNullable0();  // TODO: fix false negative.
+      *p.getNonnull()->nonnull->getNullable1();  // TODO: fix false negative.
+      *p.getNonnull()->nullable->getUnknown();   // TODO: fix false negative.
+      *p.getNonnull()->nullable->getNullable();  // TODO: fix false negative.
+      *p.getUnknown()->unknown->getUnknown();
+      *p.getUnknown()->unknown->getNullable();   // TODO: fix false negative.
+      *p.getUnknown()->nullable->getNullable();  // TODO: fix false negative.
+      *p.getUnknown()->nullable->getNonnull();   // TODO: fix false negative.
+
+      *p.getNonnull()->getNonnull()->getNullable0();  // TODO: fix false
+                                                      // negative.
+      *p.getNonnull()->getNonnull()->getNullable1();  // TODO: fix false
+                                                      // negative.
+      *p.getNonnull()->getNullable()->getUnknown();   // TODO: fix false
+                                                      // negative.
+      *p.getNonnull()->getNullable()->getNullable();  // TODO: fix false
+                                                      // negative.
+      *p.getUnknown()->getUnknown()->getUnknown();
+      *p.getUnknown()->getUnknown()->getNullable();   // TODO: fix false
+                                                      // negative.
+      *p.getUnknown()->getNullable()->getNullable();  // TODO: fix false
+                                                      // negative.
+      *p.getUnknown()->getNullable()->getNonnull();   // TODO: fix false
+                                                      // negative.
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, FunctionTemplates) {
+  // Call expression that returns the first of two type parameters.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    T0 returnFirst();
+
+    void target() {
+      *returnFirst<int *_Nonnull, int *_Nullable>();
+      *returnFirst<int *, int *_Nullable>();
+      *returnFirst<int *_Nullable, int *_Nonnull>();  // [[unsafe]]
+    }
+  )cc"));
+
+  // Call expression that returns the second of two type parameters.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    T1 returnSecond();
+
+    void target() {
+      *returnSecond<int *_Nullable, int *_Nonnull>();
+      *returnSecond<int *_Nullable, int *>();
+      *returnSecond<int *, int *_Nullable>();  // [[unsafe]]
+    }
+  )cc"));
+
+  // Call expression that has an int parameter and two type parameters,
+  // returning the first type parameter.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <int I0, typename T1, typename T2>
+    T1 fn3ArgWithInt();
+
+    void target() {
+      *fn3ArgWithInt<1, int *_Nullable, int *>();  // [[unsafe]]
+      *fn3ArgWithInt<1, int *, int *_Nullable>();
+    }
+  )cc"));
+
+  // Call expression with template parameter substituted with a concrete struct.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct StructUnknownNullable {
+      int *var0;
+      int *_Nullable var1;
+
+      int *getVar0();
+      int *_Nullable getVar1();
+    };
+
+    template <typename T0, typename T1>
+    T1 returnSecond();
+
+    void target() {
+      *returnSecond<StructUnknownNullable, int *_Nullable>();  // [[unsafe]]
+      *returnSecond<int *_Nonnull, StructUnknownNullable *>();
+      // TODO: The following line is a false positive. We correctly compute the
+      // nullability of the expression, as confirmed by the call to
+      // `assert_nullability`. However, the dataflow framework currently does
+      // not model pointer values for this expression, which results in a (in
+      // this case incorrect) nullptr value.
+      *returnSecond<int *_Nonnull, StructUnknownNullable>()  // [[unsafe]]
+           .var0;  // TODO: Fix false positive.
+      __assert_nullability<NK_unspecified>(
+          returnSecond<int *_Nonnull, StructUnknownNullable>().var0);
+      *returnSecond<int *_Nonnull, StructUnknownNullable>().var1;  // [[unsafe]]
+      *returnSecond<int *_Nonnull, StructUnknownNullable>().getVar0();
+      *returnSecond<int *_Nonnull, StructUnknownNullable>()  // [[unsafe]]
+           .getVar1();
+    }
+  )cc"));
+}
+
+TEST(PointerNullabilityTest, ParenTypeInTemplate) {
+  checkDiagnostics(R"cc(
+    template <typename T>
+    struct S {
+      T(a);
+      T(*(b));
+
+      T (*f)();
+      T(((*g)))();
+    };
+
+    void targetNullable(S<int* _Nullable> s) {
+      *s.a;   // [[unsafe]]
+      **s.b;  // [[unsafe]]
+      *s.f;
+      *s.g;
+      // TODO: Handle function pointers. The analysis currently crashes.
+      // *s.f();
+      // *s.g();
+    }
+
+    void targetNonnull(S<int* _Nonnull> s) {
+      *s.a;
+      **s.b;
+      *s.f;
+      *s.g;
+      // TODO: Handle function pointers. The analysis currently crashes.
+      // *s.f();
+      // *s.g();
+    }
+  )cc");
+
+  checkDiagnostics(R"cc(
+    template <typename T>
+    struct S {
+      T arg;
+    };
+
+    void targetNullable(S<int* _Nullable>(a), S<int* _Nullable>(*(b)),
+                        S<int(*_Nullable)> c, S<int*(*(*_Nullable))> d,
+                        S<int* _Nullable (*)()> e) {
+      *a.arg;    // [[unsafe]]
+      *b->arg;   // [[unsafe]]
+      *c.arg;    // [[unsafe]]
+      ***d.arg;  // [[unsafe]]
+      *e.arg;    // [[unsafe]]
+
+      // TODO: Handle function pointers. The analysis currently crashes.
+      // *e.arg();
+    }
+
+    void targetNonnull(S<int* _Nonnull>(a), S<int* _Nonnull>(*(b)),
+                       S<int(*_Nonnull)> c, S<int*(*(*_Nonnull))> d,
+                       S<int* _Nonnull (*)()> e) {
+      *a.arg;
+      *b->arg;
+      *c.arg;
+      ***d.arg;
+      *e.arg;
+
+      // TODO: Handle function pointers. The analysis currently crashes.
+      // *e.arg();
+    }
+  )cc");
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/temporary_materialization.cc b/nullability/test/temporary_materialization.cc
new file mode 100644
index 0000000..4851c88
--- /dev/null
+++ b/nullability/test/temporary_materialization.cc
@@ -0,0 +1,80 @@
+// 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
+
+// Tests for temporary materialization.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, NonFlowSensitiveMaterializeTemporaryExpr) {
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    int *_Nonnull makeNonnull();
+    int *_Nullable makeNullable();
+    int *makeUnannotated();
+
+    template <typename T>
+    T identity(const T &);
+
+    void target() {
+      {
+        *identity<int *_Nonnull>(makeNonnull());
+        int *const &p = makeNonnull();
+        *p;
+      }
+      {
+        *identity<int *_Nullable>(makeNullable());  // [[unsafe]]
+        int *const &p = makeNullable();
+        *p;  // [[unsafe]]
+      }
+      {
+        *identity<int *>(makeUnannotated());
+        int *const &p = makeUnannotated();
+        *p;
+      }
+    }
+  )cc"));
+
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    template <typename T0, typename T1>
+    struct Struct2Arg {
+      T0 getT0();
+      T1 getT1();
+    };
+
+    template <typename T>
+    T make();
+
+    template <typename T>
+    T identity(const T &);
+
+    void target(Struct2Arg<int *, int *_Nullable> &p) {
+      *identity<Struct2Arg<int *, int *_Nullable>>(p).getT0();
+      *identity<Struct2Arg<int *, int *_Nullable>>(
+           make<Struct2Arg<int *, int *_Nullable>>())
+           .getT0();
+      *identity<Struct2Arg<int *, int *_Nullable>>(
+           Struct2Arg<int *, int *_Nullable>(p))
+           .getT0();
+      *identity<int *>(p.getT0());
+      *identity<Struct2Arg<int *, int *_Nullable>>(p).getT1();  // [[unsafe]]
+      *identity<Struct2Arg<int *, int *_Nullable>>(             // [[unsafe]]
+           make<Struct2Arg<int *, int *_Nullable>>())
+           .getT1();
+      *identity<Struct2Arg<int *, int *_Nullable>>(  // [[unsafe]]
+           Struct2Arg<int *, int *_Nullable>(p))
+           .getT1();
+      *identity<int *_Nullable>(p.getT1());  // [[unsafe]]
+    }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/this_pointer.cc b/nullability/test/this_pointer.cc
new file mode 100644
index 0000000..cc4ed74
--- /dev/null
+++ b/nullability/test/this_pointer.cc
@@ -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
+
+// Tests for the treatment of the `this` pointer (which is always nonnull).
+#include <optional>
+#include <set>
+#include <string>
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, ThisPointer) {
+  // (->) implicit `this`
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      void foo();
+      void target() { foo(); }
+    };
+  )cc"));
+
+  // (->) explicit `this`
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    struct Foo {
+      void foo();
+      void target() { this->foo(); }
+    };
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/test/variance.cc b/nullability/test/variance.cc
new file mode 100644
index 0000000..2bb41da
--- /dev/null
+++ b/nullability/test/variance.cc
@@ -0,0 +1,29 @@
+// 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
+
+// Tests for correct treatment of type variance.
+
+#include "nullability/test/check_diagnostics.h"
+#include "third_party/llvm/llvm-project/third-party/unittest/googletest/include/gtest/gtest.h"
+
+namespace clang {
+namespace tidy {
+namespace nullability {
+namespace {
+
+TEST(PointerNullabilityTest, NonConstPointerIsInvariant) {
+  // TODO(b/275458593): This test demonstrates a bug in the checker. The call
+  // `target(pp)` should be flagged as an error because non-const pointers are
+  // invariant over their pointee type and we should therefore not allow `int *
+  // _Nonnull * _Nonnull` to be converted to `int * _Nullable * _Nonnull`.
+  EXPECT_TRUE(checkDiagnostics(R"cc(
+    void callee(int* _Nullable* _Nonnull pp);
+    void target(int* _Nonnull* _Nonnull pp) { target(pp); }
+  )cc"));
+}
+
+}  // namespace
+}  // namespace nullability
+}  // namespace tidy
+}  // namespace clang
diff --git a/nullability/type_nullability.cc b/nullability/type_nullability.cc
new file mode 100644
index 0000000..307239d
--- /dev/null
+++ b/nullability/type_nullability.cc
@@ -0,0 +1,106 @@
+// 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 "absl/log/check.h"
+#include "nullability/pointer_nullability.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclTemplate.h"
+#include "clang/AST/Type.h"
+#include "clang/AST/TypeVisitor.h"
+#include "clang/Basic/LLVM.h"
+#include "clang/Basic/Specifiers.h"
+
+namespace clang::tidy::nullability {
+namespace {
+
+// Visitor to rebuild a QualType with explicit nullability.
+// Extra AttributedType nodes are added wrapping interior PointerTypes, and
+// other sugar is added as needed to allow this (e.g. TypeSpecializationType).
+//
+// We only have to handle types that have nontrivial nullability vectors, i.e.
+// those handled by NullabilityWalker.
+// Additionally, we only operate on canonical types (otherwise the sugar we're
+// adding could conflict with existing sugar).
+//
+// This needs to stay in sync with the other algorithms that manipulate
+// nullability data structures for particular types: the non-flow-sensitive
+// transfer and NullabilityWalker.
+struct Visitor : public TypeVisitor<Visitor, QualType> {
+  Visitor(ArrayRef<NullabilityKind> Nullability, ASTContext& Ctx)
+      : Nullability(Nullability), Ctx(Ctx) {}
+
+  bool done() const { return Nullability.empty(); }
+
+  using Base = TypeVisitor<Visitor, QualType>;
+  using Base::Visit;
+  QualType Visit(QualType T) {
+    if (T.isNull()) return T;
+    return Ctx.getQualifiedType(Visit(T.getTypePtr()), T.getLocalQualifiers());
+  }
+  TemplateArgument Visit(TemplateArgument TA) {
+    if (TA.getKind() == TemplateArgument::Type)
+      return TemplateArgument(Visit(TA.getAsType()));
+    return TA;
+  }
+
+  // Default behavior for unhandled types: do not transform.
+  QualType VisitType(const Type* T) { return QualType(T, 0); }
+
+  QualType VisitPointerType(const PointerType* PT) {
+    CHECK(!Nullability.empty())
+        << "Nullability vector too short at " << QualType(PT, 0).getAsString();
+    NullabilityKind NK = Nullability.front();
+    Nullability = Nullability.drop_front();
+
+    QualType Rebuilt = Ctx.getPointerType(Visit(PT->getPointeeType()));
+    if (NK == NullabilityKind::Unspecified) return Rebuilt;
+    return Ctx.getAttributedType(AttributedType::getNullabilityAttrKind(NK),
+                                 Rebuilt, Rebuilt);
+  }
+
+  QualType VisitRecordType(const RecordType* RT) {
+    if (const auto* CTSD =
+            dyn_cast<ClassTemplateSpecializationDecl>(RT->getDecl())) {
+      std::vector<TemplateArgument> TransformedArgs;
+      for (const auto& Arg : CTSD->getTemplateArgs().asArray())
+        TransformedArgs.push_back(Visit(Arg));
+      return Ctx.getTemplateSpecializationType(
+          TemplateName(CTSD->getSpecializedTemplate()), TransformedArgs,
+          QualType(RT, 0));
+    }
+    return QualType(RT, 0);
+  }
+
+  QualType VisitFunctionProtoType(const FunctionProtoType* T) {
+    QualType Ret = Visit(T->getReturnType());
+    std::vector<QualType> Params;
+    for (const auto& Param : T->getParamTypes()) Params.push_back(Visit(Param));
+    return Ctx.getFunctionType(Ret, Params, T->getExtProtoInfo());
+  }
+
+ private:
+  ArrayRef<NullabilityKind> Nullability;
+  ASTContext& Ctx;
+};
+
+}  // namespace
+
+QualType rebuildWithNullability(QualType T,
+                                ArrayRef<NullabilityKind> Nullability,
+                                ASTContext& Ctx) {
+  Visitor V(Nullability, Ctx);
+  QualType Result = V.Visit(T.getCanonicalType());
+  CHECK(V.done()) << "Nullability vector[" << Nullability.size()
+                  << "] too long for " << T.getAsString();
+  return Result;
+}
+
+std::string printWithNullability(QualType T,
+                                 ArrayRef<NullabilityKind> Nullability,
+                                 ASTContext& Ctx) {
+  return rebuildWithNullability(T, Nullability, Ctx)
+      .getAsString(Ctx.getPrintingPolicy());
+}
+
+}  // namespace clang::tidy::nullability